mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
d3ede8546b | |||
53562ad260 | |||
6c8e7a7cb9 | |||
56c0245335 | |||
0cbd38a530 | |||
7c6da2c36a | |||
185140feb4 | |||
03c01a0b78 | |||
f823fdf241 | |||
fe87ddd8ff | |||
613ba12b05 | |||
8d7f66ecbc | |||
461aae253b |
1
.github/workflows/commit_check.yml
vendored
1
.github/workflows/commit_check.yml
vendored
@ -10,6 +10,7 @@ jobs:
|
||||
releases:
|
||||
name: Check commit
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
FLUTTER_VERSION: "3.3.10"
|
||||
steps:
|
||||
|
3
.github/workflows/publish_ios.yml
vendored
3
.github/workflows/publish_ios.yml
vendored
@ -7,12 +7,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build_and_publish:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
env:
|
||||
# Point the `ruby/setup-ruby` action at this Gemfile, so it
|
||||
|
@ -1,5 +1,82 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
typealias APNSHandler = ()->Void
|
||||
|
||||
let keyKey = "key"
|
||||
let valKey = "val"
|
||||
|
||||
final class SharedPrefsCore {
|
||||
fileprivate static let shared: SharedPrefsCore = SharedPrefsCore()
|
||||
|
||||
fileprivate func setBool(key: String?, val: Bool?) -> Bool {
|
||||
guard let key = key,
|
||||
let val = val else {
|
||||
return false
|
||||
}
|
||||
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
let allVals = keyStore.dictionaryRepresentation;
|
||||
let allKeys = allVals.keys
|
||||
|
||||
// Limit is 1024, reserve rest slots for fav and pins.
|
||||
if allKeys.count >= 1000 {
|
||||
for key in allKeys.filter({ $0.contains("hasRead") }) {
|
||||
keyStore.removeObject(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
keyStore.set(val, forKey: key)
|
||||
return true
|
||||
}
|
||||
|
||||
fileprivate func getBool(key: String?) -> Bool {
|
||||
guard let key = key else {
|
||||
return false
|
||||
}
|
||||
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
let val = keyStore.bool(forKey: key)
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
fileprivate func setStringList(key: String?, val: [String]?) -> Bool {
|
||||
guard let key = key,
|
||||
let val = val else {
|
||||
return false
|
||||
}
|
||||
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
keyStore.set(val, forKey: key)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fileprivate func getStringList(key: String?) -> [Any] {
|
||||
guard let key = key else {
|
||||
return [Any]()
|
||||
}
|
||||
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
let list = keyStore.array(forKey: key) as [Any]? ?? [Any]()
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
fileprivate func clearAll() -> Bool{
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
let allVals = keyStore.dictionaryRepresentation;
|
||||
let allKeys = allVals.keys
|
||||
|
||||
for key in allKeys.filter({ $0.contains("hasRead") }) {
|
||||
keyStore.removeObject(forKey: key)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
@ -7,46 +84,49 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin {
|
||||
let instance = SwiftSyncedSharedPreferencesPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "setBool":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("setBool"), object: nil, userInfo: info)
|
||||
let val = params[valKey] as? Bool
|
||||
let key = params[keyKey] as? String
|
||||
|
||||
let res = SharedPrefsCore.shared.setBool(key: key, val: val)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "getBool":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("getBool"), object: nil, userInfo: info)
|
||||
let key = params[keyKey] as? String
|
||||
let res = SharedPrefsCore.shared.getBool(key: key)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "setStringList":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("setStringList"), object: nil, userInfo: info)
|
||||
let val = params[valKey] as? [String]
|
||||
let key = params[keyKey] as? String
|
||||
|
||||
let res = SharedPrefsCore.shared.setStringList(key: key, val: val)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "getStringList":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("getStringList"), object: nil, userInfo: info)
|
||||
let key = params[keyKey] as? String
|
||||
let res = SharedPrefsCore.shared.getStringList(key: key)
|
||||
result(res)
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
case "clearAll":
|
||||
if let params = call.arguments as? [String: Any] {
|
||||
let info: [String: Any] = ["result": result,
|
||||
"params": params]
|
||||
NotificationCenter.default.post(name: Notification.Name("clearAll"), object: nil, userInfo: info)
|
||||
let res = SharedPrefsCore.shared.clearAll()
|
||||
result(res)
|
||||
}
|
||||
|
||||
return
|
||||
|
1
fastlane/metadata/android/en-US/changelogs/78.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/78.txt
Normal file
@ -0,0 +1 @@
|
||||
- Fixed time machine.
|
1
fastlane/metadata/android/en-US/changelogs/79.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/79.txt
Normal file
@ -0,0 +1 @@
|
||||
- Fixed time machine.
|
@ -1,6 +1,3 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '11.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
|
@ -22,6 +22,8 @@ PODS:
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (5.0.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
@ -53,6 +55,7 @@ DEPENDENCIES:
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
@ -85,6 +88,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_siri_suggestions/ios"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
receive_sharing_intent:
|
||||
@ -116,6 +121,7 @@ SPEC CHECKSUMS:
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||
@ -128,6 +134,6 @@ SPEC CHECKSUMS:
|
||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
@ -21,7 +21,6 @@
|
||||
E530B1AD283B54DA004E8EB6 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E530B1AC283B54DA004E8EB6 /* ActionViewController.swift */; };
|
||||
E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; };
|
||||
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54B4752282B3B8900579261 /* HackiCore.swift */; };
|
||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
|
||||
FC507E94AA7767C155787DB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB5AA41D6C22D228077D166 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
@ -97,7 +96,6 @@
|
||||
E530B1AF283B54DA004E8EB6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
E530B1B1283B54DA004E8EB6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
E530B1B9283B54E4004E8EB6 /* Action Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Action Extension.entitlements"; sourceTree = "<group>"; };
|
||||
E54B4752282B3B8900579261 /* HackiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackiCore.swift; sourceTree = "<group>"; };
|
||||
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
E575B6F027EBC6DA002B1508 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
|
||||
E59F28EE283B477D00512089 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
|
||||
@ -177,7 +175,6 @@
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
E54B4752282B3B8900579261 /* HackiCore.swift */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
@ -437,7 +434,6 @@
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
E54B4753282B3B8900579261 /* HackiCore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -16,8 +16,6 @@ import flutter_local_notifications
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.delegate = self
|
||||
|
||||
HackiCore.start()
|
||||
|
||||
WorkmanagerPlugin.register(with: self.registrar(forPlugin: "be.tramckrijte.workmanager.WorkmanagerPlugin")!)
|
||||
|
||||
|
@ -1,134 +0,0 @@
|
||||
//
|
||||
// HackiCore.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by Jiaqi Feng on 5/10/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Flutter
|
||||
|
||||
extension Notification.Name {
|
||||
static let setBool = Notification.Name("setBool")
|
||||
static let getBool = Notification.Name("getBool")
|
||||
static let setStringList = Notification.Name("setStringList")
|
||||
static let getStringList = Notification.Name("getStringList")
|
||||
static let clearAll = Notification.Name("clearAll")
|
||||
}
|
||||
|
||||
typealias APNSHandler = ()->Void
|
||||
|
||||
final class HackiCore: NSObject {
|
||||
private static let keyKey = "key"
|
||||
private static let valKey = "val"
|
||||
|
||||
private static let shared: HackiCore = HackiCore()
|
||||
private let notificationCenter = NotificationCenter.default
|
||||
|
||||
// Called at app launch
|
||||
class func start() {
|
||||
shared.registerNotifications()
|
||||
}
|
||||
|
||||
private class func setupFlutterEvent(channelName: String, handler: NSObjectProtocol & FlutterStreamHandler) {
|
||||
guard let rootVC = UIApplication.shared.delegate?.window.unsafelyUnwrapped?.rootViewController as? FlutterViewController else { return }
|
||||
let eventChannel = FlutterEventChannel(name: channelName, binaryMessenger: rootVC.binaryMessenger)
|
||||
eventChannel.setStreamHandler(handler)
|
||||
}
|
||||
|
||||
private func registerNotifications() {
|
||||
// SyncedSharedPreferences
|
||||
notificationCenter.addObserver(self, selector: #selector(setBool(_:)), name: .setBool, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(getBool(_:)), name: .getBool, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(setStringList(_:)), name: .setStringList, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(getStringList(_:)), name: .getStringList, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(clearAll(_:)), name: .clearAll, object: nil)
|
||||
}
|
||||
|
||||
@objc private func setBool(_ notification: Notification) {
|
||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
||||
guard
|
||||
let params = notification.userInfo?["params"] as? [String: Any],
|
||||
let key = params[HackiCore.keyKey] as? String,
|
||||
let val = params[HackiCore.valKey] as? Bool else {
|
||||
resultCompletionBlock(false)
|
||||
return
|
||||
}
|
||||
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
let allVals = keyStore.dictionaryRepresentation;
|
||||
let allKeys = allVals.keys
|
||||
|
||||
// Limit is 1024, reserve rest slots for fav and pins.
|
||||
if allKeys.count >= 1000 {
|
||||
for key in allKeys.filter({ $0.contains("hasRead") }) {
|
||||
keyStore.removeObject(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
keyStore.set(val, forKey: key)
|
||||
|
||||
resultCompletionBlock(true)
|
||||
}
|
||||
|
||||
@objc private func getBool(_ notification: Notification) {
|
||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
||||
guard
|
||||
let params = notification.userInfo?["params"] as? [String: Any],
|
||||
let key = params[HackiCore.keyKey] as? String else {
|
||||
resultCompletionBlock(false)
|
||||
return
|
||||
}
|
||||
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
let val = keyStore.bool(forKey: key)
|
||||
|
||||
resultCompletionBlock(val)
|
||||
}
|
||||
|
||||
@objc private func setStringList(_ notification: Notification) {
|
||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
||||
guard
|
||||
let params = notification.userInfo?["params"] as? [String: Any],
|
||||
let key = params[HackiCore.keyKey] as? String,
|
||||
let val = params[HackiCore.valKey] as? [String] else {
|
||||
resultCompletionBlock(false)
|
||||
return
|
||||
}
|
||||
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
keyStore.set(val, forKey: key)
|
||||
|
||||
resultCompletionBlock(true)
|
||||
}
|
||||
|
||||
@objc private func getStringList(_ notification: Notification) {
|
||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
||||
guard
|
||||
let params = notification.userInfo?["params"] as? [String: Any],
|
||||
let key = params[HackiCore.keyKey] as? String else {
|
||||
resultCompletionBlock(false)
|
||||
return
|
||||
}
|
||||
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
let list = keyStore.array(forKey: key) as [Any]? ?? [Any]()
|
||||
|
||||
resultCompletionBlock(list)
|
||||
}
|
||||
|
||||
@objc private func clearAll(_ notification: Notification) {
|
||||
guard let resultCompletionBlock: FlutterResult = notification.userInfo?["result"] as? FlutterResult else { fatalError(" failed to obtain result block") }
|
||||
|
||||
let keyStore = NSUbiquitousKeyValueStore()
|
||||
let allVals = keyStore.dictionaryRepresentation;
|
||||
let allKeys = allVals.keys
|
||||
|
||||
for key in allKeys.filter({ $0.contains("hasRead") }) {
|
||||
keyStore.removeObject(forKey: key)
|
||||
}
|
||||
|
||||
resultCompletionBlock(true)
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ abstract class Constants {
|
||||
static const String googlePlayLink =
|
||||
'https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US';
|
||||
static const String sponsorLink = 'https://github.com/sponsors/Livinglist';
|
||||
static const String guidelineLink =
|
||||
'https://news.ycombinator.com/newsguidelines.html';
|
||||
|
||||
static const String _imagePath = 'assets/images';
|
||||
static const String hackerNewsLogoPath = '$_imagePath/hacker_news_logo.png';
|
||||
|
@ -11,7 +11,7 @@ part 'collapse_state.dart';
|
||||
class CollapseCubit extends Cubit<CollapseState> {
|
||||
CollapseCubit({
|
||||
required int commentId,
|
||||
required CommentsCubit commentsCubit,
|
||||
required CommentsCubit? commentsCubit,
|
||||
CollapseCache? collapseCache,
|
||||
}) : _commentId = commentId,
|
||||
_collapseCache = collapseCache ?? locator.get<CollapseCache>(),
|
||||
@ -20,7 +20,7 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
|
||||
final int _commentId;
|
||||
final CollapseCache _collapseCache;
|
||||
final CommentsCubit _commentsCubit;
|
||||
final CommentsCubit? _commentsCubit;
|
||||
late final StreamSubscription<Map<int, Set<int>>> _streamSubscription;
|
||||
|
||||
void init() {
|
||||
@ -47,13 +47,15 @@ class CollapseCubit extends Cubit<CollapseState> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (_commentsCubit == null) return;
|
||||
|
||||
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
|
||||
final int lastCommentId = _commentsCubit.state.comments.last.id;
|
||||
final int lastCommentId = _commentsCubit!.state.comments.last.id;
|
||||
final bool shouldLoadMore = _commentId == lastCommentId ||
|
||||
collapsedCommentIds.contains(lastCommentId);
|
||||
|
||||
if (shouldLoadMore) {
|
||||
_commentsCubit.loadMore();
|
||||
_commentsCubit!.loadMore();
|
||||
}
|
||||
|
||||
emit(
|
||||
|
@ -80,7 +80,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
state.copyWith(
|
||||
comments: targetParents,
|
||||
onlyShowTargetComment: true,
|
||||
status: CommentsStatus.loaded,
|
||||
status: CommentsStatus.allLoaded,
|
||||
),
|
||||
);
|
||||
|
||||
@ -141,21 +141,21 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
if (state.offlineReading) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loading,
|
||||
),
|
||||
);
|
||||
|
||||
if (state.offlineReading) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.allLoaded,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_collapseCache.resetCollapsedComments();
|
||||
|
||||
await _streamSubscription?.cancel();
|
||||
@ -195,7 +195,6 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
item: updatedItem,
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -370,12 +369,17 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
if (!isHidden) {
|
||||
_streamSubscription?.pause();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentPage: state.currentPage + 1,
|
||||
status: CommentsStatus.loaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -64,6 +64,8 @@ class PreferenceState extends Equatable {
|
||||
|
||||
bool get showMetadata => _isOn<MetadataModePreference>();
|
||||
|
||||
bool get showUrl => _isOn<StoryUrlModePreference>();
|
||||
|
||||
bool get tapAnywhereToCollapse => _isOn<CollapseModePreference>();
|
||||
|
||||
FetchMode get fetchMode => FetchMode.values
|
||||
|
@ -90,9 +90,9 @@ Future<void> main({bool testing = false}) async {
|
||||
} else if (Platform.isAndroid) {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarDividerColor: Colors.transparent,
|
||||
statusBarColor: Palette.transparent,
|
||||
systemNavigationBarColor: Palette.transparent,
|
||||
systemNavigationBarDividerColor: Palette.transparent,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -18,10 +18,11 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
||||
CommentsOrderPreference(),
|
||||
FontSizePreference(),
|
||||
// order here reflects the order on settings screen.
|
||||
const NotificationModePreference(),
|
||||
const CollapseModePreference(),
|
||||
const DisplayModePreference(),
|
||||
const MetadataModePreference(),
|
||||
const StoryUrlModePreference(),
|
||||
const NotificationModePreference(),
|
||||
const CollapseModePreference(),
|
||||
NavigationModePreference(),
|
||||
const ReaderModePreference(),
|
||||
const MarkReadStoriesModePreference(),
|
||||
@ -50,6 +51,7 @@ const bool _trueDarkModeDefaultValue = false;
|
||||
const bool _readerModeDefaultValue = true;
|
||||
const bool _markReadStoriesModeDefaultValue = true;
|
||||
const bool _metadataModeDefaultValue = true;
|
||||
const bool _storyUrlModeDefaultValue = true;
|
||||
const bool _collapseModeDefaultValue = false;
|
||||
final int _fetchModeDefaultValue = FetchMode.eager.index;
|
||||
final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
||||
@ -132,6 +134,25 @@ class MetadataModePreference extends BooleanPreference {
|
||||
'''show number of comments and post date in story tile.''';
|
||||
}
|
||||
|
||||
class StoryUrlModePreference extends BooleanPreference {
|
||||
const StoryUrlModePreference({bool? val})
|
||||
: super(val: val ?? _storyUrlModeDefaultValue);
|
||||
|
||||
@override
|
||||
StoryUrlModePreference copyWith({required bool? val}) {
|
||||
return StoryUrlModePreference(val: val);
|
||||
}
|
||||
|
||||
@override
|
||||
String get key => 'storyUrlMode';
|
||||
|
||||
@override
|
||||
String get title => 'Show Url';
|
||||
|
||||
@override
|
||||
String get subtitle => '''show url in story tile.''';
|
||||
}
|
||||
|
||||
/// The value deciding whether or not user should be
|
||||
/// navigated to web view first. Defaults to false.
|
||||
class NavigationModePreference extends BooleanPreference {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
enum StoryType {
|
||||
@ -67,6 +68,24 @@ class Story extends Item {
|
||||
type: '',
|
||||
);
|
||||
|
||||
Story.placeholder()
|
||||
: super(
|
||||
id: 0,
|
||||
score: 0,
|
||||
descendants: 0,
|
||||
time: 1171872000,
|
||||
by: 'Y Combinator',
|
||||
title: 'Hacker News Guidelines',
|
||||
url: Constants.guidelineLink,
|
||||
kids: <int>[],
|
||||
dead: false,
|
||||
parts: <int>[],
|
||||
deleted: false,
|
||||
parent: 0,
|
||||
text: '',
|
||||
type: '',
|
||||
);
|
||||
|
||||
Story.fromJson(Map<String, dynamic> json)
|
||||
: super(
|
||||
descendants: json['descendants'] as int? ?? 0,
|
||||
@ -91,6 +110,12 @@ class Story extends Item {
|
||||
String get simpleMetadata =>
|
||||
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate''';
|
||||
|
||||
String get readableUrl {
|
||||
final Uri url = Uri.parse(this.url);
|
||||
final String authority = url.authority.replaceFirst('www.', '');
|
||||
return authority;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
|
@ -190,6 +190,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
onTap: () => onStoryTapped(story, isPin: true),
|
||||
showWebPreview: preferenceState.showComplexStoryTile,
|
||||
showMetadata: preferenceState.showMetadata,
|
||||
showUrl: preferenceState.showUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -255,145 +255,151 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
},
|
||||
),
|
||||
],
|
||||
child: BlocConsumer<CommentsCubit, CommentsState>(
|
||||
child: BlocListener<CommentsCubit, CommentsState>(
|
||||
listenWhen: (CommentsState previous, CommentsState current) =>
|
||||
previous.status != current.status,
|
||||
listener: (BuildContext context, CommentsState state) {
|
||||
if (state.status == CommentsStatus.loaded) {
|
||||
if (state.status != CommentsStatus.loading) {
|
||||
refreshController
|
||||
..refreshCompleted()
|
||||
..loadComplete();
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, CommentsState state) {
|
||||
final Widget mainView = MainView(
|
||||
scrollController: scrollController,
|
||||
refreshController: refreshController,
|
||||
commentEditingController: commentEditingController,
|
||||
authState: authState,
|
||||
state: state,
|
||||
focusNode: focusNode,
|
||||
topPadding: topPadding,
|
||||
splitViewEnabled: widget.splitViewEnabled,
|
||||
onMoreTapped: onMoreTapped,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
onLoginTapped: onLoginTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
);
|
||||
|
||||
return BlocListener<EditCubit, EditState>(
|
||||
listenWhen: (EditState previous, EditState current) {
|
||||
return previous.replyingTo != current.replyingTo ||
|
||||
previous.itemBeingEdited != current.itemBeingEdited ||
|
||||
commentEditingController.text != current.text;
|
||||
},
|
||||
listener: (BuildContext context, EditState editState) {
|
||||
if (editState.replyingTo != null ||
|
||||
editState.itemBeingEdited != null) {
|
||||
if (editState.text == null) {
|
||||
commentEditingController.clear();
|
||||
} else {
|
||||
final String text = editState.text!;
|
||||
commentEditingController
|
||||
..text = text
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: text.length),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
child: BlocListener<EditCubit, EditState>(
|
||||
listenWhen: (EditState previous, EditState current) {
|
||||
return previous.replyingTo != current.replyingTo ||
|
||||
previous.itemBeingEdited != current.itemBeingEdited ||
|
||||
commentEditingController.text != current.text;
|
||||
},
|
||||
listener: (BuildContext context, EditState editState) {
|
||||
if (editState.replyingTo != null ||
|
||||
editState.itemBeingEdited != null) {
|
||||
if (editState.text == null) {
|
||||
commentEditingController.clear();
|
||||
} else {
|
||||
final String text = editState.text!;
|
||||
commentEditingController
|
||||
..text = text
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: text.length),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: widget.splitViewEnabled
|
||||
? Material(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(
|
||||
child: mainView,
|
||||
} else {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
},
|
||||
child: widget.splitViewEnabled
|
||||
? Material(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(
|
||||
child: MainView(
|
||||
scrollController: scrollController,
|
||||
refreshController: refreshController,
|
||||
commentEditingController:
|
||||
commentEditingController,
|
||||
authState: authState,
|
||||
focusNode: focusNode,
|
||||
topPadding: topPadding,
|
||||
splitViewEnabled: widget.splitViewEnabled,
|
||||
onMoreTapped: onMoreTapped,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
onLoginTapped: onLoginTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
),
|
||||
BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
buildWhen: (
|
||||
SplitViewState previous,
|
||||
SplitViewState current,
|
||||
) =>
|
||||
previous.expanded != current.expanded,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
SplitViewState state,
|
||||
) {
|
||||
return Positioned(
|
||||
top: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: CustomAppBar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.canvasColor
|
||||
.withOpacity(0.6),
|
||||
item: widget.item,
|
||||
scrollController: scrollController,
|
||||
onBackgroundTap:
|
||||
onFeatureDiscoveryDismissed,
|
||||
onDismiss: onFeatureDiscoveryDismissed,
|
||||
splitViewEnabled: state.enabled,
|
||||
expanded: state.expanded,
|
||||
onZoomTap:
|
||||
context.read<SplitViewCubit>().zoom,
|
||||
onFontSizeTap: onFontSizeTapped,
|
||||
fontSizeIconButtonKey:
|
||||
fontSizeIconButtonKey,
|
||||
),
|
||||
);
|
||||
),
|
||||
BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
buildWhen: (
|
||||
SplitViewState previous,
|
||||
SplitViewState current,
|
||||
) =>
|
||||
previous.expanded != current.expanded,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
SplitViewState state,
|
||||
) {
|
||||
return Positioned(
|
||||
top: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: CustomAppBar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.canvasColor
|
||||
.withOpacity(0.6),
|
||||
item: widget.item,
|
||||
scrollController: scrollController,
|
||||
onBackgroundTap: onFeatureDiscoveryDismissed,
|
||||
onDismiss: onFeatureDiscoveryDismissed,
|
||||
splitViewEnabled: state.enabled,
|
||||
expanded: state.expanded,
|
||||
onZoomTap:
|
||||
context.read<SplitViewCubit>().zoom,
|
||||
onFontSizeTap: onFontSizeTapped,
|
||||
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
bottom: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: ReplyBox(
|
||||
splitViewEnabled: true,
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged:
|
||||
context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
Positioned(
|
||||
bottom: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: ReplyBox(
|
||||
splitViewEnabled: true,
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged:
|
||||
context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: CustomAppBar(
|
||||
backgroundColor:
|
||||
Theme.of(context).canvasColor.withOpacity(0.6),
|
||||
item: widget.item,
|
||||
scrollController: scrollController,
|
||||
onBackgroundTap: onFeatureDiscoveryDismissed,
|
||||
onDismiss: onFeatureDiscoveryDismissed,
|
||||
onFontSizeTap: onFontSizeTapped,
|
||||
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||
),
|
||||
body: mainView,
|
||||
bottomSheet: ReplyBox(
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged: context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: CustomAppBar(
|
||||
backgroundColor:
|
||||
Theme.of(context).canvasColor.withOpacity(0.6),
|
||||
item: widget.item,
|
||||
scrollController: scrollController,
|
||||
onBackgroundTap: onFeatureDiscoveryDismissed,
|
||||
onDismiss: onFeatureDiscoveryDismissed,
|
||||
onFontSizeTap: onFontSizeTapped,
|
||||
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||
),
|
||||
body: MainView(
|
||||
scrollController: scrollController,
|
||||
refreshController: refreshController,
|
||||
commentEditingController: commentEditingController,
|
||||
authState: authState,
|
||||
focusNode: focusNode,
|
||||
topPadding: topPadding,
|
||||
splitViewEnabled: widget.splitViewEnabled,
|
||||
onMoreTapped: onMoreTapped,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
onLoginTapped: onLoginTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
),
|
||||
bottomSheet: ReplyBox(
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
context.read<EditCubit>().onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged: context.read<EditCubit>().onTextChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -442,7 +448,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
fontSize: fontSize.fontSize,
|
||||
color:
|
||||
context.read<PreferenceCubit>().state.fontSize == fontSize
|
||||
? Colors.deepOrange
|
||||
? Palette.deepOrange
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
@ -22,7 +22,6 @@ class MainView extends StatelessWidget {
|
||||
required this.refreshController,
|
||||
required this.commentEditingController,
|
||||
required this.authState,
|
||||
required this.state,
|
||||
required this.focusNode,
|
||||
required this.topPadding,
|
||||
required this.splitViewEnabled,
|
||||
@ -36,7 +35,6 @@ class MainView extends StatelessWidget {
|
||||
final RefreshController refreshController;
|
||||
final TextEditingController commentEditingController;
|
||||
final AuthState authState;
|
||||
final CommentsState state;
|
||||
final FocusNode focusNode;
|
||||
final double topPadding;
|
||||
final bool splitViewEnabled;
|
||||
@ -47,407 +45,506 @@ class MainView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SmartRefresher(
|
||||
scrollController: scrollController,
|
||||
enablePullUp: !state.onlyShowTargetComment,
|
||||
enablePullDown: !state.onlyShowTargetComment,
|
||||
header: WaterDropMaterialHeader(
|
||||
backgroundColor: Palette.orange,
|
||||
offset: topPadding,
|
||||
),
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
builder: (BuildContext context, LoadStatus? mode) {
|
||||
const double height = 55;
|
||||
late final Widget body;
|
||||
|
||||
if (mode == LoadStatus.idle) {
|
||||
body = const Text('');
|
||||
} else if (mode == LoadStatus.loading) {
|
||||
body = const Text('');
|
||||
} else if (mode == LoadStatus.failed) {
|
||||
body = const Text(
|
||||
'',
|
||||
);
|
||||
} else if (mode == LoadStatus.canLoading) {
|
||||
body = const Text(
|
||||
'',
|
||||
);
|
||||
} else {
|
||||
body = const Text('');
|
||||
}
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Center(child: body),
|
||||
);
|
||||
},
|
||||
),
|
||||
controller: refreshController,
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (context.read<StoriesBloc>().state.offlineReading) {
|
||||
refreshController.refreshCompleted();
|
||||
} else {
|
||||
context.read<CommentsCubit>().refresh();
|
||||
|
||||
if (state.item.isPoll) {
|
||||
context.read<PollCubit>().refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoading: () {
|
||||
if (state.fetchMode == FetchMode.eager) {
|
||||
context.read<CommentsCubit>().loadMore();
|
||||
} else {
|
||||
refreshController.loadComplete();
|
||||
}
|
||||
},
|
||||
child: ListView.builder(
|
||||
primary: false,
|
||||
itemCount: state.comments.length + 2,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(
|
||||
child: BlocBuilder<CommentsCubit, CommentsState>(
|
||||
builder: (BuildContext context, CommentsState state) {
|
||||
return SmartRefresher(
|
||||
scrollController: scrollController,
|
||||
enablePullUp: !state.onlyShowTargetComment,
|
||||
enablePullDown: !state.onlyShowTargetComment,
|
||||
header: WaterDropMaterialHeader(
|
||||
backgroundColor: Palette.orange,
|
||||
offset: topPadding,
|
||||
),
|
||||
if (!splitViewEnabled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
builder: (BuildContext context, LoadStatus? mode) {
|
||||
const double height = 55;
|
||||
late final Widget body;
|
||||
|
||||
if (state.item.id !=
|
||||
if (mode == LoadStatus.idle) {
|
||||
body = const Text('');
|
||||
} else if (mode == LoadStatus.loading) {
|
||||
body = const Text('');
|
||||
} else if (mode == LoadStatus.failed) {
|
||||
body = const Text(
|
||||
'',
|
||||
);
|
||||
} else if (mode == LoadStatus.canLoading) {
|
||||
body = const Text(
|
||||
'',
|
||||
);
|
||||
} else {
|
||||
body = const Text('');
|
||||
}
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Center(child: body),
|
||||
);
|
||||
},
|
||||
),
|
||||
controller: refreshController,
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (context.read<StoriesBloc>().state.offlineReading) {
|
||||
refreshController.refreshCompleted();
|
||||
} else {
|
||||
context.read<CommentsCubit>().refresh();
|
||||
|
||||
if (state.item.isPoll) {
|
||||
context.read<PollCubit>().refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoading: () {
|
||||
if (state.fetchMode == FetchMode.eager) {
|
||||
context.read<CommentsCubit>().loadMore();
|
||||
} else {
|
||||
refreshController.loadComplete();
|
||||
}
|
||||
},
|
||||
child: ListView.builder(
|
||||
primary: false,
|
||||
itemCount: state.comments.length + 2,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return _ParentItemSection(
|
||||
scrollController: scrollController,
|
||||
refreshController: refreshController,
|
||||
commentEditingController: commentEditingController,
|
||||
state: state,
|
||||
authState: authState,
|
||||
focusNode: focusNode,
|
||||
topPadding: topPadding,
|
||||
splitViewEnabled: splitViewEnabled,
|
||||
onMoreTapped: onMoreTapped,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
onLoginTapped: onLoginTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
);
|
||||
} else if (index == state.comments.length + 1) {
|
||||
if ((state.status == CommentsStatus.allLoaded &&
|
||||
state.comments.isNotEmpty) ||
|
||||
state.onlyShowTargetComment) {
|
||||
return SizedBox(
|
||||
height: 240,
|
||||
child: Center(
|
||||
child: Text(Constants.happyFaces.pickRandomly()!),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
index = index - 1;
|
||||
final Comment comment = state.comments.elementAt(index);
|
||||
return FadeIn(
|
||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||
child: CommentTile(
|
||||
comment: comment,
|
||||
level: comment.level,
|
||||
myUsername:
|
||||
authState.isLoggedIn ? authState.username : null,
|
||||
opUsername: state.item.by,
|
||||
fetchMode: state.fetchMode,
|
||||
onReplyTapped: (Comment cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmt.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context.read<EditCubit>().onReplyTapped(state.item);
|
||||
|
||||
context.read<EditCubit>().onReplyTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
onEditTapped: (Comment cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
commentEditingController.clear();
|
||||
context.read<EditCubit>().onEditTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onMoreTapped: onMoreTapped,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
height: Dimens.pt4,
|
||||
bottom: Dimens.zero,
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: BlocBuilder<CommentsCubit, CommentsState>(
|
||||
buildWhen: (CommentsState prev, CommentsState current) =>
|
||||
prev.status != current.status,
|
||||
builder: (BuildContext context, CommentsState state) {
|
||||
return Visibility(
|
||||
visible: state.status == CommentsStatus.loading,
|
||||
child: const LinearProgressIndicator(),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ParentItemSection extends StatelessWidget {
|
||||
const _ParentItemSection({
|
||||
Key? key,
|
||||
required this.scrollController,
|
||||
required this.refreshController,
|
||||
required this.commentEditingController,
|
||||
required this.state,
|
||||
required this.authState,
|
||||
required this.focusNode,
|
||||
required this.topPadding,
|
||||
required this.splitViewEnabled,
|
||||
required this.onMoreTapped,
|
||||
required this.onStoryLinkTapped,
|
||||
required this.onLoginTapped,
|
||||
required this.onRightMoreTapped,
|
||||
}) : super(key: key);
|
||||
|
||||
final ScrollController scrollController;
|
||||
final RefreshController refreshController;
|
||||
final TextEditingController commentEditingController;
|
||||
final CommentsState state;
|
||||
final AuthState authState;
|
||||
final FocusNode focusNode;
|
||||
final double topPadding;
|
||||
final bool splitViewEnabled;
|
||||
final Function(Item item, Rect? rect) onMoreTapped;
|
||||
final ValueChanged<String> onStoryLinkTapped;
|
||||
final VoidCallback onLoginTapped;
|
||||
final ValueChanged<Comment> onRightMoreTapped;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
),
|
||||
if (!splitViewEnabled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (state.item.id !=
|
||||
context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
context.read<EditCubit>().onReplyTapped(state.item);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(state.item, context.rect),
|
||||
backgroundColor: Palette.orange,
|
||||
foregroundColor: Palette.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.fontSize != current.fontSize,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.item.by,
|
||||
style: const TextStyle(
|
||||
color: Palette.orange,
|
||||
),
|
||||
if (state.item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
state.item.url,
|
||||
useReader:
|
||||
context.read<PreferenceCubit>().state.useReader,
|
||||
offlineReading: context
|
||||
.read<StoriesBloc>()
|
||||
.state
|
||||
.offlineReading,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
state.item.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (
|
||||
PreferenceState previous,
|
||||
PreferenceState current,
|
||||
) =>
|
||||
previous.fontSize != current.fontSize,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
PreferenceState prefState,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (state.item is Story)
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launch(
|
||||
state.item.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.useReader,
|
||||
offlineReading: context
|
||||
.read<StoriesBloc>()
|
||||
.state
|
||||
.offlineReading,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
?.color,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: state.item.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
state.item.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
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,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
(prefState.fontSize.fontSize - 4),
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: state.item.text,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
child: PollView(
|
||||
onLoginTapped: onLoginTapped,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: state.item.text,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (state.item.isPoll)
|
||||
BlocProvider<PollCubit>(
|
||||
create: (BuildContext context) =>
|
||||
PollCubit(story: state.item as Story)..init(),
|
||||
child: PollView(
|
||||
onLoginTapped: onLoginTapped,
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...<Widget>[
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
context.read<CommentsCubit>().loadAll(state.item as Story),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...<Widget>[
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => context
|
||||
.read<CommentsCubit>()
|
||||
.loadAll(state.item as Story),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
Text(
|
||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (state.item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: context.read<CommentsCubit>().loadParentThread,
|
||||
child: state.fetchParentStatus == CommentsStatus.loading
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent thread',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (!state.offlineReading)
|
||||
DropdownButton<FetchMode>(
|
||||
value: state.fetchMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: FetchMode.values
|
||||
.map(
|
||||
(FetchMode val) => DropdownMenuItem<FetchMode>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onFetchModeChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
),
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: state.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: CommentsOrder.values
|
||||
.map(
|
||||
(CommentsOrder val) => DropdownMenuItem<CommentsOrder>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
TextButton(
|
||||
onPressed:
|
||||
context.read<CommentsCubit>().loadParentThread,
|
||||
child:
|
||||
state.fetchParentStatus == CommentsStatus.loading
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent thread',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (!state.offlineReading)
|
||||
DropdownButton<FetchMode>(
|
||||
value: state.fetchMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: FetchMode.values
|
||||
.map(
|
||||
(FetchMode val) => DropdownMenuItem<FetchMode>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged:
|
||||
context.read<CommentsCubit>().onFetchModeChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
),
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: state.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: CommentsOrder.values
|
||||
.map(
|
||||
(CommentsOrder val) =>
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onOrderChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
if (state.comments.isEmpty &&
|
||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Palette.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
} else if (index == state.comments.length + 1) {
|
||||
if ((state.status == CommentsStatus.allLoaded &&
|
||||
state.comments.isNotEmpty) ||
|
||||
state.onlyShowTargetComment) {
|
||||
return SizedBox(
|
||||
height: 240,
|
||||
child: Center(
|
||||
child: Text(Constants.happyFaces.pickRandomly()!),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
index = index - 1;
|
||||
final Comment comment = state.comments.elementAt(index);
|
||||
return FadeIn(
|
||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||
child: CommentTile(
|
||||
comment: comment,
|
||||
level: comment.level,
|
||||
myUsername: authState.isLoggedIn ? authState.username : null,
|
||||
opUsername: state.item.by,
|
||||
fetchMode: state.fetchMode,
|
||||
onReplyTapped: (Comment cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmt.id != context.read<EditCubit>().state.replyingTo?.id) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
|
||||
context.read<EditCubit>().onReplyTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onEditTapped: (Comment cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
commentEditingController.clear();
|
||||
context.read<EditCubit>().onEditTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onMoreTapped: onMoreTapped,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
onRightMoreTapped: onRightMoreTapped,
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().onOrderChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
),
|
||||
],
|
||||
if (state.comments.isEmpty &&
|
||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Palette.grey),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
@ -122,6 +123,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
return ItemsListView<Item>(
|
||||
showWebPreview: false,
|
||||
showMetadata: false,
|
||||
showUrl: false,
|
||||
useConsistentFontSize: true,
|
||||
refreshController: refreshControllerHistory,
|
||||
items: historyState.submittedItems
|
||||
@ -174,6 +176,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
showWebPreview:
|
||||
preferenceState.showComplexStoryTile,
|
||||
showMetadata: preferenceState.showMetadata,
|
||||
showUrl: preferenceState.showUrl,
|
||||
useCommentTile: true,
|
||||
refreshController: refreshControllerFav,
|
||||
items: favState.favItems,
|
||||
@ -374,6 +377,17 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
StoryTile(
|
||||
showWebPreview:
|
||||
preferenceState.showComplexStoryTile,
|
||||
showMetadata: preferenceState.showMetadata,
|
||||
showUrl: preferenceState.showUrl,
|
||||
story: Story.placeholder(),
|
||||
onTap: () =>
|
||||
LinkUtil.launch(Constants.guidelineLink),
|
||||
),
|
||||
const Divider(),
|
||||
for (final Preference<dynamic> preference
|
||||
in preferenceState.preferences
|
||||
.whereType<BooleanPreference>()
|
||||
@ -422,93 +436,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
title: const Text('About'),
|
||||
subtitle:
|
||||
const Text('nothing interesting here.'),
|
||||
onTap: () {
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Hacki',
|
||||
applicationVersion: 'v1.0.0',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(
|
||||
Dimens.pt12,
|
||||
),
|
||||
),
|
||||
child: Image.asset(
|
||||
Constants.hackiIconPath,
|
||||
height: Dimens.pt50,
|
||||
width: Dimens.pt50,
|
||||
),
|
||||
),
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.portfolioLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Icon(
|
||||
FontAwesomeIcons.addressCard,
|
||||
),
|
||||
SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Developer'),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.githubLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Icon(
|
||||
FontAwesomeIcons.github,
|
||||
),
|
||||
SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Source code'),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Platform.isIOS
|
||||
? Constants.appStoreLink
|
||||
: Constants.googlePlayLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Icon(
|
||||
Icons.thumb_up,
|
||||
),
|
||||
SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Like the app?'),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.sponsorLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Icon(
|
||||
FeatherIcons.coffee,
|
||||
),
|
||||
SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Buy me a coffee'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
onTap: showAboutHackiDialog,
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt48,
|
||||
@ -718,6 +646,95 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showAboutHackiDialog() async {
|
||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
final String version = packageInfo.version;
|
||||
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Hacki',
|
||||
applicationVersion: 'v$version',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(
|
||||
Dimens.pt12,
|
||||
),
|
||||
),
|
||||
child: Image.asset(
|
||||
Constants.hackiIconPath,
|
||||
height: Dimens.pt50,
|
||||
width: Dimens.pt50,
|
||||
),
|
||||
),
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.portfolioLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Icon(
|
||||
FontAwesomeIcons.addressCard,
|
||||
),
|
||||
SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Developer'),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.githubLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Icon(
|
||||
FontAwesomeIcons.github,
|
||||
),
|
||||
SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Source code'),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Platform.isIOS ? Constants.appStoreLink : Constants.googlePlayLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Icon(
|
||||
Icons.thumb_up,
|
||||
),
|
||||
SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Like the app?'),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => LinkUtil.launch(
|
||||
Constants.sponsorLink,
|
||||
),
|
||||
child: Row(
|
||||
children: const <Widget>[
|
||||
Icon(
|
||||
FeatherIcons.coffee,
|
||||
),
|
||||
SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text('Buy me a coffee'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void onCommentTapped(Comment comment, {VoidCallback? then}) {
|
||||
throttle.run(() {
|
||||
locator
|
||||
|
@ -166,6 +166,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
showWebPreview:
|
||||
prefState.showComplexStoryTile,
|
||||
showMetadata: prefState.showMetadata,
|
||||
showUrl: prefState.showUrl,
|
||||
story: e,
|
||||
onTap: () => goToItemScreen(
|
||||
args: ItemScreenArgs(item: e),
|
||||
|
@ -47,7 +47,7 @@ class CommentTile extends StatelessWidget {
|
||||
lazy: false,
|
||||
create: (_) => CollapseCubit(
|
||||
commentId: comment.id,
|
||||
commentsCubit: context.read<CommentsCubit>(),
|
||||
commentsCubit: context.tryRead<CommentsCubit>(),
|
||||
collapseCache: context.tryRead<CollapseCache>() ?? CollapseCache(),
|
||||
)..init(),
|
||||
child: BlocBuilder3<CollapseCubit, CollapseState, PreferenceCubit,
|
||||
|
@ -16,6 +16,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
super.key,
|
||||
required this.showWebPreview,
|
||||
required this.showMetadata,
|
||||
required this.showUrl,
|
||||
required this.items,
|
||||
required this.onTap,
|
||||
required this.refreshController,
|
||||
@ -39,6 +40,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
final bool showCommentBy;
|
||||
final bool showWebPreview;
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
final bool enablePullDown;
|
||||
final bool markReadStories;
|
||||
final bool showOfflineBanner;
|
||||
@ -97,6 +99,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
onTap: () => onTap(e),
|
||||
showWebPreview: showWebPreview,
|
||||
showMetadata: showMetadata,
|
||||
showUrl: showUrl,
|
||||
hasRead: markReadStories && hasRead,
|
||||
simpleTileFontSize: useConsistentFontSize
|
||||
? TextDimens.pt14
|
||||
|
@ -15,6 +15,7 @@ class LinkPreview extends StatefulWidget {
|
||||
required this.link,
|
||||
required this.story,
|
||||
required this.showMetadata,
|
||||
required this.showUrl,
|
||||
required this.offlineReading,
|
||||
this.cache = const Duration(days: 30),
|
||||
this.titleStyle,
|
||||
@ -103,6 +104,7 @@ class LinkPreview extends StatefulWidget {
|
||||
final List<BoxShadow>? boxShadow;
|
||||
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
final bool offlineReading;
|
||||
|
||||
@override
|
||||
@ -180,6 +182,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
key: widget.key ?? Key(widget.link),
|
||||
metadata: widget.story.simpleMetadata,
|
||||
url: widget.link,
|
||||
readableUrl: widget.story.readableUrl,
|
||||
title: widget.story.title,
|
||||
description: desc ?? title ?? 'no comment yet.',
|
||||
imageUri: imageUri,
|
||||
@ -194,6 +197,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
bgColor: widget.backgroundColor,
|
||||
radius: widget.borderRadius ?? 12,
|
||||
showMetadata: widget.showMetadata,
|
||||
showUrl: widget.showUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -8,10 +8,12 @@ class LinkView extends StatelessWidget {
|
||||
super.key,
|
||||
required this.metadata,
|
||||
required this.url,
|
||||
required this.readableUrl,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.onTap,
|
||||
required this.showMetadata,
|
||||
required this.showUrl,
|
||||
this.imageUri,
|
||||
this.imagePath,
|
||||
this.titleTextStyle,
|
||||
@ -30,6 +32,7 @@ class LinkView extends StatelessWidget {
|
||||
|
||||
final String metadata;
|
||||
final String url;
|
||||
final String readableUrl;
|
||||
final String title;
|
||||
final String description;
|
||||
final String? imageUri;
|
||||
@ -44,6 +47,7 @@ class LinkView extends StatelessWidget {
|
||||
final double radius;
|
||||
final Color? bgColor;
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
|
||||
double computeTitleFontSize(double width) {
|
||||
double size = width * 0.13;
|
||||
@ -146,6 +150,7 @@ class LinkView extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildTitleContainer(TextStyle _titleTS, int _maxLines) {
|
||||
final bool showUrl = this.showUrl && url.isNotEmpty;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
|
||||
child: Column(
|
||||
@ -159,6 +164,22 @@ class LinkView extends StatelessWidget {
|
||||
maxLines: _maxLines,
|
||||
),
|
||||
),
|
||||
if (showUrl)
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
'($readableUrl)',
|
||||
textAlign: TextAlign.left,
|
||||
style: _titleTS.copyWith(
|
||||
color: Palette.grey,
|
||||
fontSize:
|
||||
_titleTS.fontSize == null ? 12 : _titleTS.fontSize! - 4,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -193,8 +214,9 @@ class LinkView extends StatelessWidget {
|
||||
textAlign: TextAlign.left,
|
||||
style: _bodyTS,
|
||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||
maxLines:
|
||||
(bodyMaxLines ?? _maxLines) - (showMetadata ? 1 : 0),
|
||||
maxLines: (bodyMaxLines ?? _maxLines) -
|
||||
(showMetadata ? 1 : 0) -
|
||||
(showUrl && url.isNotEmpty ? 1 : 0),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -68,6 +68,7 @@ class _StoriesListViewState extends State<StoriesListView> {
|
||||
context.read<PreferenceCubit>().state.markReadStories,
|
||||
showWebPreview: preferenceState.showComplexStoryTile,
|
||||
showMetadata: preferenceState.showMetadata,
|
||||
showUrl: preferenceState.showUrl,
|
||||
refreshController: refreshController,
|
||||
items: state.storiesByType[storyType]!,
|
||||
onRefresh: () {
|
||||
|
@ -15,6 +15,7 @@ class StoryTile extends StatelessWidget {
|
||||
this.hasRead = false,
|
||||
required this.showWebPreview,
|
||||
required this.showMetadata,
|
||||
required this.showUrl,
|
||||
required this.story,
|
||||
required this.onTap,
|
||||
this.simpleTileFontSize = 16,
|
||||
@ -22,6 +23,7 @@ class StoryTile extends StatelessWidget {
|
||||
|
||||
final bool showWebPreview;
|
||||
final bool showMetadata;
|
||||
final bool showUrl;
|
||||
final bool hasRead;
|
||||
final Story story;
|
||||
final VoidCallback onTap;
|
||||
@ -58,6 +60,7 @@ class StoryTile extends StatelessWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
showMetadata: showMetadata,
|
||||
showUrl: showUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -76,11 +79,31 @@ class StoryTile extends StatelessWidget {
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
story.title,
|
||||
style: TextStyle(
|
||||
color: hasRead ? Palette.grey[500] : null,
|
||||
fontSize: simpleTileFontSize,
|
||||
child: RichText(
|
||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||
text: TextSpan(
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: story.title,
|
||||
style: TextStyle(
|
||||
color: hasRead
|
||||
? Palette.grey[500]
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
?.color,
|
||||
fontSize: simpleTileFontSize,
|
||||
),
|
||||
),
|
||||
if (showUrl && story.url.isNotEmpty)
|
||||
TextSpan(
|
||||
text: ' (${story.readableUrl})',
|
||||
style: TextStyle(
|
||||
color: Palette.grey[500],
|
||||
fontSize: simpleTileFontSize - 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
14
pubspec.lock
14
pubspec.lock
@ -607,6 +607,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1,6 +1,6 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 1.0.0+77
|
||||
version: 1.0.2+80
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@ -18,7 +18,6 @@ dependencies:
|
||||
dio: ^4.0.4
|
||||
equatable: ^2.0.5
|
||||
fast_gbk: ^1.0.0
|
||||
# feature_discovery: ^0.14.0
|
||||
feature_discovery:
|
||||
git:
|
||||
url: https://github.com/livinglist/feature_discovery
|
||||
@ -45,11 +44,11 @@ dependencies:
|
||||
hydrated_bloc: ^9.0.0-dev.3
|
||||
intl: ^0.17.0
|
||||
logger: ^1.1.0
|
||||
package_info_plus: ^3.0.2
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.8
|
||||
path_provider_android: ^2.0.8
|
||||
path_provider_ios: ^2.0.8
|
||||
# pull_to_refresh: ^2.0.0
|
||||
pull_to_refresh:
|
||||
git:
|
||||
url: https://github.com/livinglist/flutter_pulltorefresh
|
||||
|
Reference in New Issue
Block a user