Compare commits

...

32 Commits

Author SHA1 Message Date
d3ede8546b update link_view.dart (#107) 2023-01-19 01:15:45 -08:00
53562ad260 fix loading indicator. (#106) 2023-01-19 00:23:28 -08:00
6c8e7a7cb9 update loading indicator. (#105) 2023-01-18 16:10:25 -08:00
56c0245335 fix StoryTile. (#104) 2023-01-18 14:31:49 -08:00
0cbd38a530 fix StoryTile. (#103) 2023-01-17 23:47:04 -08:00
7c6da2c36a fix story tile overflow. (#102) 2023-01-17 23:02:57 -08:00
185140feb4 show url in story tile. (#101) 2023-01-17 21:49:10 -08:00
03c01a0b78 bump versions. (#97) 2022-12-27 13:56:21 -08:00
f823fdf241 use version from pubspec.yaml (#95) (#96) 2022-12-27 13:40:41 -08:00
fe87ddd8ff add (#95) 2022-12-27 13:36:14 -08:00
613ba12b05 fix time machine. (#94)
* bumped version.

* fix time machine.
2022-12-27 12:54:18 -08:00
8d7f66ecbc add timeout. (#93) 2022-12-27 01:17:16 -08:00
461aae253b update publish_ios.yml (#92) 2022-12-27 00:22:04 -08:00
a1b491cf0d fix regex for getting item id. (#91) 2022-12-27 00:00:10 -08:00
edf0c82040 Improve loading mechanism. (#90)
* load more comments when user folds the last comment.

* improvements.

* improve loading experience.
2022-12-26 22:55:50 -08:00
946a3c5a9a Improve loading mechanism. (#89)
* load more comments when user folds the last comment.

* improvements.
2022-12-26 22:09:31 -08:00
d8bc60c071 Add tests. (#88)
* update fontsize.

* fix title.

* fix info list.

* add small.

* nit.

* nit.

* test.

* add tests.

* update github action.
2022-12-25 20:12:11 -08:00
48477cd5c8 Fix comment tile. (#87)
* fix comment not correctly collapsing.

* fix comment tile overflow.

* bumped version to 1.0.0
2022-12-25 00:58:14 -08:00
38df6293fe update comment tile. (#86) 2022-12-22 19:32:57 -08:00
a5fe9e45fc fix NavigationModePreference 2022-12-21 11:09:59 -08:00
9de5baa77a bumped version. (#85) 2022-12-20 23:34:45 -08:00
2daccd64e8 update fontsize. (#84)
* update fontsize.

* fix title.

* fix info list.

* add small.

* nit.

* nit.
2022-12-20 22:49:33 -08:00
d0c68f9419 update Fastfile. 2022-12-20 22:10:49 -08:00
5f1dbfc510 update Fastfile 2022-12-20 21:58:45 -08:00
90eee37c17 update Fastfile 2022-12-20 21:25:21 -08:00
5630e61a74 update Fastfile 2022-12-20 21:02:34 -08:00
eaad4b01dd fix ci. (#83)
* fix ci.

* update project.

* update github checks.

* update github checks.

* nit.

* nit.

* update fastfile.

* fix info.plist

* nit.

* nit.

* nit.

* nit.

* nit.

* nit.

* nit.

* update publish_ios.yml
2022-12-20 20:37:49 -08:00
3ab172f3d3 update publish_ios.yml 2022-12-19 13:51:57 -08:00
5450eba64b fix RegExp. (#82)
* fix regexp.

* bump version.
2022-12-19 13:51:10 -08:00
e2d6bb44d0 update publish_ios.yml 2022-12-19 13:46:00 -08:00
ffbd3a2449 add flutter as submodule (#80)
* add flutter as submodule

* move flutter to submodules.

* removed unused file.

* nit.
2022-12-18 18:33:46 -08:00
2405a6d30c update publish_ios.yml 2022-12-17 18:48:16 -08:00
51 changed files with 1698 additions and 1268 deletions

View File

@ -4,11 +4,13 @@ on:
push: push:
branches: branches:
- "**" - "**"
- '!master'
jobs: jobs:
releases: releases:
name: Check commit name: Check commit
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30
env: env:
FLUTTER_VERSION: "3.3.10" FLUTTER_VERSION: "3.3.10"
steps: steps:
@ -20,3 +22,4 @@ jobs:
- run: flutter pub get - run: flutter pub get
- run: flutter format --set-exit-if-changed . - run: flutter format --set-exit-if-changed .
- run: flutter analyze - run: flutter analyze
- run: flutter test

View File

@ -6,13 +6,12 @@ on:
# Run the workflow whenever a new tag named 'v*' is pushed # Run the workflow whenever a new tag named 'v*' is pushed
push: push:
branches: branches:
- "!*" - master
tags:
- "v*"
jobs: jobs:
build_and_publish: build_and_publish:
runs-on: macos-latest runs-on: macos-latest
timeout-minutes: 30
env: env:
# Point the `ruby/setup-ruby` action at this Gemfile, so it # Point the `ruby/setup-ruby` action at this Gemfile, so it
@ -51,4 +50,4 @@ jobs:
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
SSH_AUTH_SOCK: /tmp/ssh_agent.sock SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: cd ios && bundle exec fastlane beta "build_name:${{ github.ref_name }}" run: cd ios && bundle exec fastlane beta

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "flutter"]
path = submodules/flutter
url = https://github.com/flutter/flutter

View File

@ -7,3 +7,7 @@ linter:
omit_local_variable_types: false omit_local_variable_types: false
one_member_abstracts: false one_member_abstracts: false
always_specify_types: true always_specify_types: true
analyzer:
exclude:
- "submodules/**"

View File

@ -1,5 +1,82 @@
import Flutter import Flutter
import UIKit import UIKit
import Foundation
typealias APNSHandler = ()->Void
let keyKey = "key"
let valKey = "val"
final class SharedPrefsCore {
fileprivate static let shared: SharedPrefsCore = SharedPrefsCore()
fileprivate func setBool(key: String?, val: Bool?) -> Bool {
guard let key = key,
let val = val else {
return false
}
let keyStore = NSUbiquitousKeyValueStore()
let allVals = keyStore.dictionaryRepresentation;
let allKeys = allVals.keys
// Limit is 1024, reserve rest slots for fav and pins.
if allKeys.count >= 1000 {
for key in allKeys.filter({ $0.contains("hasRead") }) {
keyStore.removeObject(forKey: key)
}
}
keyStore.set(val, forKey: key)
return true
}
fileprivate func getBool(key: String?) -> Bool {
guard let key = key else {
return false
}
let keyStore = NSUbiquitousKeyValueStore()
let val = keyStore.bool(forKey: key)
return val
}
fileprivate func setStringList(key: String?, val: [String]?) -> Bool {
guard let key = key,
let val = val else {
return false
}
let keyStore = NSUbiquitousKeyValueStore()
keyStore.set(val, forKey: key)
return true
}
fileprivate func getStringList(key: String?) -> [Any] {
guard let key = key else {
return [Any]()
}
let keyStore = NSUbiquitousKeyValueStore()
let list = keyStore.array(forKey: key) as [Any]? ?? [Any]()
return list
}
fileprivate func clearAll() -> Bool{
let keyStore = NSUbiquitousKeyValueStore()
let allVals = keyStore.dictionaryRepresentation;
let allKeys = allVals.keys
for key in allKeys.filter({ $0.contains("hasRead") }) {
keyStore.removeObject(forKey: key)
}
return true
}
}
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin { public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) { public static func register(with registrar: FlutterPluginRegistrar) {
@ -12,41 +89,44 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
switch call.method { switch call.method {
case "setBool": case "setBool":
if let params = call.arguments as? [String: Any] { if let params = call.arguments as? [String: Any] {
let info: [String: Any] = ["result": result, let val = params[valKey] as? Bool
"params": params] let key = params[keyKey] as? String
NotificationCenter.default.post(name: Notification.Name("setBool"), object: nil, userInfo: info)
let res = SharedPrefsCore.shared.setBool(key: key, val: val)
result(res)
} }
return return
case "getBool": case "getBool":
if let params = call.arguments as? [String: Any] { if let params = call.arguments as? [String: Any] {
let info: [String: Any] = ["result": result, let key = params[keyKey] as? String
"params": params] let res = SharedPrefsCore.shared.getBool(key: key)
NotificationCenter.default.post(name: Notification.Name("getBool"), object: nil, userInfo: info) result(res)
} }
return return
case "setStringList": case "setStringList":
if let params = call.arguments as? [String: Any] { if let params = call.arguments as? [String: Any] {
let info: [String: Any] = ["result": result, let val = params[valKey] as? [String]
"params": params] let key = params[keyKey] as? String
NotificationCenter.default.post(name: Notification.Name("setStringList"), object: nil, userInfo: info)
let res = SharedPrefsCore.shared.setStringList(key: key, val: val)
result(res)
} }
return return
case "getStringList": case "getStringList":
if let params = call.arguments as? [String: Any] { if let params = call.arguments as? [String: Any] {
let info: [String: Any] = ["result": result, let key = params[keyKey] as? String
"params": params] let res = SharedPrefsCore.shared.getStringList(key: key)
NotificationCenter.default.post(name: Notification.Name("getStringList"), object: nil, userInfo: info) result(res)
} }
return return
case "clearAll": case "clearAll":
if let params = call.arguments as? [String: Any] { if let params = call.arguments as? [String: Any] {
let info: [String: Any] = ["result": result, let res = SharedPrefsCore.shared.clearAll()
"params": params] result(res)
NotificationCenter.default.post(name: Notification.Name("clearAll"), object: nil, userInfo: info)
} }
return return

View File

@ -0,0 +1,2 @@
- Fixed app icon.
- Added font size setting to comments screen.

View File

@ -0,0 +1 @@
- Fixed time machine.

View File

@ -0,0 +1 @@
- Fixed time machine.

View File

@ -17,20 +17,20 @@ GEM
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.636.0) aws-partitions (1.680.0)
aws-sdk-core (3.154.0) aws-sdk-core (3.168.4)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.58.0) aws-sdk-kms (1.61.0)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0) aws-sdk-s3 (1.117.2)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.1) aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
claide (1.1.0) claide (1.1.0)
@ -86,7 +86,7 @@ GEM
escape (0.0.4) escape (0.0.4)
ethon (0.15.0) ethon (0.15.0)
ffi (>= 1.15.0) ffi (>= 1.15.0)
excon (0.92.5) excon (0.95.0)
faraday (1.10.2) faraday (1.10.2)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
@ -116,7 +116,7 @@ GEM
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.6) fastimage (2.2.6)
fastlane (2.210.1) fastlane (2.211.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@ -159,9 +159,9 @@ GEM
fourflusher (2.3.1) fourflusher (2.3.1)
fuzzy_match (2.0.4) fuzzy_match (2.0.4)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.27.0) google-apis-androidpublisher_v3 (0.32.0)
google-apis-core (>= 0.7.2, < 2.a) google-apis-core (>= 0.9.1, < 2.a)
google-apis-core (0.9.0) google-apis-core (0.9.2)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@ -170,27 +170,27 @@ GEM
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick webrick
google-apis-iamcredentials_v1 (0.14.0) google-apis-iamcredentials_v1 (0.16.0)
google-apis-core (>= 0.7.2, < 2.a) google-apis-core (>= 0.9.1, < 2.a)
google-apis-playcustomapp_v1 (0.10.0) google-apis-playcustomapp_v1 (0.12.0)
google-apis-core (>= 0.7, < 2.a) google-apis-core (>= 0.9.1, < 2.a)
google-apis-storage_v1 (0.17.0) google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.7, < 2.a) google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.0) google-cloud-errors (1.3.0)
google-cloud-storage (1.42.0) google-cloud-storage (1.44.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.17.0) google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.2.0) googleauth (1.3.0)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
@ -203,11 +203,11 @@ GEM
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.12.0) i18n (1.12.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jmespath (1.6.1) jmespath (1.6.2)
json (2.6.2) json (2.6.3)
jwt (2.5.0) jwt (2.5.0)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.2)
minitest (5.16.3) minitest (5.16.3)
molinillo (0.8.0) molinillo (0.8.0)

View File

@ -1,6 +1,3 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -22,6 +22,8 @@ PODS:
- integration_test (0.0.1): - integration_test (0.0.1):
- Flutter - Flutter
- OrderedSet (5.0.0) - OrderedSet (5.0.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_ios (0.0.1): - path_provider_ios (0.0.1):
- Flutter - Flutter
- ReachabilitySwift (5.0.0) - ReachabilitySwift (5.0.0)
@ -53,6 +55,7 @@ DEPENDENCIES:
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`) - flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
@ -85,6 +88,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_siri_suggestions/ios" :path: ".symlinks/plugins/flutter_siri_suggestions/ios"
integration_test: integration_test:
:path: ".symlinks/plugins/integration_test/ios" :path: ".symlinks/plugins/integration_test/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios: path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios" :path: ".symlinks/plugins/path_provider_ios/ios"
receive_sharing_intent: receive_sharing_intent:
@ -116,6 +121,7 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1 receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
@ -128,6 +134,6 @@ SPEC CHECKSUMS:
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
COCOAPODS: 1.11.3 COCOAPODS: 1.11.3

View File

@ -21,7 +21,6 @@
E530B1AD283B54DA004E8EB6 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E530B1AC283B54DA004E8EB6 /* ActionViewController.swift */; }; E530B1AD283B54DA004E8EB6 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E530B1AC283B54DA004E8EB6 /* ActionViewController.swift */; };
E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; }; E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; };
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54B4752282B3B8900579261 /* HackiCore.swift */; };
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; }; E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */; }; FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -56,7 +55,7 @@
}; };
E51D52B8283B464E00FC8DD8 /* Embed App Extensions */ = { E51D52B8283B464E00FC8DD8 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 8;
dstPath = ""; dstPath = "";
dstSubfolderSpec = 13; dstSubfolderSpec = 13;
files = ( files = (
@ -64,7 +63,7 @@
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */, E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */,
); );
name = "Embed App Extensions"; name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 1;
}; };
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
@ -97,7 +96,6 @@
E530B1AF283B54DA004E8EB6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; }; E530B1AF283B54DA004E8EB6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
E530B1B1283B54DA004E8EB6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; E530B1B1283B54DA004E8EB6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E530B1B9283B54E4004E8EB6 /* Action Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Action Extension.entitlements"; sourceTree = "<group>"; }; E530B1B9283B54E4004E8EB6 /* Action Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Action Extension.entitlements"; sourceTree = "<group>"; };
E54B4752282B3B8900579261 /* HackiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackiCore.swift; sourceTree = "<group>"; };
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; }; E575B6EF27EBC6C6002B1508 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E575B6F027EBC6DA002B1508 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; E575B6F027EBC6DA002B1508 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
E59F28EE283B477D00512089 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; }; E59F28EE283B477D00512089 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
@ -177,7 +175,6 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
E54B4752282B3B8900579261 /* HackiCore.swift */,
); );
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
@ -437,7 +434,6 @@
files = ( files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -569,17 +565,19 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.33; MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -709,17 +707,19 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.33; MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -743,17 +743,19 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.2.33; MARKETING_VERSION = 0.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki; PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -778,7 +780,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -821,7 +823,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -861,7 +863,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -903,7 +905,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -948,7 +950,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -990,7 +992,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;

View File

@ -17,8 +17,6 @@ import flutter_local_notifications
let center = UNUserNotificationCenter.current() let center = UNUserNotificationCenter.current()
center.delegate = self center.delegate = self
HackiCore.start()
WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!) WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!)
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {

View File

@ -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)
}
}

View File

@ -23,7 +23,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -38,7 +38,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>https</string> <string>https</string>
@ -72,5 +72,7 @@
<array> <array>
<string>applinks:example.com</string> <string>applinks:example.com</string>
</array> </array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict> </dict>
</plist> </plist>

View File

@ -31,10 +31,6 @@ platform :ios do
is_example_repo = ENV['CI'] && ENV['GITHUB_REPOSITORY'] == 'jorgenpt/flutter_github_example' is_example_repo = ENV['CI'] && ENV['GITHUB_REPOSITORY'] == 'jorgenpt/flutter_github_example'
if !is_example_repo && APP_IDENTIFIER == 'no.tjer.HelloWorld' then
UI.user_error! "You need to update your Fastfile to use your own `APP_IDENTIFIER`"
end
# Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret) # Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret)
sync_code_signing( sync_code_signing(
type: "appstore", type: "appstore",
@ -42,15 +38,6 @@ platform :ios do
readonly: true readonly: true
) )
if !is_example_repo then
if APPSTORECONNECT_ISSUER_ID == '69a6de83-feb7-47e3-e053-5b8c7c11a4d1' then
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_ISSUER_ID`"
end
if APPSTORECONNECT_KEY_ID == 'YRQDJRKMR9' then
UI.user_error! "You need to update your Fastfile to use your own `APPSTORECONNECT_KEY_ID`"
end
end
# We expose the key data using `APP_STORE_CONNECT_API_KEY_KEY` secret on GH # We expose the key data using `APP_STORE_CONNECT_API_KEY_KEY` secret on GH
app_store_connect_api_key( app_store_connect_api_key(
key_id: APPSTORECONNECT_KEY_ID, key_id: APPSTORECONNECT_KEY_ID,
@ -59,21 +46,18 @@ platform :ios do
latest_testflight_build_number latest_testflight_build_number
# Figure out the build number (and optionally build name) # Figure out the build number (and optionally build name)
new_build_number = ( + 1) new_build_number = ( + 1)
extra_config_args = []
if options.key?(:build_name) then
extra_config_args = ["--build-name", options[:build_name].delete_prefix('v')]
end
# Prep the xcodeproject from Flutter without building (`--config-only`) # Prep the xcodeproject from Flutter without building (`--config-only`)
sh( sh(
"flutter", "build", "ios", "--config-only", "flutter", "build", "ios", "--config-only",
"--release", "--no-pub", "--no-codesign", "--release", "--no-pub", "--no-codesign",
"--build-number", new_build_number.to_s, "--build-number", new_build_number.to_s
*extra_config_args
) )
version = get_version_number(xcodeproj: "Runner.xcodeproj", target: 'Runner')
increment_version_number( increment_version_number(
version_number: options[:build_name].delete_prefix('v').delete_suffix('-rc') version_number: version
) )
increment_build_number({ increment_build_number({

View File

@ -20,7 +20,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
_sembastRepository = _sembastRepository =
sembastRepository ?? locator.get<SembastRepository>(), sembastRepository ?? locator.get<SembastRepository>(),
super(AuthState.init()) { super(const AuthState.init()) {
on<AuthInitialize>(onInitialize); on<AuthInitialize>(onInitialize);
on<AuthLogin>(onLogin); on<AuthLogin>(onLogin);
on<AuthLogout>(onLogout); on<AuthLogout>(onLogout);
@ -101,7 +101,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
Future<void> onLogout(AuthLogout event, Emitter<AuthState> emit) async { Future<void> onLogout(AuthLogout event, Emitter<AuthState> emit) async {
emit( emit(
state.copyWith( state.copyWith(
user: User.empty(), user: const User.empty(),
isLoggedIn: false, isLoggedIn: false,
agreedToEULA: false, agreedToEULA: false,
), ),

View File

@ -14,8 +14,8 @@ class AuthState extends Equatable {
required this.agreedToEULA, required this.agreedToEULA,
}); });
AuthState.init() const AuthState.init()
: user = User.empty(), : user = const User.empty(),
isLoggedIn = false, isLoggedIn = false,
status = AuthStatus.loaded, status = AuthStatus.loaded,
agreedToEULA = false; agreedToEULA = false;

View File

@ -10,6 +10,8 @@ abstract class Constants {
static const String googlePlayLink = static const String googlePlayLink =
'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US'; 'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US';
static const String sponsorLink = 'https://github.com/sponsors/Livinglist'; static const String sponsorLink = 'https://github.com/sponsors/Livinglist';
static const String guidelineLink =
'https://news.ycombinator.com/newsguidelines.html';
static const String _imagePath = 'assets/images'; static const String _imagePath = 'assets/images';
static const String hackerNewsLogoPath = '$_imagePath/hacker_news_logo.png'; static const String hackerNewsLogoPath = '$_imagePath/hacker_news_logo.png';
@ -48,3 +50,8 @@ abstract class Constants {
'(ㆆ_ㆆ)', '(ㆆ_ㆆ)',
]; ];
} }
abstract class RegExpConstants {
static const String linkSuffix = r'(\)|])(.)*$';
static const String number = '[0-9]+';
}

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
part 'collapse_state.dart'; part 'collapse_state.dart';
@ -10,13 +11,16 @@ part 'collapse_state.dart';
class CollapseCubit extends Cubit<CollapseState> { class CollapseCubit extends Cubit<CollapseState> {
CollapseCubit({ CollapseCubit({
required int commentId, required int commentId,
required CommentsCubit? commentsCubit,
CollapseCache? collapseCache, CollapseCache? collapseCache,
}) : _commentId = commentId, }) : _commentId = commentId,
_collapseCache = collapseCache ?? locator.get<CollapseCache>(), _collapseCache = collapseCache ?? locator.get<CollapseCache>(),
_commentsCubit = commentsCubit,
super(const CollapseState.init()); super(const CollapseState.init());
final int _commentId; final int _commentId;
final CollapseCache _collapseCache; final CollapseCache _collapseCache;
final CommentsCubit? _commentsCubit;
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription; late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
void init() { void init() {
@ -43,12 +47,21 @@ class CollapseCubit extends Cubit<CollapseState> {
), ),
); );
} else { } else {
final int count = _collapseCache.collapse(_commentId); if (_commentsCubit == null) return;
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
final int lastCommentId = _commentsCubit!.state.comments.last.id;
final bool shouldLoadMore = _commentId == lastCommentId ||
collapsedCommentIds.contains(lastCommentId);
if (shouldLoadMore) {
_commentsCubit!.loadMore();
}
emit( emit(
state.copyWith( state.copyWith(
collapsed: true, collapsed: true,
collapsedCount: state.collapsed ? 0 : count, collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
), ),
); );
} }

View File

@ -80,7 +80,7 @@ class CommentsCubit extends Cubit<CommentsState> {
state.copyWith( state.copyWith(
comments: targetParents, comments: targetParents,
onlyShowTargetComment: true, onlyShowTargetComment: true,
status: CommentsStatus.loaded, status: CommentsStatus.allLoaded,
), ),
); );
@ -141,21 +141,21 @@ class CommentsCubit extends Cubit<CommentsState> {
} }
Future<void> refresh() async { Future<void> refresh() async {
if (state.offlineReading) {
emit(
state.copyWith(
status: CommentsStatus.loaded,
),
);
return;
}
emit( emit(
state.copyWith( state.copyWith(
status: CommentsStatus.loading, status: CommentsStatus.loading,
), ),
); );
if (state.offlineReading) {
emit(
state.copyWith(
status: CommentsStatus.allLoaded,
),
);
return;
}
_collapseCache.resetCollapsedComments(); _collapseCache.resetCollapsedComments();
await _streamSubscription?.cancel(); await _streamSubscription?.cancel();
@ -195,7 +195,6 @@ class CommentsCubit extends Cubit<CommentsState> {
emit( emit(
state.copyWith( state.copyWith(
item: updatedItem, item: updatedItem,
status: CommentsStatus.loaded,
), ),
); );
} }
@ -213,6 +212,8 @@ class CommentsCubit extends Cubit<CommentsState> {
/// [comment] is only used for lazy fetching. /// [comment] is only used for lazy fetching.
void loadMore({Comment? comment}) { void loadMore({Comment? comment}) {
if (comment == null && state.status == CommentsStatus.loading) return;
switch (state.fetchMode) { switch (state.fetchMode) {
case FetchMode.lazy: case FetchMode.lazy:
if (comment == null) return; if (comment == null) return;
@ -368,12 +369,17 @@ class CommentsCubit extends Cubit<CommentsState> {
if (!isHidden) { if (!isHidden) {
_streamSubscription?.pause(); _streamSubscription?.pause();
emit(
state.copyWith(
status: CommentsStatus.loaded,
),
);
} }
emit( emit(
state.copyWith( state.copyWith(
currentPage: state.currentPage + 1, currentPage: state.currentPage + 1,
status: CommentsStatus.loaded,
), ),
); );
} }

View File

@ -64,6 +64,8 @@ class PreferenceState extends Equatable {
bool get showMetadata => _isOn<MetadataModePreference>(); bool get showMetadata => _isOn<MetadataModePreference>();
bool get showUrl => _isOn<StoryUrlModePreference>();
bool get tapAnywhereToCollapse => _isOn<CollapseModePreference>(); bool get tapAnywhereToCollapse => _isOn<CollapseModePreference>();
FetchMode get fetchMode => FetchMode.values FetchMode get fetchMode => FetchMode.values

View File

@ -10,7 +10,7 @@ class UserCubit extends Cubit<UserState> {
UserCubit({StoriesRepository? storiesRepository}) UserCubit({StoriesRepository? storiesRepository})
: _storiesRepository = : _storiesRepository =
storiesRepository ?? locator.get<StoriesRepository>(), storiesRepository ?? locator.get<StoriesRepository>(),
super(UserState.init()); super(const UserState.init());
final StoriesRepository _storiesRepository; final StoriesRepository _storiesRepository;

View File

@ -13,8 +13,8 @@ class UserState extends Equatable {
required this.status, required this.status,
}); });
UserState.init() const UserState.init()
: user = User.empty(), : user = const User.empty(),
status = UserStatus.initial; status = UserStatus.initial;
final User user; final User user;

View File

@ -1,7 +1,9 @@
import 'package:hacki/config/constants.dart';
extension StringExtension on String { extension StringExtension on String {
int? get itemId { int? get itemId {
final RegExp regex = RegExp(r'\d+$'); final RegExp regex = RegExp(RegExpConstants.number);
final RegExp exception = RegExp(r'\)|].*$'); final RegExp exception = RegExp(RegExpConstants.linkSuffix);
final String match = regex.stringMatch(replaceAll(exception, '')) ?? ''; final String match = regex.stringMatch(replaceAll(exception, '')) ?? '';
return int.tryParse(match); return int.tryParse(match);
} }

View File

@ -90,9 +90,9 @@ Future<void> main({bool testing = false}) async {
} else if (Platform.isAndroid) { } else if (Platform.isAndroid) {
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle( const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Palette.transparent,
systemNavigationBarColor: Colors.transparent, systemNavigationBarColor: Palette.transparent,
systemNavigationBarDividerColor: Colors.transparent, systemNavigationBarDividerColor: Palette.transparent,
), ),
); );

View File

@ -1,8 +1,9 @@
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
enum FontSize { enum FontSize {
regular('Regular', TextDimens.pt15), small('Small', TextDimens.pt15),
large('Large', TextDimens.pt16), regular('Regular', TextDimens.pt16),
large('Large', TextDimens.pt17),
xlarge('XLarge', TextDimens.pt18); xlarge('XLarge', TextDimens.pt18);
const FontSize(this.description, this.fontSize); const FontSize(this.description, this.fontSize);

View File

@ -18,10 +18,11 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
CommentsOrderPreference(), CommentsOrderPreference(),
FontSizePreference(), FontSizePreference(),
// order here reflects the order on settings screen. // order here reflects the order on settings screen.
const NotificationModePreference(),
const CollapseModePreference(),
const DisplayModePreference(), const DisplayModePreference(),
const MetadataModePreference(), const MetadataModePreference(),
const StoryUrlModePreference(),
const NotificationModePreference(),
const CollapseModePreference(),
NavigationModePreference(), NavigationModePreference(),
const ReaderModePreference(), const ReaderModePreference(),
const MarkReadStoriesModePreference(), const MarkReadStoriesModePreference(),
@ -50,6 +51,7 @@ const bool _trueDarkModeDefaultValue = false;
const bool _readerModeDefaultValue = true; const bool _readerModeDefaultValue = true;
const bool _markReadStoriesModeDefaultValue = true; const bool _markReadStoriesModeDefaultValue = true;
const bool _metadataModeDefaultValue = true; const bool _metadataModeDefaultValue = true;
const bool _storyUrlModeDefaultValue = true;
const bool _collapseModeDefaultValue = false; const bool _collapseModeDefaultValue = false;
final int _fetchModeDefaultValue = FetchMode.eager.index; final int _fetchModeDefaultValue = FetchMode.eager.index;
final int _commentsOrderDefaultValue = CommentsOrder.natural.index; final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
@ -132,6 +134,25 @@ class MetadataModePreference extends BooleanPreference {
'''show number of comments and post date in story tile.'''; '''show number of comments and post date in story tile.''';
} }
class StoryUrlModePreference extends BooleanPreference {
const StoryUrlModePreference({bool? val})
: super(val: val ?? _storyUrlModeDefaultValue);
@override
StoryUrlModePreference copyWith({required bool? val}) {
return StoryUrlModePreference(val: val);
}
@override
String get key => 'storyUrlMode';
@override
String get title => 'Show Url';
@override
String get subtitle => '''show url in story tile.''';
}
/// The value deciding whether or not user should be /// The value deciding whether or not user should be
/// navigated to web view first. Defaults to false. /// navigated to web view first. Defaults to false.
class NavigationModePreference extends BooleanPreference { class NavigationModePreference extends BooleanPreference {
@ -155,7 +176,7 @@ class NavigationModePreference extends BooleanPreference {
String get title => 'Show Web Page First'; String get title => 'Show Web Page First';
@override @override
String get subtitle => ''''show web page first after tapping on story.'''; String get subtitle => '''show web page first after tapping on story.''';
} }
class ReaderModePreference extends BooleanPreference { class ReaderModePreference extends BooleanPreference {

View File

@ -1,3 +1,4 @@
import 'package:hacki/config/constants.dart';
import 'package:hacki/models/item.dart'; import 'package:hacki/models/item.dart';
enum StoryType { enum StoryType {
@ -67,6 +68,24 @@ class Story extends Item {
type: '', type: '',
); );
Story.placeholder()
: super(
id: 0,
score: 0,
descendants: 0,
time: 1171872000,
by: 'Y Combinator',
title: 'Hacker News Guidelines',
url: Constants.guidelineLink,
kids: <int>[],
dead: false,
parts: <int>[],
deleted: false,
parent: 0,
text: '',
type: '',
);
Story.fromJson(Map<String, dynamic> json) Story.fromJson(Map<String, dynamic> json)
: super( : super(
descendants: json['descendants'] as int? ?? 0, descendants: json['descendants'] as int? ?? 0,
@ -91,6 +110,12 @@ class Story extends Item {
String get simpleMetadata => String get simpleMetadata =>
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate'''; '''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate''';
String get readableUrl {
final Uri url = Uri.parse(this.url);
final String authority = url.authority.replaceFirst('www.', '');
return authority;
}
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{

View File

@ -1,7 +1,8 @@
import 'package:equatable/equatable.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class User { class User extends Equatable {
User({ const User({
required this.about, required this.about,
required this.created, required this.created,
required this.delay, required this.delay,
@ -9,7 +10,7 @@ class User {
required this.karma, required this.karma,
}); });
User.empty() const User.empty()
: about = '', : about = '',
created = 0, created = 0,
delay = 0, delay = 0,
@ -39,4 +40,13 @@ class User {
String toString() { String toString() {
return 'User $about, $created, $delay, $id, $karma'; return 'User $about, $created, $delay, $id, $karma';
} }
@override
List<Object?> get props => <Object?>[
about,
created,
delay,
id,
karma,
];
} }

View File

@ -190,6 +190,7 @@ class _HomeScreenState extends State<HomeScreen>
onTap: () => onStoryTapped(story, isPin: true), onTap: () => onStoryTapped(story, isPin: true),
showWebPreview: preferenceState.showComplexStoryTile, showWebPreview: preferenceState.showComplexStoryTile,
showMetadata: preferenceState.showMetadata, showMetadata: preferenceState.showMetadata,
showUrl: preferenceState.showUrl,
), ),
), ),
), ),

View File

@ -255,33 +255,17 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
}, },
), ),
], ],
child: BlocConsumer<CommentsCubit, CommentsState>( child: BlocListener<CommentsCubit, CommentsState>(
listenWhen: (CommentsState previous, CommentsState current) => listenWhen: (CommentsState previous, CommentsState current) =>
previous.status != current.status, previous.status != current.status,
listener: (BuildContext context, CommentsState state) { listener: (BuildContext context, CommentsState state) {
if (state.status == CommentsStatus.loaded) { if (state.status != CommentsStatus.loading) {
refreshController refreshController
..refreshCompleted() ..refreshCompleted()
..loadComplete(); ..loadComplete();
} }
}, },
builder: (BuildContext context, CommentsState state) { child: BlocListener<EditCubit, EditState>(
final Widget mainView = MainView(
scrollController: scrollController,
refreshController: refreshController,
commentEditingController: commentEditingController,
authState: authState,
state: state,
focusNode: focusNode,
topPadding: topPadding,
splitViewEnabled: widget.splitViewEnabled,
onMoreTapped: onMoreTapped,
onStoryLinkTapped: onStoryLinkTapped,
onLoginTapped: onLoginTapped,
onRightMoreTapped: onRightMoreTapped,
);
return BlocListener<EditCubit, EditState>(
listenWhen: (EditState previous, EditState current) { listenWhen: (EditState previous, EditState current) {
return previous.replyingTo != current.replyingTo || return previous.replyingTo != current.replyingTo ||
previous.itemBeingEdited != current.itemBeingEdited || previous.itemBeingEdited != current.itemBeingEdited ||
@ -309,7 +293,20 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
Positioned.fill( Positioned.fill(
child: mainView, child: MainView(
scrollController: scrollController,
refreshController: refreshController,
commentEditingController:
commentEditingController,
authState: authState,
focusNode: focusNode,
topPadding: topPadding,
splitViewEnabled: widget.splitViewEnabled,
onMoreTapped: onMoreTapped,
onStoryLinkTapped: onStoryLinkTapped,
onLoginTapped: onLoginTapped,
onRightMoreTapped: onRightMoreTapped,
),
), ),
BlocBuilder<SplitViewCubit, SplitViewState>( BlocBuilder<SplitViewCubit, SplitViewState>(
buildWhen: ( buildWhen: (
@ -331,16 +328,14 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
.withOpacity(0.6), .withOpacity(0.6),
item: widget.item, item: widget.item,
scrollController: scrollController, scrollController: scrollController,
onBackgroundTap: onBackgroundTap: onFeatureDiscoveryDismissed,
onFeatureDiscoveryDismissed,
onDismiss: onFeatureDiscoveryDismissed, onDismiss: onFeatureDiscoveryDismissed,
splitViewEnabled: state.enabled, splitViewEnabled: state.enabled,
expanded: state.expanded, expanded: state.expanded,
onZoomTap: onZoomTap:
context.read<SplitViewCubit>().zoom, context.read<SplitViewCubit>().zoom,
onFontSizeTap: onFontSizeTapped, onFontSizeTap: onFontSizeTapped,
fontSizeIconButtonKey: fontSizeIconButtonKey: fontSizeIconButtonKey,
fontSizeIconButtonKey,
), ),
); );
}, },
@ -379,7 +374,19 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
onFontSizeTap: onFontSizeTapped, onFontSizeTap: onFontSizeTapped,
fontSizeIconButtonKey: fontSizeIconButtonKey, fontSizeIconButtonKey: fontSizeIconButtonKey,
), ),
body: mainView, body: MainView(
scrollController: scrollController,
refreshController: refreshController,
commentEditingController: commentEditingController,
authState: authState,
focusNode: focusNode,
topPadding: topPadding,
splitViewEnabled: widget.splitViewEnabled,
onMoreTapped: onMoreTapped,
onStoryLinkTapped: onStoryLinkTapped,
onLoginTapped: onLoginTapped,
onRightMoreTapped: onRightMoreTapped,
),
bottomSheet: ReplyBox( bottomSheet: ReplyBox(
focusNode: focusNode, focusNode: focusNode,
textEditingController: commentEditingController, textEditingController: commentEditingController,
@ -392,8 +399,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
onChanged: context.read<EditCubit>().onTextChanged, onChanged: context.read<EditCubit>().onTextChanged,
), ),
), ),
); ),
},
), ),
); );
}, },
@ -442,7 +448,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
fontSize: fontSize.fontSize, fontSize: fontSize.fontSize,
color: color:
context.read<PreferenceCubit>().state.fontSize == fontSize context.read<PreferenceCubit>().state.fontSize == fontSize
? Colors.deepOrange ? Palette.deepOrange
: null, : null,
), ),
), ),

View File

@ -22,7 +22,6 @@ class MainView extends StatelessWidget {
required this.refreshController, required this.refreshController,
required this.commentEditingController, required this.commentEditingController,
required this.authState, required this.authState,
required this.state,
required this.focusNode, required this.focusNode,
required this.topPadding, required this.topPadding,
required this.splitViewEnabled, required this.splitViewEnabled,
@ -36,7 +35,6 @@ class MainView extends StatelessWidget {
final RefreshController refreshController; final RefreshController refreshController;
final TextEditingController commentEditingController; final TextEditingController commentEditingController;
final AuthState authState; final AuthState authState;
final CommentsState state;
final FocusNode focusNode; final FocusNode focusNode;
final double topPadding; final double topPadding;
final bool splitViewEnabled; final bool splitViewEnabled;
@ -47,6 +45,11 @@ class MainView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned.fill(
child: BlocBuilder<CommentsCubit, CommentsState>(
builder: (BuildContext context, CommentsState state) {
return SmartRefresher( return SmartRefresher(
scrollController: scrollController, scrollController: scrollController,
enablePullUp: !state.onlyShowTargetComment, enablePullUp: !state.onlyShowTargetComment,
@ -108,6 +111,133 @@ class MainView extends StatelessWidget {
itemCount: state.comments.length + 2, itemCount: state.comments.length + 2,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
if (index == 0) { if (index == 0) {
return _ParentItemSection(
scrollController: scrollController,
refreshController: refreshController,
commentEditingController: commentEditingController,
state: state,
authState: authState,
focusNode: focusNode,
topPadding: topPadding,
splitViewEnabled: splitViewEnabled,
onMoreTapped: onMoreTapped,
onStoryLinkTapped: onStoryLinkTapped,
onLoginTapped: onLoginTapped,
onRightMoreTapped: onRightMoreTapped,
);
} else if (index == state.comments.length + 1) {
if ((state.status == CommentsStatus.allLoaded &&
state.comments.isNotEmpty) ||
state.onlyShowTargetComment) {
return SizedBox(
height: 240,
child: Center(
child: Text(Constants.happyFaces.pickRandomly()!),
),
);
} else {
return const SizedBox.shrink();
}
}
index = index - 1;
final Comment comment = state.comments.elementAt(index);
return FadeIn(
key: ValueKey<String>('${comment.id}-FadeIn'),
child: CommentTile(
comment: comment,
level: comment.level,
myUsername:
authState.isLoggedIn ? authState.username : null,
opUsername: state.item.by,
fetchMode: state.fetchMode,
onReplyTapped: (Comment cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) {
return;
}
if (cmt.id !=
context.read<EditCubit>().state.replyingTo?.id) {
commentEditingController.clear();
}
context.read<EditCubit>().onReplyTapped(cmt);
focusNode.requestFocus();
},
onEditTapped: (Comment cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) {
return;
}
commentEditingController.clear();
context.read<EditCubit>().onEditTapped(cmt);
focusNode.requestFocus();
},
onMoreTapped: onMoreTapped,
onStoryLinkTapped: onStoryLinkTapped,
onRightMoreTapped: onRightMoreTapped,
),
);
},
),
);
},
),
),
Positioned(
height: Dimens.pt4,
bottom: Dimens.zero,
left: Dimens.zero,
right: Dimens.zero,
child: BlocBuilder<CommentsCubit, CommentsState>(
buildWhen: (CommentsState prev, CommentsState current) =>
prev.status != current.status,
builder: (BuildContext context, CommentsState state) {
return Visibility(
visible: state.status == CommentsStatus.loading,
child: const LinearProgressIndicator(),
);
},
),
)
],
);
}
}
class _ParentItemSection extends StatelessWidget {
const _ParentItemSection({
Key? key,
required this.scrollController,
required this.refreshController,
required this.commentEditingController,
required this.state,
required this.authState,
required this.focusNode,
required this.topPadding,
required this.splitViewEnabled,
required this.onMoreTapped,
required this.onStoryLinkTapped,
required this.onLoginTapped,
required this.onRightMoreTapped,
}) : super(key: key);
final ScrollController scrollController;
final RefreshController refreshController;
final TextEditingController commentEditingController;
final CommentsState state;
final AuthState authState;
final FocusNode focusNode;
final double topPadding;
final bool splitViewEnabled;
final Function(Item item, Rect? rect) onMoreTapped;
final ValueChanged<String> onStoryLinkTapped;
final VoidCallback onLoginTapped;
final ValueChanged<Comment> onRightMoreTapped;
@override
Widget build(BuildContext context) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
@ -171,6 +301,18 @@ class MainView extends StatelessWidget {
], ],
), ),
), ),
BlocBuilder<PreferenceCubit, PreferenceState>(
buildWhen: (
PreferenceState previous,
PreferenceState current,
) =>
previous.fontSize != current.fontSize,
builder: (
BuildContext context,
PreferenceState prefState,
) {
return Column(
children: <Widget>[
if (state.item is Story) if (state.item is Story)
InkWell( InkWell(
onTap: () => LinkUtil.launch( onTap: () => LinkUtil.launch(
@ -189,16 +331,50 @@ class MainView extends StatelessWidget {
bottom: Dimens.pt12, bottom: Dimens.pt12,
top: Dimens.pt12, top: Dimens.pt12,
), ),
child: Text( child: RichText(
state.item.title,
textAlign: TextAlign.center, textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
color: Theme.of(context)
.textTheme
.bodyText1
?.color,
),
children: <TextSpan>[
TextSpan(
text: state.item.title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: MediaQuery.of(
context,
).textScaleFactor *
prefState.fontSize.fontSize,
color: state.item.url.isNotEmpty color: state.item.url.isNotEmpty
? Palette.orange ? Palette.orange
: null, : null,
), ),
), ),
if (state.item.url.isNotEmpty)
TextSpan(
text:
''' (${(state.item as Story).readableUrl})''',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: MediaQuery.of(
context,
).textScaleFactor *
(prefState.fontSize.fontSize - 4),
color: Palette.orange,
),
),
],
),
),
), ),
) )
else else
@ -206,25 +382,14 @@ class MainView extends StatelessWidget {
height: Dimens.pt6, height: Dimens.pt6,
), ),
if (state.item.text.isNotEmpty) if (state.item.text.isNotEmpty)
BlocBuilder<PreferenceCubit, PreferenceState>( Padding(
buildWhen: (
PreferenceState previous,
PreferenceState current,
) =>
previous.fontSize != current.fontSize,
builder: (
BuildContext context,
PreferenceState prefState,
) {
return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt10, horizontal: Dimens.pt10,
), ),
child: SelectableLinkify( child: SelectableLinkify(
text: state.item.text, text: state.item.text,
style: TextStyle( style: TextStyle(
fontSize: fontSize: MediaQuery.of(context).textScaleFactor *
MediaQuery.of(context).textScaleFactor *
context context
.read<PreferenceCubit>() .read<PreferenceCubit>()
.state .state
@ -232,8 +397,7 @@ class MainView extends StatelessWidget {
.fontSize, .fontSize,
), ),
linkStyle: TextStyle( linkStyle: TextStyle(
fontSize: fontSize: MediaQuery.of(context).textScaleFactor *
MediaQuery.of(context).textScaleFactor *
context context
.read<PreferenceCubit>() .read<PreferenceCubit>()
.state .state
@ -249,6 +413,8 @@ class MainView extends StatelessWidget {
} }
}, },
), ),
),
],
); );
}, },
), ),
@ -273,9 +439,8 @@ class MainView extends StatelessWidget {
if (state.onlyShowTargetComment) ...<Widget>[ if (state.onlyShowTargetComment) ...<Widget>[
Center( Center(
child: TextButton( child: TextButton(
onPressed: () => context onPressed: () =>
.read<CommentsCubit>() context.read<CommentsCubit>().loadAll(state.item as Story),
.loadAll(state.item as Story),
child: const Text('View all comments'), child: const Text('View all comments'),
), ),
), ),
@ -300,10 +465,8 @@ class MainView extends StatelessWidget {
width: Dimens.pt4, width: Dimens.pt4,
), ),
TextButton( TextButton(
onPressed: onPressed: context.read<CommentsCubit>().loadParentThread,
context.read<CommentsCubit>().loadParentThread, child: state.fetchParentStatus == CommentsStatus.loading
child:
state.fetchParentStatus == CommentsStatus.loading
? const SizedBox( ? const SizedBox(
height: Dimens.pt12, height: Dimens.pt12,
width: Dimens.pt12, width: Dimens.pt12,
@ -337,8 +500,7 @@ class MainView extends StatelessWidget {
), ),
) )
.toList(), .toList(),
onChanged: onChanged: context.read<CommentsCubit>().onFetchModeChanged,
context.read<CommentsCubit>().onFetchModeChanged,
), ),
const SizedBox( const SizedBox(
width: Dimens.pt6, width: Dimens.pt6,
@ -348,8 +510,7 @@ class MainView extends StatelessWidget {
underline: const SizedBox.shrink(), underline: const SizedBox.shrink(),
items: CommentsOrder.values items: CommentsOrder.values
.map( .map(
(CommentsOrder val) => (CommentsOrder val) => DropdownMenuItem<CommentsOrder>(
DropdownMenuItem<CommentsOrder>(
value: val, value: val,
child: Text( child: Text(
val.description, val.description,
@ -385,60 +546,5 @@ class MainView extends StatelessWidget {
], ],
], ],
); );
} else if (index == state.comments.length + 1) {
if ((state.status == CommentsStatus.allLoaded &&
state.comments.isNotEmpty) ||
state.onlyShowTargetComment) {
return SizedBox(
height: 240,
child: Center(
child: Text(Constants.happyFaces.pickRandomly()!),
),
);
} else {
return const SizedBox.shrink();
}
}
index = index - 1;
final Comment comment = state.comments.elementAt(index);
return FadeIn(
key: ValueKey<String>('${comment.id}-FadeIn'),
child: CommentTile(
comment: comment,
level: comment.level,
myUsername: authState.isLoggedIn ? authState.username : null,
opUsername: state.item.by,
fetchMode: state.fetchMode,
onReplyTapped: (Comment cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) {
return;
}
if (cmt.id != context.read<EditCubit>().state.replyingTo?.id) {
commentEditingController.clear();
}
context.read<EditCubit>().onReplyTapped(cmt);
focusNode.requestFocus();
},
onEditTapped: (Comment cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) {
return;
}
commentEditingController.clear();
context.read<EditCubit>().onEditTapped(cmt);
focusNode.requestFocus();
},
onMoreTapped: onMoreTapped,
onStoryLinkTapped: onStoryLinkTapped,
onRightMoreTapped: onRightMoreTapped,
),
);
},
),
);
} }
} }

View File

@ -20,6 +20,7 @@ import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -122,6 +123,7 @@ class _ProfileScreenState extends State<ProfileScreen>
return ItemsListView<Item>( return ItemsListView<Item>(
showWebPreview: false, showWebPreview: false,
showMetadata: false, showMetadata: false,
showUrl: false,
useConsistentFontSize: true, useConsistentFontSize: true,
refreshController: refreshControllerHistory, refreshController: refreshControllerHistory,
items: historyState.submittedItems items: historyState.submittedItems
@ -174,6 +176,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showWebPreview: showWebPreview:
preferenceState.showComplexStoryTile, preferenceState.showComplexStoryTile,
showMetadata: preferenceState.showMetadata, showMetadata: preferenceState.showMetadata,
showUrl: preferenceState.showUrl,
useCommentTile: true, useCommentTile: true,
refreshController: refreshControllerFav, refreshController: refreshControllerFav,
items: favState.favItems, items: favState.favItems,
@ -374,6 +377,17 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
], ],
), ),
const Divider(),
StoryTile(
showWebPreview:
preferenceState.showComplexStoryTile,
showMetadata: preferenceState.showMetadata,
showUrl: preferenceState.showUrl,
story: Story.placeholder(),
onTap: () =>
LinkUtil.launch(Constants.guidelineLink),
),
const Divider(),
for (final Preference<dynamic> preference for (final Preference<dynamic> preference
in preferenceState.preferences in preferenceState.preferences
.whereType<BooleanPreference>() .whereType<BooleanPreference>()
@ -422,93 +436,7 @@ class _ProfileScreenState extends State<ProfileScreen>
title: const Text('About'), title: const Text('About'),
subtitle: subtitle:
const Text('nothing interesting here.'), const Text('nothing interesting here.'),
onTap: () { onTap: showAboutHackiDialog,
showAboutDialog(
context: context,
applicationName: 'Hacki',
applicationVersion: 'v0.2.33',
applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(
Dimens.pt12,
),
),
child: Image.asset(
Constants.hackiIconPath,
height: Dimens.pt50,
width: Dimens.pt50,
),
),
children: <Widget>[
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.portfolioLink,
),
child: Row(
children: const <Widget>[
Icon(
FontAwesomeIcons.addressCard,
),
SizedBox(
width: Dimens.pt12,
),
Text('Developer'),
],
),
),
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.githubLink,
),
child: Row(
children: const <Widget>[
Icon(
FontAwesomeIcons.github,
),
SizedBox(
width: Dimens.pt12,
),
Text('Source code'),
],
),
),
ElevatedButton(
onPressed: () => LinkUtil.launch(
Platform.isIOS
? Constants.appStoreLink
: Constants.googlePlayLink,
),
child: Row(
children: const <Widget>[
Icon(
Icons.thumb_up,
),
SizedBox(
width: Dimens.pt12,
),
Text('Like the app?'),
],
),
),
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.sponsorLink,
),
child: Row(
children: const <Widget>[
Icon(
FeatherIcons.coffee,
),
SizedBox(
width: Dimens.pt12,
),
Text('Buy me a coffee'),
],
),
),
],
);
},
), ),
const SizedBox( const SizedBox(
height: Dimens.pt48, height: Dimens.pt48,
@ -718,6 +646,95 @@ class _ProfileScreenState extends State<ProfileScreen>
); );
} }
Future<void> showAboutHackiDialog() async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
final String version = packageInfo.version;
showAboutDialog(
context: context,
applicationName: 'Hacki',
applicationVersion: 'v$version',
applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(
Dimens.pt12,
),
),
child: Image.asset(
Constants.hackiIconPath,
height: Dimens.pt50,
width: Dimens.pt50,
),
),
children: <Widget>[
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.portfolioLink,
),
child: Row(
children: const <Widget>[
Icon(
FontAwesomeIcons.addressCard,
),
SizedBox(
width: Dimens.pt12,
),
Text('Developer'),
],
),
),
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.githubLink,
),
child: Row(
children: const <Widget>[
Icon(
FontAwesomeIcons.github,
),
SizedBox(
width: Dimens.pt12,
),
Text('Source code'),
],
),
),
ElevatedButton(
onPressed: () => LinkUtil.launch(
Platform.isIOS ? Constants.appStoreLink : Constants.googlePlayLink,
),
child: Row(
children: const <Widget>[
Icon(
Icons.thumb_up,
),
SizedBox(
width: Dimens.pt12,
),
Text('Like the app?'),
],
),
),
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.sponsorLink,
),
child: Row(
children: const <Widget>[
Icon(
FeatherIcons.coffee,
),
SizedBox(
width: Dimens.pt12,
),
Text('Buy me a coffee'),
],
),
),
],
);
}
void onCommentTapped(Comment comment, {VoidCallback? then}) { void onCommentTapped(Comment comment, {VoidCallback? then}) {
throttle.run(() { throttle.run(() {
locator locator

View File

@ -166,6 +166,7 @@ class _SearchScreenState extends State<SearchScreen> {
showWebPreview: showWebPreview:
prefState.showComplexStoryTile, prefState.showComplexStoryTile,
showMetadata: prefState.showMetadata, showMetadata: prefState.showMetadata,
showUrl: prefState.showUrl,
story: e, story: e,
onTap: () => goToItemScreen( onTap: () => goToItemScreen(
args: ItemScreenArgs(item: e), args: ItemScreenArgs(item: e),

View 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);
},
);
},
);
},
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/bloc_builder_3.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:hacki/styles/styles.dart'; import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart'; import 'package:hacki/utils/utils.dart';
@ -46,16 +47,19 @@ class CommentTile extends StatelessWidget {
lazy: false, lazy: false,
create: (_) => CollapseCubit( create: (_) => CollapseCubit(
commentId: comment.id, commentId: comment.id,
commentsCubit: context.tryRead<CommentsCubit>(),
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(), collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
)..init(), )..init(),
child: BlocBuilder<CollapseCubit, CollapseState>( child: BlocBuilder3<CollapseCubit, CollapseState, PreferenceCubit,
builder: (BuildContext context, CollapseState state) { PreferenceState, BlocklistCubit, BlocklistState>(
builder: (
BuildContext context,
CollapseState state,
PreferenceState prefState,
BlocklistState blocklistState,
) {
if (actionable && state.hidden) return const SizedBox.shrink(); if (actionable && state.hidden) return const SizedBox.shrink();
return BlocBuilder<PreferenceCubit, PreferenceState>(
builder: (BuildContext context, PreferenceState prefState) {
return BlocBuilder<BlocklistCubit, BlocklistState>(
builder: (BuildContext context, BlocklistState blocklistState) {
const Color orange = Color.fromRGBO(255, 152, 0, 1); const Color orange = Color.fromRGBO(255, 152, 0, 1);
final Color color = _getColor(level); final Color color = _getColor(level);
@ -70,21 +74,15 @@ class CommentTile extends StatelessWidget {
motion: const StretchMotion(), motion: const StretchMotion(),
children: <Widget>[ children: <Widget>[
SlidableAction( SlidableAction(
onPressed: (_) => onPressed: (_) => onReplyTapped?.call(comment),
onReplyTapped?.call(comment),
backgroundColor: Palette.orange, backgroundColor: Palette.orange,
foregroundColor: Palette.white, foregroundColor: Palette.white,
icon: Icons.message, icon: Icons.message,
), ),
if (context if (context.read<AuthBloc>().state.user.id ==
.read<AuthBloc>()
.state
.user
.id ==
comment.by) comment.by)
SlidableAction( SlidableAction(
onPressed: (_) => onPressed: (_) => onEditTapped?.call(comment),
onEditTapped?.call(comment),
backgroundColor: Palette.orange, backgroundColor: Palette.orange,
foregroundColor: Palette.white, foregroundColor: Palette.white,
icon: Icons.edit, icon: Icons.edit,
@ -137,9 +135,8 @@ class CommentTile extends StatelessWidget {
Text( Text(
comment.by, comment.by,
style: TextStyle( style: TextStyle(
color: prefState.showEyeCandy color:
? orange prefState.showEyeCandy ? orange : color,
: color,
), ),
), ),
if (comment.by == opUsername) if (comment.by == opUsername)
@ -202,8 +199,7 @@ class CommentTile extends StatelessWidget {
), ),
), ),
) )
else if (blocklistState.blocklist else if (blocklistState.blocklist.contains(comment.by))
.contains(comment.by))
const Center( const Center(
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -229,8 +225,7 @@ class CommentTile extends StatelessWidget {
? SelectableText.rich( ? SelectableText.rich(
key: ValueKey<int>(comment.id), key: ValueKey<int>(comment.id),
buildTextSpan( buildTextSpan(
(comment as BuildableComment) (comment as BuildableComment).elements,
.elements,
style: TextStyle( style: TextStyle(
fontSize: MediaQuery.of( fontSize: MediaQuery.of(
context, context,
@ -242,14 +237,12 @@ class CommentTile extends StatelessWidget {
context, context,
).textScaleFactor * ).textScaleFactor *
prefState.fontSize.fontSize, prefState.fontSize.fontSize,
decoration: decoration: TextDecoration.underline,
TextDecoration.underline,
color: Palette.orange, color: Palette.orange,
), ),
onOpen: (LinkableElement link) { onOpen: (LinkableElement link) {
if (link.url.isStoryLink) { if (link.url.isStoryLink) {
onStoryLinkTapped onStoryLinkTapped.call(link.url);
.call(link.url);
} else { } else {
LinkUtil.launch(link.url); LinkUtil.launch(link.url);
} }
@ -273,8 +266,7 @@ class CommentTile extends StatelessWidget {
), ),
onOpen: (LinkableElement link) { onOpen: (LinkableElement link) {
if (link.url.isStoryLink) { if (link.url.isStoryLink) {
onStoryLinkTapped onStoryLinkTapped.call(link.url);
.call(link.url);
} else { } else {
LinkUtil.launch(link.url); LinkUtil.launch(link.url);
} }
@ -300,16 +292,13 @@ class CommentTile extends StatelessWidget {
horizontal: Dimens.pt12, horizontal: Dimens.pt12,
), ),
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.center,
MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
context context.read<CommentsCubit>().loadMore(
.read<CommentsCubit>()
.loadMore(
comment: comment, comment: comment,
); );
}, },
@ -337,9 +326,7 @@ class CommentTile extends StatelessWidget {
); );
final double commentBackgroundColorOpacity = final double commentBackgroundColorOpacity =
Theme.of(context).brightness == Brightness.dark Theme.of(context).brightness == Brightness.dark ? 0.03 : 0.15;
? 0.03
: 0.15;
final Color commentColor = prefState.showEyeCandy final Color commentColor = prefState.showEyeCandy
? color.withOpacity(commentBackgroundColorOpacity) ? color.withOpacity(commentBackgroundColorOpacity)
@ -350,7 +337,10 @@ class CommentTile extends StatelessWidget {
if (isMyComment && level == 0) { if (isMyComment && level == 0) {
return Container( return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Palette.orange.withOpacity(0.2), color: Palette.orange.withOpacity(0.2),
),
child: wrapper, child: wrapper,
); );
} }
@ -361,7 +351,7 @@ class CommentTile extends StatelessWidget {
wrapper = Container( wrapper = Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
left: Dimens.pt12, left: Dimens.pt8,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
border: i != 0 border: i != 0
@ -381,10 +371,6 @@ class CommentTile extends StatelessWidget {
return wrapper; return wrapper;
}, },
);
},
);
},
), ),
); );
} }

View File

@ -16,6 +16,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
super.key, super.key,
required this.showWebPreview, required this.showWebPreview,
required this.showMetadata, required this.showMetadata,
required this.showUrl,
required this.items, required this.items,
required this.onTap, required this.onTap,
required this.refreshController, required this.refreshController,
@ -39,6 +40,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
final bool showCommentBy; final bool showCommentBy;
final bool showWebPreview; final bool showWebPreview;
final bool showMetadata; final bool showMetadata;
final bool showUrl;
final bool enablePullDown; final bool enablePullDown;
final bool markReadStories; final bool markReadStories;
final bool showOfflineBanner; final bool showOfflineBanner;
@ -97,6 +99,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
onTap: () => onTap(e), onTap: () => onTap(e),
showWebPreview: showWebPreview, showWebPreview: showWebPreview,
showMetadata: showMetadata, showMetadata: showMetadata,
showUrl: showUrl,
hasRead: markReadStories && hasRead, hasRead: markReadStories && hasRead,
simpleTileFontSize: useConsistentFontSize simpleTileFontSize: useConsistentFontSize
? TextDimens.pt14 ? TextDimens.pt14

View File

@ -15,6 +15,7 @@ class LinkPreview extends StatefulWidget {
required this.link, required this.link,
required this.story, required this.story,
required this.showMetadata, required this.showMetadata,
required this.showUrl,
required this.offlineReading, required this.offlineReading,
this.cache = const Duration(days: 30), this.cache = const Duration(days: 30),
this.titleStyle, this.titleStyle,
@ -103,6 +104,7 @@ class LinkPreview extends StatefulWidget {
final List<BoxShadow>? boxShadow; final List<BoxShadow>? boxShadow;
final bool showMetadata; final bool showMetadata;
final bool showUrl;
final bool offlineReading; final bool offlineReading;
@override @override
@ -180,6 +182,7 @@ class _LinkPreviewState extends State<LinkPreview> {
key: widget.key ?? Key(widget.link), key: widget.key ?? Key(widget.link),
metadata: widget.story.simpleMetadata, metadata: widget.story.simpleMetadata,
url: widget.link, url: widget.link,
readableUrl: widget.story.readableUrl,
title: widget.story.title, title: widget.story.title,
description: desc ?? title ?? 'no comment yet.', description: desc ?? title ?? 'no comment yet.',
imageUri: imageUri, imageUri: imageUri,
@ -194,6 +197,7 @@ class _LinkPreviewState extends State<LinkPreview> {
bgColor: widget.backgroundColor, bgColor: widget.backgroundColor,
radius: widget.borderRadius ?? 12, radius: widget.borderRadius ?? 12,
showMetadata: widget.showMetadata, showMetadata: widget.showMetadata,
showUrl: widget.showUrl,
), ),
); );
} }

View File

@ -8,10 +8,12 @@ class LinkView extends StatelessWidget {
super.key, super.key,
required this.metadata, required this.metadata,
required this.url, required this.url,
required this.readableUrl,
required this.title, required this.title,
required this.description, required this.description,
required this.onTap, required this.onTap,
required this.showMetadata, required this.showMetadata,
required this.showUrl,
this.imageUri, this.imageUri,
this.imagePath, this.imagePath,
this.titleTextStyle, this.titleTextStyle,
@ -30,6 +32,7 @@ class LinkView extends StatelessWidget {
final String metadata; final String metadata;
final String url; final String url;
final String readableUrl;
final String title; final String title;
final String description; final String description;
final String? imageUri; final String? imageUri;
@ -44,6 +47,7 @@ class LinkView extends StatelessWidget {
final double radius; final double radius;
final Color? bgColor; final Color? bgColor;
final bool showMetadata; final bool showMetadata;
final bool showUrl;
double computeTitleFontSize(double width) { double computeTitleFontSize(double width) {
double size = width * 0.13; double size = width * 0.13;
@ -146,6 +150,7 @@ class LinkView extends StatelessWidget {
} }
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) { Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
final bool showUrl = this.showUrl && url.isNotEmpty;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(4, 2, 3, 0), padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
child: Column( child: Column(
@ -159,6 +164,22 @@ class LinkView extends StatelessWidget {
maxLines: _maxLines, maxLines: _maxLines,
), ),
), ),
if (showUrl)
Container(
alignment: Alignment.topLeft,
child: Text(
'($readableUrl)',
textAlign: TextAlign.left,
style: _titleTS.copyWith(
color: Palette.grey,
fontSize:
_titleTS.fontSize == null ? 12 : _titleTS.fontSize! - 4,
fontWeight: FontWeight.w400,
),
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
maxLines: 1,
),
),
], ],
), ),
); );
@ -193,8 +214,9 @@ class LinkView extends StatelessWidget {
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: _bodyTS, style: _bodyTS,
overflow: bodyTextOverflow ?? TextOverflow.ellipsis, overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
maxLines: maxLines: (bodyMaxLines ?? _maxLines) -
(bodyMaxLines ?? _maxLines) - (showMetadata ? 1 : 0), (showMetadata ? 1 : 0) -
(showUrl && url.isNotEmpty ? 1 : 0),
), ),
), ),
), ),

View File

@ -68,6 +68,7 @@ class _StoriesListViewState extends State<StoriesListView> {
context.read<PreferenceCubit>().state.markReadStories, context.read<PreferenceCubit>().state.markReadStories,
showWebPreview: preferenceState.showComplexStoryTile, showWebPreview: preferenceState.showComplexStoryTile,
showMetadata: preferenceState.showMetadata, showMetadata: preferenceState.showMetadata,
showUrl: preferenceState.showUrl,
refreshController: refreshController, refreshController: refreshController,
items: state.storiesByType[storyType]!, items: state.storiesByType[storyType]!,
onRefresh: () { onRefresh: () {

View File

@ -15,6 +15,7 @@ class StoryTile extends StatelessWidget {
this.hasRead = false, this.hasRead = false,
required this.showWebPreview, required this.showWebPreview,
required this.showMetadata, required this.showMetadata,
required this.showUrl,
required this.story, required this.story,
required this.onTap, required this.onTap,
this.simpleTileFontSize = 16, this.simpleTileFontSize = 16,
@ -22,6 +23,7 @@ class StoryTile extends StatelessWidget {
final bool showWebPreview; final bool showWebPreview;
final bool showMetadata; final bool showMetadata;
final bool showUrl;
final bool hasRead; final bool hasRead;
final Story story; final Story story;
final VoidCallback onTap; final VoidCallback onTap;
@ -42,7 +44,108 @@ class StoryTile extends StatelessWidget {
story: story, story: story,
link: story.url, link: story.url,
offlineReading: context.read<StoriesBloc>().state.offlineReading, offlineReading: context.read<StoriesBloc>().state.offlineReading,
placeholderWidget: FadeIn( placeholderWidget: _LinkPreviewPlaceholder(
height: height,
),
errorImage: Constants.hackerNewsLogoLink,
backgroundColor: Palette.transparent,
borderRadius: Dimens.zero,
removeElevation: true,
bodyMaxLines: context.storyTileMaxLines,
errorTitle: story.title,
titleStyle: TextStyle(
color: hasRead
? Palette.grey[500]
: Theme.of(context).textTheme.subtitle1?.color,
fontWeight: FontWeight.bold,
),
showMetadata: showMetadata,
showUrl: showUrl,
),
),
),
);
} else {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.only(left: Dimens.pt12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(
height: Dimens.pt8,
),
Row(
children: <Widget>[
Expanded(
child: RichText(
textScaleFactor: MediaQuery.of(context).textScaleFactor,
text: TextSpan(
children: <TextSpan>[
TextSpan(
text: story.title,
style: TextStyle(
color: hasRead
? Palette.grey[500]
: Theme.of(context)
.textTheme
.bodyText1
?.color,
fontSize: simpleTileFontSize,
),
),
if (showUrl && story.url.isNotEmpty)
TextSpan(
text: ' (${story.readableUrl})',
style: TextStyle(
color: Palette.grey[500],
fontSize: simpleTileFontSize - 4,
),
),
],
),
),
),
],
),
if (showMetadata)
Row(
children: <Widget>[
Expanded(
child: Text(
story.metadata,
style: TextStyle(
color: Palette.grey,
fontSize: simpleTileFontSize - 2,
),
maxLines: 1,
),
),
],
),
const SizedBox(
height: Dimens.pt8,
),
],
),
),
);
}
}
}
class _LinkPreviewPlaceholder extends StatelessWidget {
const _LinkPreviewPlaceholder({
Key? key,
required this.height,
}) : super(key: key);
final double height;
@override
Widget build(BuildContext context) {
return FadeIn(
child: SizedBox( child: SizedBox(
height: height, height: height,
child: Shimmer.fromColors( child: Shimmer.fromColors(
@ -126,70 +229,6 @@ class StoryTile extends StatelessWidget {
), ),
), ),
), ),
),
errorImage: Constants.hackerNewsLogoLink,
backgroundColor: Palette.transparent,
borderRadius: Dimens.zero,
removeElevation: true,
bodyMaxLines: context.storyTileMaxLines,
errorTitle: story.title,
titleStyle: TextStyle(
color: hasRead
? Palette.grey[500]
: Theme.of(context).textTheme.subtitle1!.color,
fontWeight: FontWeight.bold,
),
showMetadata: showMetadata,
),
),
),
);
} else {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.only(left: Dimens.pt12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(
height: Dimens.pt8,
),
Row(
children: <Widget>[
Expanded(
child: Text(
story.title,
style: TextStyle(
color: hasRead ? Palette.grey[500] : null,
fontSize: simpleTileFontSize,
),
),
),
],
),
if (showMetadata)
Row(
children: <Widget>[
Expanded(
child: Text(
story.metadata,
style: TextStyle(
color: Palette.grey,
fontSize: simpleTileFontSize - 2,
),
maxLines: 1,
),
),
],
),
const SizedBox(
height: Dimens.pt8,
),
],
),
),
); );
} }
} }
}

View File

@ -1,3 +1,4 @@
export 'bloc_builder_3.dart';
export 'circle_tab_indicator.dart'; export 'circle_tab_indicator.dart';
export 'comment_tile.dart'; export 'comment_tile.dart';
export 'countdown_reminder.dart'; export 'countdown_reminder.dart';

View File

@ -15,7 +15,7 @@ class CollapseCache {
addIfParentIsHiddenOrCollapsed(commentId, to); addIfParentIsHiddenOrCollapsed(commentId, to);
} }
int collapse(int commentId) { Set<int> collapse(int commentId) {
_collapsed.add(commentId); _collapsed.add(commentId);
Set<int> findHiddenComments(int commentId) { Set<int> findHiddenComments(int commentId) {
@ -35,7 +35,7 @@ class CollapseCache {
_hiddenCommentsSubject.add(_hidden); _hiddenCommentsSubject.add(_hidden);
return hiddenComments.length; return hiddenComments;
} }
void uncollapse(int commentId) { void uncollapse(int commentId) {

View File

@ -31,6 +31,7 @@ abstract class TextDimens {
static const double pt14 = 14; static const double pt14 = 14;
static const double pt15 = 15; static const double pt15 = 15;
static const double pt16 = 16; static const double pt16 = 16;
static const double pt17 = 17;
static const double pt18 = 18; static const double pt18 = 18;
static const double pt20 = 20; static const double pt20 = 20;
static const double pt24 = 24; static const double pt24 = 24;

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/main.dart'; import 'package:hacki/main.dart';
import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/repositories/repositories.dart';
@ -35,7 +36,7 @@ abstract class LinkUtil {
} }
Uri rinseLink(String link) { Uri rinseLink(String link) {
final RegExp regex = RegExp(r'\)|].*$'); final RegExp regex = RegExp(RegExpConstants.linkSuffix);
if (!link.contains('en.wikipedia.org') && link.contains(regex)) { if (!link.contains('en.wikipedia.org') && link.contains(regex)) {
final String match = regex.stringMatch(link) ?? ''; final String match = regex.stringMatch(link) ?? '';
return Uri.parse(link.replaceAll(match, '')); return Uri.parse(link.replaceAll(match, ''));

View File

@ -607,6 +607,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -1,6 +1,6 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 0.2.33+76 version: 1.0.2+80
publish_to: none publish_to: none
environment: environment:
@ -18,7 +18,6 @@ dependencies:
dio: ^4.0.4 dio: ^4.0.4
equatable: ^2.0.5 equatable: ^2.0.5
fast_gbk: ^1.0.0 fast_gbk: ^1.0.0
# feature_discovery: ^0.14.0
feature_discovery: feature_discovery:
git: git:
url: https://github.com/livinglist/feature_discovery url: https://github.com/livinglist/feature_discovery
@ -45,11 +44,11 @@ dependencies:
hydrated_bloc: ^9.0.0-dev.3 hydrated_bloc: ^9.0.0-dev.3
intl: ^0.17.0 intl: ^0.17.0
logger: ^1.1.0 logger: ^1.1.0
package_info_plus: ^3.0.2
path: ^1.8.0 path: ^1.8.0
path_provider: ^2.0.8 path_provider: ^2.0.8
path_provider_android: ^2.0.8 path_provider_android: ^2.0.8
path_provider_ios: ^2.0.8 path_provider_ios: ^2.0.8
# pull_to_refresh: ^2.0.0
pull_to_refresh: pull_to_refresh:
git: git:
url: https://github.com/livinglist/flutter_pulltorefresh url: https://github.com/livinglist/flutter_pulltorefresh

1
submodules/flutter Submodule

Submodule submodules/flutter added at 135454af32

View 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);
},
);
});
}

View File

@ -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);
});
}