mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
f70377d99c | |||
e190826fcf | |||
6f8dfaca8e | |||
889011e249 | |||
c141bd7ba5 | |||
d10e630c17 | |||
5ae7769a36 | |||
4f9761b2fa | |||
c244435d01 | |||
db2de7aae4 | |||
35d7ffc09e | |||
dd4e56e31c | |||
38f3efbe50 | |||
4ebde84a79 | |||
382d2be5ba | |||
7f66c80577 | |||
21d47cd54c | |||
1139fa3c3b | |||
369a1e7bbf | |||
c3c86a4a44 | |||
5f39ad7df8 | |||
897ee81970 | |||
12098ce8cc |
1
.gitignore
vendored
1
.gitignore
vendored
@ -46,4 +46,3 @@ app.*.map.json
|
||||
/android/app/release
|
||||
|
||||
/ios/Podfile.lock
|
||||
pubspec.lock
|
||||
|
33
README.md
33
README.md
@ -2,7 +2,6 @@
|
||||
|
||||
A simple noiseless Hacker News reader made with Flutter that is just enough.
|
||||
|
||||

|
||||
[](https://apps.apple.com/us/app/hacki/id1602043763)
|
||||
[](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US)
|
||||
[](https://badges.pufler.dev)
|
||||
@ -31,18 +30,24 @@ Features:
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859621-965080f3-a191-44cd-a2fc-9ac1f489ef84.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859627-48290a22-9679-442b-bae4-97f21546b3ae.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859630-93f7e372-f2e7-4357-86c0-250a3f69c10f.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859632-b52a89ca-b8d7-464c-a508-faa86bcc87f8.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/155449312-4208a961-44ac-42b3-968b-9526d4a07787.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/150713047-2710add8-0493-4c42-a710-f96dc77cfde1.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/150918515-0fc4869f-efa3-473f-90af-381daf5e4915.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/152305175-94fa3696-f40f-4f40-b040-f17fc59ff260.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/152301588-070ded9a-117a-48d8-bad4-9f77d54d98df.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/152301590-5383200c-db73-487d-8742-57ccbbbf04e8.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/153973720-8a6aad44-7df3-4deb-8465-8c88b5e5f587.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/157003000-15671cf0-9470-4a89-b123-f63ca70a970f.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799288-6e98352a-fe89-4a2e-8a74-c5782463a1e1.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799297-75b52eac-2066-4df9-bdfc-7c82bf7b81c8.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799302-860c61b8-abba-486a-9592-bc84a6af3232.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799305-308743d3-1c89-45de-9645-3b6ec789c282.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798176-5212e9bf-296d-4d9b-ab48-19b741684c8a.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798179-72edbe49-7444-4e54-a07c-fc1244447a74.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798182-28397805-a7cc-4124-b65b-c02c80afbbec.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798183-c2984270-ee99-4419-841e-65e98890464f.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798184-2fce5d97-710e-44a7-b99a-3296ebcf273b.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798185-d7c81348-956e-483c-a1bc-5cd872bdad62.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798186-1457ae21-f1aa-40a4-9206-0f3a1e24653e.png">
|
||||
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798187-4404adea-b2bc-472e-8568-2379e6db01a4.png">
|
||||
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162711-a9146326-9645-4db6-a04e-1f82e6133e40.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162726-ef1d3f2a-5179-417c-8a5f-0cddb52249da.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162733-906c4afd-39a8-48ae-946a-8019b327eaa0.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162735-f2b25119-4702-4308-b2f5-281a2a2c5901.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160163024-6dcd65b6-bada-4c1c-95af-387fd4f42fb2.png">
|
||||
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160163033-7bcf7038-b9aa-4ce4-8b58-64578eae8531.png">
|
||||
</p>
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
D1B8F07E58D19B7A04062545 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A738A80ED7D259B7B74BB44E /* Pods_Runner.framework */; };
|
||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@ -47,6 +48,8 @@
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
9F471B54216646F2690E5F66 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
A738A80ED7D259B7B74BB44E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -55,6 +58,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D1B8F07E58D19B7A04062545 /* Pods_Runner.framework in Frameworks */,
|
||||
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -94,6 +98,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
@ -109,6 +114,7 @@
|
||||
B3F4F49CF582C662A01499C0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
|
||||
A738A80ED7D259B7B74BB44E /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
@ -354,6 +360,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
@ -365,13 +372,13 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.0;
|
||||
MARKETING_VERSION = 0.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
@ -489,6 +496,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
@ -500,14 +508,14 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.0;
|
||||
MARKETING_VERSION = 0.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
@ -518,6 +526,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
@ -529,13 +538,13 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.0;
|
||||
MARKETING_VERSION = 0.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
|
22
ios/Runner/Runner.entitlements
Normal file
22
ios/Runner/Runner.entitlements
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.jiaqi.hacki</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudDocuments</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.jiaqi.hacki</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||
</dict>
|
||||
</plist>
|
@ -48,6 +48,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
));
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
status: AuthStatus.loaded,
|
||||
isLoggedIn: false,
|
||||
));
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
class Constants {
|
||||
abstract class Constants {
|
||||
static const String endUserAgreementLink =
|
||||
'https://www.termsfeed.com/live/c1417f5c-a48b-4bd7-93b2-9cd4577bfc45';
|
||||
static const String hackerNewsLogoLink =
|
||||
|
@ -22,8 +22,10 @@ class CustomRouter {
|
||||
/// Nested routing for bottom navigation bar.
|
||||
static Route onGenerateNestedRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case HomeScreen.routeName:
|
||||
return HomeScreen.route();
|
||||
case StoryScreen.routeName:
|
||||
return StoryScreen.route(settings.arguments! as StoryScreenArgs);
|
||||
case SubmitScreen.routeName:
|
||||
return SubmitScreen.route();
|
||||
default:
|
||||
return _errorRoute();
|
||||
}
|
||||
|
@ -24,6 +24,13 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
|
||||
final CacheRepository _cacheRepository;
|
||||
final StoriesRepository _storiesRepository;
|
||||
|
||||
@override
|
||||
void emit(CommentsState state) {
|
||||
if (!isClosed) {
|
||||
super.emit(state);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> init({
|
||||
bool onlyShowTargetComment = false,
|
||||
Comment? targetComment,
|
||||
@ -63,11 +70,10 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
));
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
));
|
||||
} else {
|
||||
final comment = state.item as Comment;
|
||||
|
||||
@ -93,11 +99,9 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
));
|
||||
}
|
||||
emit(state.copyWith(
|
||||
status: CommentsStatus.loaded,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,7 +164,7 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
|
||||
}
|
||||
|
||||
void _onCommentFetched(Comment? comment) {
|
||||
if (comment != null && !isClosed) {
|
||||
if (comment != null) {
|
||||
_cacheService.cacheComment(comment);
|
||||
emit(state.copyWith(comments: List.from(state.comments)..add(comment)));
|
||||
}
|
||||
|
@ -9,5 +9,6 @@ export 'pin/pin_cubit.dart';
|
||||
export 'post/post_cubit.dart';
|
||||
export 'preference/preference_cubit.dart';
|
||||
export 'search/search_cubit.dart';
|
||||
export 'split_view/split_view_cubit.dart';
|
||||
export 'submit/submit_cubit.dart';
|
||||
export 'vote/vote_cubit.dart';
|
||||
|
17
lib/cubits/split_view/split_view_cubit.dart
Normal file
17
lib/cubits/split_view/split_view_cubit.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
|
||||
part 'split_view_state.dart';
|
||||
|
||||
class SplitViewCubit extends Cubit<SplitViewState> {
|
||||
SplitViewCubit() : super(const SplitViewState.init());
|
||||
|
||||
void updateStoryScreenArgs(StoryScreenArgs args) {
|
||||
emit(state.copyWith(storyScreenArgs: args));
|
||||
}
|
||||
|
||||
void enableSplitView() => emit(state.copyWith(enabled: true));
|
||||
|
||||
void disableSplitView() => emit(state.copyWith(enabled: false));
|
||||
}
|
28
lib/cubits/split_view/split_view_state.dart
Normal file
28
lib/cubits/split_view/split_view_state.dart
Normal file
@ -0,0 +1,28 @@
|
||||
part of 'split_view_cubit.dart';
|
||||
|
||||
class SplitViewState extends Equatable {
|
||||
const SplitViewState({
|
||||
required this.storyScreenArgs,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
const SplitViewState.init()
|
||||
: enabled = false,
|
||||
storyScreenArgs = null;
|
||||
|
||||
final bool enabled;
|
||||
final StoryScreenArgs? storyScreenArgs;
|
||||
|
||||
SplitViewState copyWith({bool? enabled, StoryScreenArgs? storyScreenArgs}) {
|
||||
return SplitViewState(
|
||||
enabled: enabled ?? this.enabled,
|
||||
storyScreenArgs: storyScreenArgs ?? this.storyScreenArgs,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
enabled,
|
||||
storyScreenArgs,
|
||||
];
|
||||
}
|
@ -1,2 +1,4 @@
|
||||
export 'date_time_extension.dart';
|
||||
export 'object_extension.dart';
|
||||
export 'state_extension.dart';
|
||||
export 'widget_extension.dart';
|
||||
|
7
lib/extensions/object_extension.dart
Normal file
7
lib/extensions/object_extension.dart
Normal file
@ -0,0 +1,7 @@
|
||||
import 'dart:developer' as dev;
|
||||
|
||||
extension ObjectExtension on Object {
|
||||
void log({String identifier = ''}) {
|
||||
dev.log('$identifier ${toString()}');
|
||||
}
|
||||
}
|
43
lib/extensions/state_extension.dart
Normal file
43
lib/extensions/state_extension.dart
Normal file
@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/screens/screens.dart' show StoryScreen, StoryScreenArgs;
|
||||
|
||||
extension StateExtension on State {
|
||||
void showSnackBar({
|
||||
required String content,
|
||||
VoidCallback? action,
|
||||
String? label,
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.deepOrange,
|
||||
content: Text(content),
|
||||
action: action != null && label != null
|
||||
? SnackBarAction(
|
||||
label: label,
|
||||
onPressed: action,
|
||||
textColor: Theme.of(context).textTheme.bodyText1?.color,
|
||||
)
|
||||
: null,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void>? goToStoryScreen({required StoryScreenArgs args}) {
|
||||
final splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||
|
||||
if (splitViewEnabled) {
|
||||
context.read<SplitViewCubit>().updateStoryScreenArgs(args);
|
||||
} else {
|
||||
return HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
StoryScreen.routeName,
|
||||
arguments: args,
|
||||
);
|
||||
}
|
||||
|
||||
return Future.value();
|
||||
}
|
||||
}
|
@ -87,6 +87,10 @@ class HackiApp extends StatelessWidget {
|
||||
lazy: false,
|
||||
create: (context) => CacheCubit(),
|
||||
),
|
||||
BlocProvider<SplitViewCubit>(
|
||||
lazy: false,
|
||||
create: (context) => SplitViewCubit(),
|
||||
),
|
||||
],
|
||||
child: AdaptiveTheme(
|
||||
light: ThemeData(
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
@ -63,6 +65,12 @@ class Comment extends Item {
|
||||
'score': score,
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final prettyString = const JsonEncoder.withIndent(' ').convert(this);
|
||||
return 'Comment $prettyString';
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
@ -96,6 +98,12 @@ class Story extends Item {
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final prettyString = const JsonEncoder.withIndent(' ').convert(this);
|
||||
return 'Story $prettyString';
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class User {
|
||||
User({
|
||||
required this.about,
|
||||
@ -26,4 +28,10 @@ class User {
|
||||
final int delay;
|
||||
final String id;
|
||||
final int karma;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final prettyString = const JsonEncoder.withIndent(' ').convert(this);
|
||||
return 'User $prettyString';
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({Key? key}) : super(key: key);
|
||||
@ -39,11 +39,6 @@ class HomeScreen extends StatefulWidget {
|
||||
class _HomeScreenState extends State<HomeScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final cacheService = locator.get<CacheService>();
|
||||
final refreshControllerTop = RefreshController();
|
||||
final refreshControllerNew = RefreshController();
|
||||
final refreshControllerAsk = RefreshController();
|
||||
final refreshControllerShow = RefreshController();
|
||||
final refreshControllerJobs = RefreshController();
|
||||
late final TabController tabController;
|
||||
int currentIndex = 0;
|
||||
|
||||
@ -78,7 +73,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
final homeScreen = BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.showComplexStoryTile != current.showComplexStoryTile,
|
||||
builder: (context, preferenceState) {
|
||||
@ -136,105 +131,113 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size(0, 48),
|
||||
preferredSize: const Size(0, 40),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).padding.top,
|
||||
height: MediaQuery.of(context).padding.top - 8,
|
||||
),
|
||||
TabBar(
|
||||
isScrollable: true,
|
||||
controller: tabController,
|
||||
indicatorColor: Colors.orange,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
'TOP',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: currentIndex == 0
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
primaryColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
controller: tabController,
|
||||
indicatorColor: Colors.orange,
|
||||
indicator: CircleTabIndicator(
|
||||
color: Colors.orange, radius: 2),
|
||||
indicatorPadding: const EdgeInsets.only(bottom: 8),
|
||||
onTap: (_) {
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
'TOP',
|
||||
style: TextStyle(
|
||||
fontSize: currentIndex == 0 ? 14 : 10,
|
||||
color: currentIndex == 0
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'NEW',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: currentIndex == 1
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
Tab(
|
||||
child: Text(
|
||||
'NEW',
|
||||
style: TextStyle(
|
||||
fontSize: currentIndex == 1 ? 14 : 10,
|
||||
color: currentIndex == 1
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'ASK',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: currentIndex == 2
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
Tab(
|
||||
child: Text(
|
||||
'ASK',
|
||||
style: TextStyle(
|
||||
fontSize: currentIndex == 2 ? 14 : 10,
|
||||
color: currentIndex == 2
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'SHOW',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: currentIndex == 3
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
Tab(
|
||||
child: Text(
|
||||
'SHOW',
|
||||
style: TextStyle(
|
||||
fontSize: currentIndex == 3 ? 14 : 10,
|
||||
color: currentIndex == 3
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'JOBS',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: currentIndex == 4
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
Tab(
|
||||
child: Text(
|
||||
'JOBS',
|
||||
style: TextStyle(
|
||||
fontSize: currentIndex == 4 ? 14 : 10,
|
||||
color: currentIndex == 4
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: DescribedFeatureOverlay(
|
||||
barrierDismissible: false,
|
||||
overflowMode: OverflowMode.extendBackground,
|
||||
targetColor: Theme.of(context).primaryColor,
|
||||
tapTarget: const Icon(
|
||||
Icons.person,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
featureId: Constants.featureLogIn,
|
||||
title: const Text(''),
|
||||
description: const Text(
|
||||
'Log in using your Hacker News account '
|
||||
'to check out stories and comments you have '
|
||||
'posted in the past, and get in-app '
|
||||
'notification when there is new reply to '
|
||||
'your comments or stories.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
child: BlocBuilder<NotificationCubit,
|
||||
NotificationState>(
|
||||
builder: (context, state) {
|
||||
if (state.unreadCommentsIds.isEmpty) {
|
||||
return Icon(
|
||||
Icons.person,
|
||||
size: 16,
|
||||
color: currentIndex == 5
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
);
|
||||
} else {
|
||||
Tab(
|
||||
child: DescribedFeatureOverlay(
|
||||
barrierDismissible: false,
|
||||
overflowMode: OverflowMode.extendBackground,
|
||||
targetColor: Theme.of(context).primaryColor,
|
||||
tapTarget: const Icon(
|
||||
Icons.person,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
featureId: Constants.featureLogIn,
|
||||
title: const Text(''),
|
||||
description: const Text(
|
||||
'Log in using your Hacker News account '
|
||||
'to check out stories and comments you have '
|
||||
'posted in the past, and get in-app '
|
||||
'notification when there is new reply to '
|
||||
'your comments or stories.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
child: BlocBuilder<NotificationCubit,
|
||||
NotificationState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.unreadCommentsIds.length !=
|
||||
current.unreadCommentsIds.length,
|
||||
builder: (context, state) {
|
||||
return Badge(
|
||||
showBadge:
|
||||
state.unreadCommentsIds.isNotEmpty,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
badgeContent: Container(
|
||||
height: 3,
|
||||
@ -245,18 +248,18 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 16,
|
||||
size: currentIndex == 5 ? 16 : 12,
|
||||
color: currentIndex == 5
|
||||
? Colors.orange
|
||||
: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -266,34 +269,34 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
controller: tabController,
|
||||
children: [
|
||||
StoriesListView(
|
||||
key: const ValueKey(StoryType.top),
|
||||
storyType: StoryType.top,
|
||||
header: pinnedStories,
|
||||
onStoryTapped: onStoryTapped,
|
||||
refreshController: refreshControllerTop,
|
||||
),
|
||||
StoriesListView(
|
||||
key: const ValueKey(StoryType.latest),
|
||||
storyType: StoryType.latest,
|
||||
header: pinnedStories,
|
||||
onStoryTapped: onStoryTapped,
|
||||
refreshController: refreshControllerNew,
|
||||
),
|
||||
StoriesListView(
|
||||
key: const ValueKey(StoryType.ask),
|
||||
storyType: StoryType.ask,
|
||||
header: pinnedStories,
|
||||
onStoryTapped: onStoryTapped,
|
||||
refreshController: refreshControllerAsk,
|
||||
),
|
||||
StoriesListView(
|
||||
key: const ValueKey(StoryType.show),
|
||||
storyType: StoryType.show,
|
||||
header: pinnedStories,
|
||||
onStoryTapped: onStoryTapped,
|
||||
refreshController: refreshControllerShow,
|
||||
),
|
||||
StoriesListView(
|
||||
key: const ValueKey(StoryType.jobs),
|
||||
storyType: StoryType.jobs,
|
||||
header: pinnedStories,
|
||||
onStoryTapped: onStoryTapped,
|
||||
refreshController: refreshControllerJobs,
|
||||
),
|
||||
const ProfileScreen(),
|
||||
],
|
||||
@ -304,6 +307,61 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return ScreenTypeLayout.builder(
|
||||
mobile: (context) {
|
||||
context.read<SplitViewCubit>().disableSplitView();
|
||||
return homeScreen;
|
||||
},
|
||||
tablet: (context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizeInfo) {
|
||||
context.read<SplitViewCubit>().enableSplitView();
|
||||
var homeScreenWidth = 428.0;
|
||||
|
||||
if (sizeInfo.screenSize.width < homeScreenWidth * 2) {
|
||||
homeScreenWidth = 345.0;
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: homeScreenWidth,
|
||||
child: homeScreen,
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: homeScreenWidth,
|
||||
child: BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.storyScreenArgs != current.storyScreenArgs,
|
||||
builder: (context, state) {
|
||||
if (state.storyScreenArgs != null) {
|
||||
return StoryScreen.build(state.storyScreenArgs!);
|
||||
}
|
||||
|
||||
return Material(
|
||||
child: Container(
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: const Center(
|
||||
child: Text('Tap on story tile to view comments.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void onStoryTapped(Story story) {
|
||||
@ -311,14 +369,22 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
final useReader = context.read<PreferenceCubit>().state.useReader;
|
||||
final offlineReading = context.read<StoriesBloc>().state.offlineReading;
|
||||
final firstTimeReading = cacheService.isFirstTimeReading(story.id);
|
||||
final splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||
|
||||
// If a story is a job story and it has a link to the job posting,
|
||||
// it would be better to just navigate to the web page.
|
||||
final isJobWithLink = story.type == 'job' && story.url.isNotEmpty;
|
||||
|
||||
if (!isJobWithLink) {
|
||||
HackiApp.navigatorKey.currentState!.pushNamed(StoryScreen.routeName,
|
||||
arguments: StoryScreenArgs(story: story));
|
||||
final args = StoryScreenArgs(story: story);
|
||||
if (splitViewEnabled) {
|
||||
context.read<SplitViewCubit>().updateStoryScreenArgs(args);
|
||||
} else {
|
||||
HackiApp.navigatorKey.currentState?.pushNamed(
|
||||
StoryScreen.routeName,
|
||||
arguments: args,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!offlineReading &&
|
||||
|
@ -9,6 +9,7 @@ import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
@ -121,9 +122,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
onTap: (item) {
|
||||
if (item is Story) {
|
||||
HackiApp.navigatorKey.currentState!.pushNamed(
|
||||
StoryScreen.routeName,
|
||||
arguments: StoryScreenArgs(story: item));
|
||||
goToStoryScreen(
|
||||
args: StoryScreenArgs(story: item));
|
||||
} else if (item is Comment) {
|
||||
onCommentTapped(item);
|
||||
}
|
||||
@ -167,11 +167,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
onLoadMore: () {
|
||||
context.read<FavCubit>().loadMore();
|
||||
},
|
||||
onTap: (story) {
|
||||
HackiApp.navigatorKey.currentState!.pushNamed(
|
||||
StoryScreen.routeName,
|
||||
arguments: StoryScreenArgs(story: story));
|
||||
},
|
||||
onTap: (story) => goToStoryScreen(
|
||||
args: StoryScreenArgs(story: story)),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -188,27 +185,38 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
top: 50,
|
||||
child: Offstage(
|
||||
offstage: pageType != _PageType.notification,
|
||||
child: InboxView(
|
||||
refreshController: refreshControllerNotification,
|
||||
unreadCommentsIds:
|
||||
notificationState.unreadCommentsIds,
|
||||
comments: notificationState.comments,
|
||||
onCommentTapped: (cmt) {
|
||||
onCommentTapped(cmt, then: () {
|
||||
context.read<NotificationCubit>().markAsRead(cmt);
|
||||
});
|
||||
},
|
||||
onMarkAllAsReadTapped: () {
|
||||
context.read<NotificationCubit>().markAllAsRead();
|
||||
},
|
||||
onLoadMore: () {
|
||||
context.read<NotificationCubit>().loadMore();
|
||||
},
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
context.read<NotificationCubit>().refresh();
|
||||
},
|
||||
),
|
||||
child: notificationState.comments.isEmpty
|
||||
? const CenteredMessageView(
|
||||
content:
|
||||
'New replies to your comments or stories '
|
||||
'will show up here.',
|
||||
)
|
||||
: InboxView(
|
||||
refreshController:
|
||||
refreshControllerNotification,
|
||||
unreadCommentsIds:
|
||||
notificationState.unreadCommentsIds,
|
||||
comments: notificationState.comments,
|
||||
onCommentTapped: (cmt) {
|
||||
onCommentTapped(cmt, then: () {
|
||||
context
|
||||
.read<NotificationCubit>()
|
||||
.markAsRead(cmt);
|
||||
});
|
||||
},
|
||||
onMarkAllAsReadTapped: () {
|
||||
context
|
||||
.read<NotificationCubit>()
|
||||
.markAllAsRead();
|
||||
},
|
||||
onLoadMore: () {
|
||||
context.read<NotificationCubit>().loadMore();
|
||||
},
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
context.read<NotificationCubit>().refresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
@ -360,7 +368,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Hacki',
|
||||
applicationVersion: 'v0.2.0',
|
||||
applicationVersion: 'v0.2.1',
|
||||
applicationIcon: Image.asset(
|
||||
Constants.hackiIconPath,
|
||||
height: 50,
|
||||
@ -447,18 +455,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
HackiApp.navigatorKey.currentState
|
||||
?.pushNamed(SubmitScreen.routeName);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text(
|
||||
'You need to log in first.',
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
action: SnackBarAction(
|
||||
label: 'Log in',
|
||||
textColor: Colors.black,
|
||||
onPressed: onLoginTapped,
|
||||
),
|
||||
),
|
||||
showSnackBar(
|
||||
content: 'You need to log in first.',
|
||||
label: 'Log in',
|
||||
action: onLoginTapped,
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -554,11 +554,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
|
||||
void showThemeSettingDialog({bool useTrueDarkMode = false}) {
|
||||
if (useTrueDarkMode) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Can't choose theme when using true dark mode."),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
showSnackBar(
|
||||
content: "Can't choose theme when using true dark mode.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -601,18 +598,14 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
.fetchParentStoryWithComments(id: comment.parent)
|
||||
.then((tuple) {
|
||||
if (tuple != null && mounted) {
|
||||
HackiApp.navigatorKey.currentState!
|
||||
.pushNamed(
|
||||
StoryScreen.routeName,
|
||||
arguments: StoryScreenArgs(
|
||||
story: tuple.item1,
|
||||
targetComments: tuple.item2.isEmpty
|
||||
? [comment]
|
||||
: [comment, ...tuple.item2],
|
||||
onlyShowTargetComment: true,
|
||||
),
|
||||
)
|
||||
.then((_) => then?.call());
|
||||
goToStoryScreen(
|
||||
args: StoryScreenArgs(
|
||||
story: tuple.item1,
|
||||
targetComments:
|
||||
tuple.item2.isEmpty ? [comment] : [comment, ...tuple.item2],
|
||||
onlyShowTargetComment: true,
|
||||
),
|
||||
)?.then((_) => then?.call());
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -630,12 +623,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
listener: (context, state) {
|
||||
if (state.isLoggedIn) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Logged in successfully!'),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
showSnackBar(content: 'Logged in successfully!');
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
@ -759,7 +747,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
child: ButtonBar(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.read<AuthBloc>().add(AuthInitialize());
|
||||
},
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(
|
||||
|
@ -104,6 +104,13 @@ class InboxView extends StatelessWidget {
|
||||
? textColor
|
||||
: Colors.grey,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color:
|
||||
unreadCommentsIds.contains(e.id)
|
||||
? Colors.orange
|
||||
: Colors.orange
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
maxLines: 4,
|
||||
onOpen: (link) =>
|
||||
LinkUtil.launchUrl(link.url),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
@ -22,6 +23,22 @@ class OfflineListTile extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
final downloading =
|
||||
state.downloadStatus == StoriesDownloadStatus.downloading;
|
||||
final downloaded =
|
||||
state.downloadStatus == StoriesDownloadStatus.finished;
|
||||
|
||||
final trailingWidget = () {
|
||||
if (downloading) {
|
||||
return const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CustomCircularProgressIndicator(),
|
||||
);
|
||||
} else if (downloaded) {
|
||||
return const Icon(Icons.check_circle);
|
||||
}
|
||||
return const Icon(Icons.download);
|
||||
}();
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
downloading ? 'Downloading All Stories...' : 'Download All Stories',
|
||||
@ -30,17 +47,15 @@ class OfflineListTile extends StatelessWidget {
|
||||
'download all latest stories that have at least one comment '
|
||||
"for offline reading. (web page won't be downloaded)",
|
||||
),
|
||||
trailing: downloading
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CustomCircularProgressIndicator(),
|
||||
)
|
||||
: const Icon(Icons.download),
|
||||
trailing: trailingWidget,
|
||||
isThreeLine: true,
|
||||
onTap: () {
|
||||
Wakelock.enable();
|
||||
context.read<StoriesBloc>().add(StoriesDownload());
|
||||
Connectivity().checkConnectivity().then((res) {
|
||||
if (res != ConnectivityResult.none) {
|
||||
Wakelock.enable();
|
||||
context.read<StoriesBloc>().add(StoriesDownload());
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
@ -31,6 +31,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -100,25 +101,24 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
child: ListView(
|
||||
children: [
|
||||
...state.results
|
||||
.map((e) => [
|
||||
FadeIn(
|
||||
child: StoryTile(
|
||||
showWebPreview:
|
||||
prefState.showComplexStoryTile,
|
||||
story: e,
|
||||
onTap: () {
|
||||
HackiApp.navigatorKey.currentState!
|
||||
.pushNamed(
|
||||
StoryScreen.routeName,
|
||||
arguments: StoryScreenArgs(
|
||||
story: e));
|
||||
}),
|
||||
),
|
||||
if (!prefState.showComplexStoryTile)
|
||||
const Divider(
|
||||
height: 0,
|
||||
.map(
|
||||
(e) => [
|
||||
FadeIn(
|
||||
child: StoryTile(
|
||||
showWebPreview:
|
||||
prefState.showComplexStoryTile,
|
||||
story: e,
|
||||
onTap: () => goToStoryScreen(
|
||||
args: StoryScreenArgs(story: e),
|
||||
),
|
||||
])
|
||||
),
|
||||
),
|
||||
if (!prefState.showComplexStoryTile)
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
],
|
||||
)
|
||||
.expand((e) => e)
|
||||
.toList(),
|
||||
const SizedBox(
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
@ -13,6 +14,7 @@ import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
@ -30,8 +32,8 @@ enum _MenuAction {
|
||||
cancel,
|
||||
}
|
||||
|
||||
class StoryScreenArgs {
|
||||
StoryScreenArgs({
|
||||
class StoryScreenArgs extends Equatable {
|
||||
const StoryScreenArgs({
|
||||
required this.story,
|
||||
this.onlyShowTargetComment = false,
|
||||
this.targetComments,
|
||||
@ -40,12 +42,22 @@ class StoryScreenArgs {
|
||||
final Story story;
|
||||
final bool onlyShowTargetComment;
|
||||
final List<Comment>? targetComments;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
story,
|
||||
onlyShowTargetComment,
|
||||
targetComments,
|
||||
];
|
||||
}
|
||||
|
||||
class StoryScreen extends StatefulWidget {
|
||||
const StoryScreen(
|
||||
{Key? key, required this.story, required this.parentComments})
|
||||
: super(key: key);
|
||||
const StoryScreen({
|
||||
Key? key,
|
||||
this.splitViewEnabled = false,
|
||||
required this.story,
|
||||
required this.parentComments,
|
||||
}) : super(key: key);
|
||||
|
||||
static const String routeName = '/story';
|
||||
|
||||
@ -78,6 +90,35 @@ class StoryScreen extends StatefulWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static Widget build(StoryScreenArgs args) {
|
||||
return MultiBlocProvider(
|
||||
key: ValueKey(args),
|
||||
providers: [
|
||||
BlocProvider<PostCubit>(
|
||||
create: (context) => PostCubit(),
|
||||
),
|
||||
BlocProvider<CommentsCubit>(
|
||||
create: (context) => CommentsCubit<Story>(
|
||||
offlineReading: context.read<StoriesBloc>().state.offlineReading,
|
||||
item: args.story,
|
||||
)..init(
|
||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||
targetComment: args.targetComments?.last,
|
||||
),
|
||||
),
|
||||
BlocProvider<EditCubit>(
|
||||
create: (context) => EditCubit(),
|
||||
),
|
||||
],
|
||||
child: StoryScreen(
|
||||
story: args.story,
|
||||
parentComments: args.targetComments ?? [],
|
||||
splitViewEnabled: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final bool splitViewEnabled;
|
||||
final Story story;
|
||||
final List<Comment> parentComments;
|
||||
|
||||
@ -166,16 +207,12 @@ class _StoryScreenState extends State<StoryScreen> {
|
||||
editCubit.onReplySubmittedSuccessfully();
|
||||
context.read<PostCubit>().reset();
|
||||
} else if (postState.status == PostStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
'Something went wrong...${(sadFaces..shuffle()).first}',
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
action: SnackBarAction(
|
||||
label: 'Okay',
|
||||
onPressed: () =>
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar()),
|
||||
));
|
||||
showSnackBar(
|
||||
content:
|
||||
'Something went wrong...${(sadFaces..shuffle()).first}',
|
||||
label: 'Okay',
|
||||
action: ScaffoldMessenger.of(context).hideCurrentSnackBar,
|
||||
);
|
||||
context.read<PostCubit>().reset();
|
||||
}
|
||||
},
|
||||
@ -190,6 +227,263 @@ class _StoryScreenState extends State<StoryScreen> {
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final mainView = SmartRefresher(
|
||||
scrollController: scrollController,
|
||||
enablePullUp: !state.onlyShowTargetComment,
|
||||
enablePullDown: !state.onlyShowTargetComment,
|
||||
header: WaterDropMaterialHeader(
|
||||
backgroundColor: Colors.orange,
|
||||
offset: topPadding,
|
||||
),
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
builder: (context, mode) {
|
||||
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: 55,
|
||||
child: Center(child: body),
|
||||
);
|
||||
},
|
||||
),
|
||||
controller: refreshController,
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
locator.get<CacheService>().resetComments();
|
||||
context.read<CommentsCubit>().refresh();
|
||||
},
|
||||
onLoading: () {},
|
||||
child: ListView(
|
||||
primary: false,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
),
|
||||
if (!widget.splitViewEnabled)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (widget.story !=
|
||||
context.read<EditCubit>().state.replyingTo) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
editCubit.onReplyTapped(widget.story);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (_) => onMorePressed(widget.story),
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6,
|
||||
right: 6,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.story.by,
|
||||
style: const TextStyle(
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
widget.story.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launchUrl(
|
||||
widget.story.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.useReader,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6,
|
||||
right: 6,
|
||||
bottom: 12,
|
||||
top: 12,
|
||||
),
|
||||
child: Text(
|
||||
widget.story.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: widget.story.url.isNotEmpty
|
||||
? Colors.orange
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.story.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
),
|
||||
child: SelectableHtml(
|
||||
data: widget.story.text,
|
||||
style: {
|
||||
'body': Style(
|
||||
fontSize: FontSize(
|
||||
MediaQuery.of(context).textScaleFactor *
|
||||
15,
|
||||
),
|
||||
),
|
||||
'a': Style(
|
||||
fontSize: FontSize(
|
||||
MediaQuery.of(context).textScaleFactor *
|
||||
15,
|
||||
),
|
||||
color: Colors.orange,
|
||||
),
|
||||
},
|
||||
onLinkTap: (link, _, __, ___) =>
|
||||
LinkUtil.launchUrl(link ?? ''),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.story.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...[
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
context.read<CommentsCubit>().loadAll(widget.story),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
],
|
||||
if (state.comments.isEmpty &&
|
||||
state.status == CommentsStatus.loaded) ...[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
...state.comments.map(
|
||||
(e) => FadeIn(
|
||||
child: CommentTile(
|
||||
comment: e,
|
||||
onlyShowTargetComment: state.onlyShowTargetComment,
|
||||
targetComments: widget.parentComments.sublist(
|
||||
0, max(widget.parentComments.length - 1, 0)),
|
||||
myUsername:
|
||||
authState.isLoggedIn ? authState.username : null,
|
||||
onReplyTapped: (cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmt !=
|
||||
context.read<EditCubit>().state.replyingTo) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
|
||||
editCubit.onReplyTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onEditTapped: (cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
commentEditingController.clear();
|
||||
editCubit.onEditTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onMoreTapped: onMorePressed,
|
||||
onStoryLinkTapped: (link) {
|
||||
final regex = RegExp(r'\d+$');
|
||||
final match = regex.stringMatch(link) ?? '';
|
||||
final id = int.tryParse(match);
|
||||
if (id != null) {
|
||||
throttle.run(() {
|
||||
locator
|
||||
.get<StoriesRepository>()
|
||||
.fetchParentStory(id: id)
|
||||
.then((story) {
|
||||
if (mounted) {
|
||||
if (story != null) {
|
||||
HackiApp.navigatorKey.currentState!
|
||||
.pushNamed(
|
||||
StoryScreen.routeName,
|
||||
arguments:
|
||||
StoryScreenArgs(story: story),
|
||||
);
|
||||
} else {}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
LinkUtil.launchUrl(link);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return BlocListener<EditCubit, EditState>(
|
||||
listenWhen: (previous, current) {
|
||||
return previous.replyingTo != current.replyingTo ||
|
||||
@ -211,297 +505,67 @@ class _StoryScreenState extends State<StoryScreen> {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor:
|
||||
Theme.of(context).canvasColor.withOpacity(0.6),
|
||||
elevation: 0,
|
||||
actions: [
|
||||
ScrollUpIconButton(
|
||||
scrollController: scrollController,
|
||||
),
|
||||
PinIconButton(story: widget.story),
|
||||
FavIconButton(storyId: widget.story.id),
|
||||
LinkIconButton(storyId: widget.story.id),
|
||||
],
|
||||
),
|
||||
body: SmartRefresher(
|
||||
scrollController: scrollController,
|
||||
enablePullUp: !state.onlyShowTargetComment,
|
||||
enablePullDown: !state.onlyShowTargetComment,
|
||||
header: WaterDropMaterialHeader(
|
||||
backgroundColor: Colors.orange,
|
||||
offset: topPadding,
|
||||
),
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
builder: (context, mode) {
|
||||
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: 55,
|
||||
child: Center(child: body),
|
||||
);
|
||||
},
|
||||
),
|
||||
controller: refreshController,
|
||||
onRefresh: () {
|
||||
HapticFeedback.lightImpact();
|
||||
locator.get<CacheService>().resetComments();
|
||||
context.read<CommentsCubit>().refresh();
|
||||
},
|
||||
onLoading: () {},
|
||||
child: ListView(
|
||||
primary: false,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: topPadding,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 6),
|
||||
child: OfflineBanner(),
|
||||
),
|
||||
Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (widget.story !=
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.state
|
||||
.replyingTo) {
|
||||
commentEditingController.clear();
|
||||
}
|
||||
editCubit.onReplyTapped(widget.story);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
icon: Icons.message,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (_) => onMorePressed(widget.story),
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6,
|
||||
right: 6,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.story.by,
|
||||
style: const TextStyle(
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
widget.story.postedDate,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => LinkUtil.launchUrl(
|
||||
widget.story.url,
|
||||
useReader: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.useReader,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 6,
|
||||
right: 6,
|
||||
bottom: 12,
|
||||
top: 12,
|
||||
),
|
||||
child: Text(
|
||||
widget.story.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.story.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
),
|
||||
child: SelectableHtml(
|
||||
data: widget.story.text,
|
||||
onLinkTap: (link, _, __, ___) =>
|
||||
LinkUtil.launchUrl(link ?? ''),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.story.text.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
if (state.onlyShowTargetComment) ...[
|
||||
TextButton(
|
||||
onPressed: () => context
|
||||
.read<CommentsCubit>()
|
||||
.loadAll(widget.story),
|
||||
child: const Text('View all comments'),
|
||||
),
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
],
|
||||
if (state.comments.isEmpty &&
|
||||
state.status == CommentsStatus.loaded) ...[
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nothing yet',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
child: widget.splitViewEnabled
|
||||
? Material(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: mainView,
|
||||
),
|
||||
),
|
||||
],
|
||||
...state.comments.map(
|
||||
(e) => FadeIn(
|
||||
child: CommentTile(
|
||||
comment: e,
|
||||
onlyShowTargetComment:
|
||||
state.onlyShowTargetComment,
|
||||
targetComments: widget.parentComments.sublist(
|
||||
0, max(widget.parentComments.length - 1, 0)),
|
||||
myUsername: authState.isLoggedIn
|
||||
? authState.username
|
||||
: null,
|
||||
onReplyTapped: (cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmt !=
|
||||
context
|
||||
.read<EditCubit>()
|
||||
.state
|
||||
.replyingTo) {
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: CustomAppBar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.canvasColor
|
||||
.withOpacity(0.6),
|
||||
story: widget.story,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ReplyBox(
|
||||
splitViewEnabled: true,
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
editCubit.onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
}
|
||||
|
||||
editCubit.onReplyTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onEditTapped: (cmt) {
|
||||
HapticFeedback.lightImpact();
|
||||
if (cmt.deleted || cmt.dead) {
|
||||
return;
|
||||
}
|
||||
commentEditingController.clear();
|
||||
editCubit.onEditTapped(cmt);
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onMoreTapped: onMorePressed,
|
||||
onStoryLinkTapped: (link) {
|
||||
final regex = RegExp(r'\d+$');
|
||||
final match = regex.stringMatch(link) ?? '';
|
||||
final id = int.tryParse(match);
|
||||
if (id != null) {
|
||||
throttle.run(() {
|
||||
locator
|
||||
.get<StoriesRepository>()
|
||||
.fetchParentStory(id: id)
|
||||
.then((story) {
|
||||
if (mounted) {
|
||||
if (story != null) {
|
||||
HackiApp.navigatorKey.currentState!
|
||||
.pushNamed(
|
||||
StoryScreen.routeName,
|
||||
arguments:
|
||||
StoryScreenArgs(story: story),
|
||||
);
|
||||
} else {}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
LinkUtil.launchUrl(link);
|
||||
}
|
||||
},
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged: editCubit.onTextChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 240,
|
||||
)
|
||||
: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: CustomAppBar(
|
||||
backgroundColor:
|
||||
Theme.of(context).canvasColor.withOpacity(0.6),
|
||||
story: widget.story,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomSheet: Offstage(
|
||||
offstage: !editCubit.state.showReplyBox,
|
||||
child: BlocBuilder<PostCubit, PostState>(
|
||||
builder: (context, postState) {
|
||||
return BlocBuilder<EditCubit, EditState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.itemBeingEdited !=
|
||||
current.itemBeingEdited ||
|
||||
previous.replyingTo != current.replyingTo,
|
||||
builder: (context, editState) {
|
||||
return ReplyBox(
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
editing: editState.itemBeingEdited,
|
||||
replyingTo: editState.replyingTo,
|
||||
isLoading: postState.status == PostStatus.loading,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
editCubit.onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onChanged: editCubit.onTextChanged,
|
||||
);
|
||||
body: mainView,
|
||||
bottomSheet: ReplyBox(
|
||||
focusNode: focusNode,
|
||||
textEditingController: commentEditingController,
|
||||
onSendTapped: onSendTapped,
|
||||
onCloseTapped: () {
|
||||
editCubit.onReplyBoxClosed();
|
||||
commentEditingController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: editCubit.onTextChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -546,7 +610,8 @@ class _StoryScreenState extends State<StoryScreen> {
|
||||
} else if (voteState.status == VoteStatus.failureNotLoggedIn) {
|
||||
showSnackBar(
|
||||
content: 'Not logged in, no voting! (;`O´)o',
|
||||
withLoginAction: true,
|
||||
action: onLoginTapped,
|
||||
label: 'Log in',
|
||||
);
|
||||
} else if (voteState.status == VoteStatus.failureBeHumble) {
|
||||
showSnackBar(content: 'No voting on your own post! (;`O´)o');
|
||||
@ -649,6 +714,7 @@ class _StoryScreenState extends State<StoryScreen> {
|
||||
onBlockTapped(item, isBlocked);
|
||||
break;
|
||||
case _MenuAction.cancel:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -946,7 +1012,10 @@ class _StoryScreenState extends State<StoryScreen> {
|
||||
child: ButtonBar(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.read<AuthBloc>().add(AuthInitialize());
|
||||
},
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(
|
||||
@ -993,22 +1062,4 @@ class _StoryScreenState extends State<StoryScreen> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showSnackBar({required String content, bool withLoginAction = false}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
content,
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
action: withLoginAction
|
||||
? SnackBarAction(
|
||||
label: 'Log in',
|
||||
textColor: Colors.black,
|
||||
onPressed: onLoginTapped,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
27
lib/screens/story/widgets/custom_app_bar.dart
Normal file
27
lib/screens/story/widgets/custom_app_bar.dart
Normal file
@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/story/widgets/fav_icon_button.dart';
|
||||
import 'package:hacki/screens/story/widgets/link_icon_button.dart';
|
||||
import 'package:hacki/screens/story/widgets/pin_icon_button.dart';
|
||||
import 'package:hacki/screens/story/widgets/scroll_up_icon_button.dart';
|
||||
|
||||
class CustomAppBar extends AppBar {
|
||||
CustomAppBar({
|
||||
Key? key,
|
||||
required ScrollController scrollController,
|
||||
required Story story,
|
||||
required Color backgroundColor,
|
||||
}) : super(
|
||||
key: key,
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
ScrollUpIconButton(
|
||||
scrollController: scrollController,
|
||||
),
|
||||
PinIconButton(story: story),
|
||||
FavIconButton(storyId: story.id),
|
||||
LinkIconButton(storyId: story.id),
|
||||
],
|
||||
);
|
||||
}
|
@ -1,31 +1,29 @@
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/utils/link_util.dart';
|
||||
|
||||
class ReplyBox extends StatefulWidget {
|
||||
const ReplyBox({
|
||||
Key? key,
|
||||
this.splitViewEnabled = false,
|
||||
required this.focusNode,
|
||||
required this.textEditingController,
|
||||
required this.replyingTo,
|
||||
required this.editing,
|
||||
required this.onSendTapped,
|
||||
required this.onCloseTapped,
|
||||
required this.onChanged,
|
||||
required this.isLoading,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool splitViewEnabled;
|
||||
final FocusNode focusNode;
|
||||
final TextEditingController textEditingController;
|
||||
final Item? replyingTo;
|
||||
final Item? editing;
|
||||
final VoidCallback onSendTapped;
|
||||
final VoidCallback onCloseTapped;
|
||||
final ValueChanged<String> onChanged;
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
_ReplyBoxState createState() => _ReplyBoxState();
|
||||
@ -34,219 +32,266 @@ class ReplyBox extends StatefulWidget {
|
||||
class _ReplyBoxState extends State<ReplyBox> {
|
||||
bool expanded = false;
|
||||
double? expandedHeight;
|
||||
double? topPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
expandedHeight ??= MediaQuery.of(context).size.height;
|
||||
topPadding ??= MediaQuery.of(context).padding.top + kToolbarHeight;
|
||||
return AnimatedContainer(
|
||||
height: expanded ? expandedHeight : 100,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: expanded ? Colors.transparent : Colors.black54,
|
||||
offset: const Offset(0, 20), //(x,y)
|
||||
blurRadius: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
height: expanded ? topPadding : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
widget.replyingTo == null
|
||||
? 'Editing'
|
||||
: 'Replying '
|
||||
'${widget.replyingTo?.by}',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (!widget.isLoading) ...[
|
||||
...[
|
||||
if (widget.replyingTo != null)
|
||||
AnimatedOpacity(
|
||||
opacity: expanded ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: IconButton(
|
||||
key: const Key('quote'),
|
||||
icon: const Icon(
|
||||
FeatherIcons.code,
|
||||
color: Colors.orange,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: expanded ? showTextPopup : null,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: const Key('expand'),
|
||||
icon: Icon(
|
||||
expanded
|
||||
? FeatherIcons.minimize2
|
||||
: FeatherIcons.maximize2,
|
||||
color: Colors.orange,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
expanded = !expanded;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
key: const Key('close'),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.onCloseTapped();
|
||||
expanded = false;
|
||||
},
|
||||
),
|
||||
],
|
||||
if (widget.isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.orange,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
key: const Key('send'),
|
||||
icon: const Icon(
|
||||
Icons.send,
|
||||
color: Colors.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.onSendTapped();
|
||||
expanded = false;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextField(
|
||||
focusNode: widget.focusNode,
|
||||
controller: widget.textEditingController,
|
||||
maxLines: 100,
|
||||
decoration: const InputDecoration(
|
||||
alignLabelWithHint: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: '...',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return BlocBuilder<EditCubit, EditState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.showReplyBox != current.showReplyBox,
|
||||
builder: (context, editState) {
|
||||
return Offstage(
|
||||
offstage: !editState.showReplyBox,
|
||||
child: BlocBuilder<PostCubit, PostState>(
|
||||
builder: (context, postState) {
|
||||
return BlocBuilder<EditCubit, EditState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.itemBeingEdited != current.itemBeingEdited ||
|
||||
previous.replyingTo != current.replyingTo,
|
||||
builder: (context, editState) {
|
||||
final replyingTo = editState.replyingTo;
|
||||
final isLoading = postState.status == PostStatus.loading;
|
||||
|
||||
void showTextPopup() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 64,
|
||||
bottom: 64,
|
||||
),
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 6,
|
||||
top: 6,
|
||||
bottom: 12,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.replyingTo?.by ?? '',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: expanded
|
||||
? 0
|
||||
: widget.splitViewEnabled
|
||||
? MediaQuery.of(context).viewInsets.bottom
|
||||
: 0,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
height: expanded ? expandedHeight : 100,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
if (!context.read<SplitViewCubit>().state.enabled)
|
||||
BoxShadow(
|
||||
color: expanded
|
||||
? Colors.transparent
|
||||
: Colors.black26,
|
||||
blurRadius: 40,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
child: const Text('Copy All'),
|
||||
onPressed: () => FlutterClipboard.copy(
|
||||
widget.replyingTo?.text ?? '',
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.orange,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 6,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
isAlwaysShown: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableLinkify(
|
||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||
text: widget.replyingTo?.text ?? '',
|
||||
onOpen: (link) => LinkUtil.launchUrl(link.url),
|
||||
child: Material(
|
||||
child: Column(
|
||||
children: [
|
||||
if (context.read<SplitViewCubit>().state.enabled)
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
AnimatedContainer(
|
||||
height: expanded ? 36 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
replyingTo == null
|
||||
? 'Editing'
|
||||
: 'Replying '
|
||||
'${replyingTo.by}',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (!isLoading) ...[
|
||||
...[
|
||||
if (replyingTo != null)
|
||||
AnimatedOpacity(
|
||||
opacity: expanded ? 1 : 0,
|
||||
duration:
|
||||
const Duration(milliseconds: 300),
|
||||
child: IconButton(
|
||||
key: const Key('quote'),
|
||||
icon: const Icon(
|
||||
FeatherIcons.code,
|
||||
color: Colors.orange,
|
||||
size: 18,
|
||||
),
|
||||
onPressed:
|
||||
expanded ? showTextPopup : null,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: const Key('expand'),
|
||||
icon: Icon(
|
||||
expanded
|
||||
? FeatherIcons.minimize2
|
||||
: FeatherIcons.maximize2,
|
||||
color: Colors.orange,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
expanded = !expanded;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
key: const Key('close'),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.onCloseTapped();
|
||||
expanded = false;
|
||||
},
|
||||
),
|
||||
],
|
||||
if (isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.orange,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
key: const Key('send'),
|
||||
icon: const Icon(
|
||||
Icons.send,
|
||||
color: Colors.orange,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.onSendTapped();
|
||||
expanded = false;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextField(
|
||||
focusNode: widget.focusNode,
|
||||
controller: widget.textEditingController,
|
||||
maxLines: 100,
|
||||
decoration: const InputDecoration(
|
||||
alignLabelWithHint: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: '...',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showTextPopup() {
|
||||
final replyingTo = context.read<EditCubit>().state.replyingTo;
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 24,
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 500,
|
||||
maxHeight: 500,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
top: 6,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
replyingTo?.by ?? '',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
child: const Text('Copy All'),
|
||||
onPressed: () => FlutterClipboard.copy(
|
||||
replyingTo?.text ?? '',
|
||||
).then((_) => HapticFeedback.selectionClick()),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.orange,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
isAlwaysShown: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 6,
|
||||
top: 6,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableHtml(
|
||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||
data: replyingTo?.text ?? '',
|
||||
style: {
|
||||
'a': Style(
|
||||
color: Colors.orange,
|
||||
),
|
||||
},
|
||||
onLinkTap: (link, _, __, ___) =>
|
||||
LinkUtil.launchUrl(link!),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
export 'custom_app_bar.dart';
|
||||
export 'fav_icon_button.dart';
|
||||
export 'link_icon_button.dart';
|
||||
export 'pin_icon_button.dart';
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
|
||||
class SubmitScreen extends StatefulWidget {
|
||||
const SubmitScreen({Key? key}) : super(key: key);
|
||||
@ -199,13 +200,4 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
||||
(textEditingController.text.isNotEmpty ||
|
||||
urlEditingController.text.isNotEmpty);
|
||||
}
|
||||
|
||||
void showSnackBar({required String content}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
content,
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
28
lib/screens/widgets/circle_tab_indicator.dart
Normal file
28
lib/screens/widgets/circle_tab_indicator.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CircleTabIndicator extends Decoration {
|
||||
CircleTabIndicator({required Color color, required double radius})
|
||||
: _painter = _CirclePainter(color, radius);
|
||||
|
||||
final BoxPainter _painter;
|
||||
|
||||
@override
|
||||
BoxPainter createBoxPainter([VoidCallback? onChanged]) => _painter;
|
||||
}
|
||||
|
||||
class _CirclePainter extends BoxPainter {
|
||||
_CirclePainter(Color color, this.radius)
|
||||
: _paint = Paint()
|
||||
..color = color
|
||||
..isAntiAlias = true;
|
||||
|
||||
final Paint _paint;
|
||||
final double radius;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
|
||||
final circleOffset =
|
||||
offset + Offset(cfg.size!.width / 2, cfg.size!.height - radius);
|
||||
canvas.drawCircle(circleOffset, radius, _paint);
|
||||
}
|
||||
}
|
@ -189,6 +189,17 @@ class CommentTile extends StatelessWidget {
|
||||
child: SelectableLinkify(
|
||||
key: ObjectKey(comment),
|
||||
text: comment.text,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
15,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
15,
|
||||
color: Colors.orange,
|
||||
),
|
||||
onOpen: (link) {
|
||||
if (link.url.contains(
|
||||
'news.ycombinator.com/item')) {
|
||||
|
@ -133,6 +133,9 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
||||
child: Linkify(
|
||||
text: e.text,
|
||||
maxLines: 4,
|
||||
linkStyle: const TextStyle(
|
||||
color: Colors.orange,
|
||||
),
|
||||
onOpen: (link) =>
|
||||
LinkUtil.launchUrl(link.url),
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
@ -184,7 +185,12 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _height = (MediaQuery.of(context).size.height) * 0.15;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final showSmallerPreviewPic =
|
||||
Platform.isIOS && screenWidth > 428.0 && screenWidth < 850;
|
||||
final _height = showSmallerPreviewPic
|
||||
? 100.0
|
||||
: (MediaQuery.of(context).size.height * 0.14).clamp(118.0, 140.0);
|
||||
final loadingWidget = widget.placeholderWidget ??
|
||||
Container(
|
||||
height: _height,
|
||||
|
@ -83,22 +83,23 @@ class LinkView extends StatelessWidget {
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (showMultiMedia)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 5,
|
||||
top: 5,
|
||||
bottom: 5,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 5,
|
||||
top: 5,
|
||||
bottom: 5,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: layoutHeight,
|
||||
width: layoutHeight,
|
||||
child: (imageUri?.isEmpty ?? true) && imagePath != null
|
||||
? Image.asset(
|
||||
imagePath!,
|
||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.cover,
|
||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: imageUri!,
|
||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.cover,
|
||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||
memCacheHeight: layoutHeight.toInt() * 4,
|
||||
errorWidget: (context, _, dynamic __) {
|
||||
return Image.asset(
|
||||
|
@ -7,22 +7,37 @@ import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
class StoriesListView extends StatelessWidget {
|
||||
class StoriesListView extends StatefulWidget {
|
||||
const StoriesListView({
|
||||
Key? key,
|
||||
required this.storyType,
|
||||
required this.header,
|
||||
required this.onStoryTapped,
|
||||
required this.refreshController,
|
||||
}) : super(key: key);
|
||||
|
||||
final StoryType storyType;
|
||||
final Widget header;
|
||||
final ValueChanged<Story> onStoryTapped;
|
||||
final RefreshController refreshController;
|
||||
|
||||
@override
|
||||
State<StoriesListView> createState() => _StoriesListViewState();
|
||||
}
|
||||
|
||||
class _StoriesListViewState extends State<StoriesListView> {
|
||||
final refreshController = RefreshController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
refreshController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final storyType = widget.storyType;
|
||||
final header = widget.header;
|
||||
final onStoryTapped = widget.onStoryTapped;
|
||||
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.showComplexStoryTile != current.showComplexStoryTile,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
@ -26,7 +28,12 @@ class StoryTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (showWebPreview) {
|
||||
final height = (MediaQuery.of(context).size.height) * 0.15;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final showSmallerPreviewPic =
|
||||
Platform.isIOS && screenWidth > 428.0 && screenWidth < 850;
|
||||
final height = showSmallerPreviewPic
|
||||
? 100.0
|
||||
: (MediaQuery.of(context).size.height * 0.14).clamp(118.0, 140.0);
|
||||
|
||||
if (story.url.isNotEmpty) {
|
||||
return TapDownWrapper(
|
||||
@ -47,19 +54,16 @@ class StoryTile extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
top: 5,
|
||||
),
|
||||
child: Container(
|
||||
height: height,
|
||||
width: height,
|
||||
color: Colors.white,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
top: 5,
|
||||
),
|
||||
child: Container(
|
||||
height: height,
|
||||
width: height,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@ -119,7 +123,7 @@ class StoryTile extends StatelessWidget {
|
||||
backgroundColor: Colors.transparent,
|
||||
borderRadius: 0,
|
||||
removeElevation: true,
|
||||
bodyMaxLines: 4,
|
||||
bodyMaxLines: height == 100 ? 3 : 4,
|
||||
errorTitle: story.title,
|
||||
titleStyle: TextStyle(
|
||||
color: wasRead
|
||||
@ -149,7 +153,7 @@ class StoryTile extends StatelessWidget {
|
||||
onTap: (_) {},
|
||||
url: '',
|
||||
imagePath: Constants.hackerNewsLogoPath,
|
||||
bodyMaxLines: 4,
|
||||
bodyMaxLines: height == 100 ? 3 : 4,
|
||||
titleTextStyle: TextStyle(
|
||||
color: wasRead
|
||||
? Colors.grey[500]
|
||||
|
@ -1,3 +1,4 @@
|
||||
export 'circle_tab_indicator.dart';
|
||||
export 'comment_tile.dart';
|
||||
export 'custom_circular_progress_indicator.dart';
|
||||
export 'items_list_view.dart';
|
||||
|
@ -1,6 +1,6 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 0.2.0+31
|
||||
version: 0.2.1+32
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@ -13,6 +13,7 @@ dependencies:
|
||||
cached_network_image: ^3.2.0
|
||||
clipboard: ^0.1.3
|
||||
collection:
|
||||
connectivity_plus: ^2.2.1
|
||||
dio: ^4.0.4
|
||||
equatable: 2.0.3
|
||||
fast_gbk: ^1.0.0
|
||||
@ -38,6 +39,7 @@ dependencies:
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.8
|
||||
pull_to_refresh: ^2.0.0
|
||||
responsive_builder: ^0.4.2
|
||||
sembast: ^3.1.1+1
|
||||
shared_preferences: ^2.0.11
|
||||
shimmer: ^2.0.0
|
||||
|
Reference in New Issue
Block a user