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
|
/android/app/release
|
||||||
|
|
||||||
/ios/Podfile.lock
|
/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.
|
A simple noiseless Hacker News reader made with Flutter that is just enough.
|
||||||
|
|
||||||

|
|
||||||
[](https://apps.apple.com/us/app/hacki/id1602043763)
|
[](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://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US)
|
||||||
[](https://badges.pufler.dev)
|
[](https://badges.pufler.dev)
|
||||||
@ -31,18 +30,24 @@ Features:
|
|||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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/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>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
D1B8F07E58D19B7A04062545 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A738A80ED7D259B7B74BB44E /* Pods_Runner.framework */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
@ -47,6 +48,8 @@
|
|||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
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>"; };
|
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; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -55,6 +58,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D1B8F07E58D19B7A04062545 /* Pods_Runner.framework in Frameworks */,
|
D1B8F07E58D19B7A04062545 /* Pods_Runner.framework in Frameworks */,
|
||||||
|
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -94,6 +98,7 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */,
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
@ -109,6 +114,7 @@
|
|||||||
B3F4F49CF582C662A01499C0 /* Frameworks */ = {
|
B3F4F49CF582C662A01499C0 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
|
||||||
A738A80ED7D259B7B74BB44E /* Pods_Runner.framework */,
|
A738A80ED7D259B7B74BB44E /* Pods_Runner.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
@ -354,6 +360,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 7;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
@ -365,13 +372,13 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.0;
|
MARKETING_VERSION = 0.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
@ -489,6 +496,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 7;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
@ -500,14 +508,14 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.0;
|
MARKETING_VERSION = 0.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@ -518,6 +526,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 7;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
@ -529,13 +538,13 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.2.0;
|
MARKETING_VERSION = 0.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Release;
|
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 {
|
} else {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
|
status: AuthStatus.loaded,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
class Constants {
|
abstract class Constants {
|
||||||
static const String endUserAgreementLink =
|
static const String endUserAgreementLink =
|
||||||
'https://www.termsfeed.com/live/c1417f5c-a48b-4bd7-93b2-9cd4577bfc45';
|
'https://www.termsfeed.com/live/c1417f5c-a48b-4bd7-93b2-9cd4577bfc45';
|
||||||
static const String hackerNewsLogoLink =
|
static const String hackerNewsLogoLink =
|
||||||
|
@ -22,8 +22,10 @@ class CustomRouter {
|
|||||||
/// Nested routing for bottom navigation bar.
|
/// Nested routing for bottom navigation bar.
|
||||||
static Route onGenerateNestedRoute(RouteSettings settings) {
|
static Route onGenerateNestedRoute(RouteSettings settings) {
|
||||||
switch (settings.name) {
|
switch (settings.name) {
|
||||||
case HomeScreen.routeName:
|
case StoryScreen.routeName:
|
||||||
return HomeScreen.route();
|
return StoryScreen.route(settings.arguments! as StoryScreenArgs);
|
||||||
|
case SubmitScreen.routeName:
|
||||||
|
return SubmitScreen.route();
|
||||||
default:
|
default:
|
||||||
return _errorRoute();
|
return _errorRoute();
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,13 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
|
|||||||
final CacheRepository _cacheRepository;
|
final CacheRepository _cacheRepository;
|
||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void emit(CommentsState state) {
|
||||||
|
if (!isClosed) {
|
||||||
|
super.emit(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> init({
|
Future<void> init({
|
||||||
bool onlyShowTargetComment = false,
|
bool onlyShowTargetComment = false,
|
||||||
Comment? targetComment,
|
Comment? targetComment,
|
||||||
@ -63,11 +70,10 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isClosed) {
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: CommentsStatus.loaded,
|
status: CommentsStatus.loaded,
|
||||||
));
|
));
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
final comment = state.item as Comment;
|
final comment = state.item as Comment;
|
||||||
|
|
||||||
@ -93,13 +99,11 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isClosed) {
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: CommentsStatus.loaded,
|
status: CommentsStatus.loaded,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
final offlineReading = await _cacheRepository.hasCachedStories;
|
final offlineReading = await _cacheRepository.hasCachedStories;
|
||||||
@ -160,7 +164,7 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onCommentFetched(Comment? comment) {
|
void _onCommentFetched(Comment? comment) {
|
||||||
if (comment != null && !isClosed) {
|
if (comment != null) {
|
||||||
_cacheService.cacheComment(comment);
|
_cacheService.cacheComment(comment);
|
||||||
emit(state.copyWith(comments: List.from(state.comments)..add(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 'post/post_cubit.dart';
|
||||||
export 'preference/preference_cubit.dart';
|
export 'preference/preference_cubit.dart';
|
||||||
export 'search/search_cubit.dart';
|
export 'search/search_cubit.dart';
|
||||||
|
export 'split_view/split_view_cubit.dart';
|
||||||
export 'submit/submit_cubit.dart';
|
export 'submit/submit_cubit.dart';
|
||||||
export 'vote/vote_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 'date_time_extension.dart';
|
||||||
|
export 'object_extension.dart';
|
||||||
|
export 'state_extension.dart';
|
||||||
export 'widget_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,
|
lazy: false,
|
||||||
create: (context) => CacheCubit(),
|
create: (context) => CacheCubit(),
|
||||||
),
|
),
|
||||||
|
BlocProvider<SplitViewCubit>(
|
||||||
|
lazy: false,
|
||||||
|
create: (context) => SplitViewCubit(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: AdaptiveTheme(
|
child: AdaptiveTheme(
|
||||||
light: ThemeData(
|
light: ThemeData(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item.dart';
|
||||||
|
|
||||||
@ -63,6 +65,12 @@ class Comment extends Item {
|
|||||||
'score': score,
|
'score': score,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final prettyString = const JsonEncoder.withIndent(' ').convert(this);
|
||||||
|
return 'Comment $prettyString';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
id,
|
id,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/item.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
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
id,
|
id,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
User({
|
User({
|
||||||
required this.about,
|
required this.about,
|
||||||
@ -26,4 +28,10 @@ class User {
|
|||||||
final int delay;
|
final int delay;
|
||||||
final String id;
|
final String id;
|
||||||
final int karma;
|
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/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
import 'package:hacki/utils/utils.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 {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({Key? key}) : super(key: key);
|
const HomeScreen({Key? key}) : super(key: key);
|
||||||
@ -39,11 +39,6 @@ class HomeScreen extends StatefulWidget {
|
|||||||
class _HomeScreenState extends State<HomeScreen>
|
class _HomeScreenState extends State<HomeScreen>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
final cacheService = locator.get<CacheService>();
|
final cacheService = locator.get<CacheService>();
|
||||||
final refreshControllerTop = RefreshController();
|
|
||||||
final refreshControllerNew = RefreshController();
|
|
||||||
final refreshControllerAsk = RefreshController();
|
|
||||||
final refreshControllerShow = RefreshController();
|
|
||||||
final refreshControllerJobs = RefreshController();
|
|
||||||
late final TabController tabController;
|
late final TabController tabController;
|
||||||
int currentIndex = 0;
|
int currentIndex = 0;
|
||||||
|
|
||||||
@ -78,7 +73,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
final homeScreen = BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) =>
|
||||||
previous.showComplexStoryTile != current.showComplexStoryTile,
|
previous.showComplexStoryTile != current.showComplexStoryTile,
|
||||||
builder: (context, preferenceState) {
|
builder: (context, preferenceState) {
|
||||||
@ -136,22 +131,34 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size(0, 48),
|
preferredSize: const Size(0, 40),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: MediaQuery.of(context).padding.top,
|
height: MediaQuery.of(context).padding.top - 8,
|
||||||
),
|
),
|
||||||
TabBar(
|
Theme(
|
||||||
|
data: ThemeData(
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
splashColor: Colors.transparent,
|
||||||
|
primaryColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
child: TabBar(
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
indicatorColor: Colors.orange,
|
indicatorColor: Colors.orange,
|
||||||
|
indicator: CircleTabIndicator(
|
||||||
|
color: Colors.orange, radius: 2),
|
||||||
|
indicatorPadding: const EdgeInsets.only(bottom: 8),
|
||||||
|
onTap: (_) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
},
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(
|
Tab(
|
||||||
child: Text(
|
child: Text(
|
||||||
'TOP',
|
'TOP',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: currentIndex == 0 ? 14 : 10,
|
||||||
color: currentIndex == 0
|
color: currentIndex == 0
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: Colors.grey,
|
: Colors.grey,
|
||||||
@ -162,7 +169,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: Text(
|
child: Text(
|
||||||
'NEW',
|
'NEW',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: currentIndex == 1 ? 14 : 10,
|
||||||
color: currentIndex == 1
|
color: currentIndex == 1
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: Colors.grey,
|
: Colors.grey,
|
||||||
@ -173,7 +180,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: Text(
|
child: Text(
|
||||||
'ASK',
|
'ASK',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: currentIndex == 2 ? 14 : 10,
|
||||||
color: currentIndex == 2
|
color: currentIndex == 2
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: Colors.grey,
|
: Colors.grey,
|
||||||
@ -184,7 +191,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: Text(
|
child: Text(
|
||||||
'SHOW',
|
'SHOW',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: currentIndex == 3 ? 14 : 10,
|
||||||
color: currentIndex == 3
|
color: currentIndex == 3
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: Colors.grey,
|
: Colors.grey,
|
||||||
@ -195,7 +202,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: Text(
|
child: Text(
|
||||||
'JOBS',
|
'JOBS',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: currentIndex == 4 ? 14 : 10,
|
||||||
color: currentIndex == 4
|
color: currentIndex == 4
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: Colors.grey,
|
: Colors.grey,
|
||||||
@ -224,17 +231,13 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
child: BlocBuilder<NotificationCubit,
|
child: BlocBuilder<NotificationCubit,
|
||||||
NotificationState>(
|
NotificationState>(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
previous.unreadCommentsIds.length !=
|
||||||
|
current.unreadCommentsIds.length,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.unreadCommentsIds.isEmpty) {
|
|
||||||
return Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 16,
|
|
||||||
color: currentIndex == 5
|
|
||||||
? Colors.orange
|
|
||||||
: Colors.grey,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Badge(
|
return Badge(
|
||||||
|
showBadge:
|
||||||
|
state.unreadCommentsIds.isNotEmpty,
|
||||||
borderRadius: BorderRadius.circular(100),
|
borderRadius: BorderRadius.circular(100),
|
||||||
badgeContent: Container(
|
badgeContent: Container(
|
||||||
height: 3,
|
height: 3,
|
||||||
@ -245,19 +248,19 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.person,
|
Icons.person,
|
||||||
size: 16,
|
size: currentIndex == 5 ? 16 : 12,
|
||||||
color: currentIndex == 5
|
color: currentIndex == 5
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: Colors.grey,
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -266,34 +269,34 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
controller: tabController,
|
controller: tabController,
|
||||||
children: [
|
children: [
|
||||||
StoriesListView(
|
StoriesListView(
|
||||||
|
key: const ValueKey(StoryType.top),
|
||||||
storyType: StoryType.top,
|
storyType: StoryType.top,
|
||||||
header: pinnedStories,
|
header: pinnedStories,
|
||||||
onStoryTapped: onStoryTapped,
|
onStoryTapped: onStoryTapped,
|
||||||
refreshController: refreshControllerTop,
|
|
||||||
),
|
),
|
||||||
StoriesListView(
|
StoriesListView(
|
||||||
|
key: const ValueKey(StoryType.latest),
|
||||||
storyType: StoryType.latest,
|
storyType: StoryType.latest,
|
||||||
header: pinnedStories,
|
header: pinnedStories,
|
||||||
onStoryTapped: onStoryTapped,
|
onStoryTapped: onStoryTapped,
|
||||||
refreshController: refreshControllerNew,
|
|
||||||
),
|
),
|
||||||
StoriesListView(
|
StoriesListView(
|
||||||
|
key: const ValueKey(StoryType.ask),
|
||||||
storyType: StoryType.ask,
|
storyType: StoryType.ask,
|
||||||
header: pinnedStories,
|
header: pinnedStories,
|
||||||
onStoryTapped: onStoryTapped,
|
onStoryTapped: onStoryTapped,
|
||||||
refreshController: refreshControllerAsk,
|
|
||||||
),
|
),
|
||||||
StoriesListView(
|
StoriesListView(
|
||||||
|
key: const ValueKey(StoryType.show),
|
||||||
storyType: StoryType.show,
|
storyType: StoryType.show,
|
||||||
header: pinnedStories,
|
header: pinnedStories,
|
||||||
onStoryTapped: onStoryTapped,
|
onStoryTapped: onStoryTapped,
|
||||||
refreshController: refreshControllerShow,
|
|
||||||
),
|
),
|
||||||
StoriesListView(
|
StoriesListView(
|
||||||
|
key: const ValueKey(StoryType.jobs),
|
||||||
storyType: StoryType.jobs,
|
storyType: StoryType.jobs,
|
||||||
header: pinnedStories,
|
header: pinnedStories,
|
||||||
onStoryTapped: onStoryTapped,
|
onStoryTapped: onStoryTapped,
|
||||||
refreshController: refreshControllerJobs,
|
|
||||||
),
|
),
|
||||||
const ProfileScreen(),
|
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) {
|
void onStoryTapped(Story story) {
|
||||||
@ -311,14 +369,22 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
final useReader = context.read<PreferenceCubit>().state.useReader;
|
final useReader = context.read<PreferenceCubit>().state.useReader;
|
||||||
final offlineReading = context.read<StoriesBloc>().state.offlineReading;
|
final offlineReading = context.read<StoriesBloc>().state.offlineReading;
|
||||||
final firstTimeReading = cacheService.isFirstTimeReading(story.id);
|
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,
|
// 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.
|
// it would be better to just navigate to the web page.
|
||||||
final isJobWithLink = story.type == 'job' && story.url.isNotEmpty;
|
final isJobWithLink = story.type == 'job' && story.url.isNotEmpty;
|
||||||
|
|
||||||
if (!isJobWithLink) {
|
if (!isJobWithLink) {
|
||||||
HackiApp.navigatorKey.currentState!.pushNamed(StoryScreen.routeName,
|
final args = StoryScreenArgs(story: story);
|
||||||
arguments: StoryScreenArgs(story: story));
|
if (splitViewEnabled) {
|
||||||
|
context.read<SplitViewCubit>().updateStoryScreenArgs(args);
|
||||||
|
} else {
|
||||||
|
HackiApp.navigatorKey.currentState?.pushNamed(
|
||||||
|
StoryScreen.routeName,
|
||||||
|
arguments: args,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!offlineReading &&
|
if (!offlineReading &&
|
||||||
|
@ -9,6 +9,7 @@ import 'package:hacki/blocs/blocs.dart';
|
|||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/main.dart';
|
import 'package:hacki/main.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
@ -121,9 +122,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
},
|
},
|
||||||
onTap: (item) {
|
onTap: (item) {
|
||||||
if (item is Story) {
|
if (item is Story) {
|
||||||
HackiApp.navigatorKey.currentState!.pushNamed(
|
goToStoryScreen(
|
||||||
StoryScreen.routeName,
|
args: StoryScreenArgs(story: item));
|
||||||
arguments: StoryScreenArgs(story: item));
|
|
||||||
} else if (item is Comment) {
|
} else if (item is Comment) {
|
||||||
onCommentTapped(item);
|
onCommentTapped(item);
|
||||||
}
|
}
|
||||||
@ -167,11 +167,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
onLoadMore: () {
|
onLoadMore: () {
|
||||||
context.read<FavCubit>().loadMore();
|
context.read<FavCubit>().loadMore();
|
||||||
},
|
},
|
||||||
onTap: (story) {
|
onTap: (story) => goToStoryScreen(
|
||||||
HackiApp.navigatorKey.currentState!.pushNamed(
|
args: StoryScreenArgs(story: story)),
|
||||||
StoryScreen.routeName,
|
|
||||||
arguments: StoryScreenArgs(story: story));
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -188,18 +185,29 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
top: 50,
|
top: 50,
|
||||||
child: Offstage(
|
child: Offstage(
|
||||||
offstage: pageType != _PageType.notification,
|
offstage: pageType != _PageType.notification,
|
||||||
child: InboxView(
|
child: notificationState.comments.isEmpty
|
||||||
refreshController: refreshControllerNotification,
|
? const CenteredMessageView(
|
||||||
|
content:
|
||||||
|
'New replies to your comments or stories '
|
||||||
|
'will show up here.',
|
||||||
|
)
|
||||||
|
: InboxView(
|
||||||
|
refreshController:
|
||||||
|
refreshControllerNotification,
|
||||||
unreadCommentsIds:
|
unreadCommentsIds:
|
||||||
notificationState.unreadCommentsIds,
|
notificationState.unreadCommentsIds,
|
||||||
comments: notificationState.comments,
|
comments: notificationState.comments,
|
||||||
onCommentTapped: (cmt) {
|
onCommentTapped: (cmt) {
|
||||||
onCommentTapped(cmt, then: () {
|
onCommentTapped(cmt, then: () {
|
||||||
context.read<NotificationCubit>().markAsRead(cmt);
|
context
|
||||||
|
.read<NotificationCubit>()
|
||||||
|
.markAsRead(cmt);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onMarkAllAsReadTapped: () {
|
onMarkAllAsReadTapped: () {
|
||||||
context.read<NotificationCubit>().markAllAsRead();
|
context
|
||||||
|
.read<NotificationCubit>()
|
||||||
|
.markAllAsRead();
|
||||||
},
|
},
|
||||||
onLoadMore: () {
|
onLoadMore: () {
|
||||||
context.read<NotificationCubit>().loadMore();
|
context.read<NotificationCubit>().loadMore();
|
||||||
@ -360,7 +368,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
showAboutDialog(
|
showAboutDialog(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: 'Hacki',
|
applicationName: 'Hacki',
|
||||||
applicationVersion: 'v0.2.0',
|
applicationVersion: 'v0.2.1',
|
||||||
applicationIcon: Image.asset(
|
applicationIcon: Image.asset(
|
||||||
Constants.hackiIconPath,
|
Constants.hackiIconPath,
|
||||||
height: 50,
|
height: 50,
|
||||||
@ -447,18 +455,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
HackiApp.navigatorKey.currentState
|
HackiApp.navigatorKey.currentState
|
||||||
?.pushNamed(SubmitScreen.routeName);
|
?.pushNamed(SubmitScreen.routeName);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showSnackBar(
|
||||||
SnackBar(
|
content: 'You need to log in first.',
|
||||||
content: const Text(
|
|
||||||
'You need to log in first.',
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
action: SnackBarAction(
|
|
||||||
label: 'Log in',
|
label: 'Log in',
|
||||||
textColor: Colors.black,
|
action: onLoginTapped,
|
||||||
onPressed: onLoginTapped,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -554,11 +554,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
|
|
||||||
void showThemeSettingDialog({bool useTrueDarkMode = false}) {
|
void showThemeSettingDialog({bool useTrueDarkMode = false}) {
|
||||||
if (useTrueDarkMode) {
|
if (useTrueDarkMode) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showSnackBar(
|
||||||
const SnackBar(
|
content: "Can't choose theme when using true dark mode.",
|
||||||
content: Text("Can't choose theme when using true dark mode."),
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -601,18 +598,14 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
.fetchParentStoryWithComments(id: comment.parent)
|
.fetchParentStoryWithComments(id: comment.parent)
|
||||||
.then((tuple) {
|
.then((tuple) {
|
||||||
if (tuple != null && mounted) {
|
if (tuple != null && mounted) {
|
||||||
HackiApp.navigatorKey.currentState!
|
goToStoryScreen(
|
||||||
.pushNamed(
|
args: StoryScreenArgs(
|
||||||
StoryScreen.routeName,
|
|
||||||
arguments: StoryScreenArgs(
|
|
||||||
story: tuple.item1,
|
story: tuple.item1,
|
||||||
targetComments: tuple.item2.isEmpty
|
targetComments:
|
||||||
? [comment]
|
tuple.item2.isEmpty ? [comment] : [comment, ...tuple.item2],
|
||||||
: [comment, ...tuple.item2],
|
|
||||||
onlyShowTargetComment: true,
|
onlyShowTargetComment: true,
|
||||||
),
|
),
|
||||||
)
|
)?.then((_) => then?.call());
|
||||||
.then((_) => then?.call());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -630,12 +623,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.isLoggedIn) {
|
if (state.isLoggedIn) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showSnackBar(content: 'Logged in successfully!');
|
||||||
const SnackBar(
|
|
||||||
content: Text('Logged in successfully!'),
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
@ -759,7 +747,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
child: ButtonBar(
|
child: ButtonBar(
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.read<AuthBloc>().add(AuthInitialize());
|
||||||
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Cancel',
|
'Cancel',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -104,6 +104,13 @@ class InboxView extends StatelessWidget {
|
|||||||
? textColor
|
? textColor
|
||||||
: Colors.grey,
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
|
linkStyle: TextStyle(
|
||||||
|
color:
|
||||||
|
unreadCommentsIds.contains(e.id)
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.orange
|
||||||
|
.withOpacity(0.6),
|
||||||
|
),
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
onOpen: (link) =>
|
onOpen: (link) =>
|
||||||
LinkUtil.launchUrl(link.url),
|
LinkUtil.launchUrl(link.url),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hacki/blocs/blocs.dart';
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
@ -22,6 +23,22 @@ class OfflineListTile extends StatelessWidget {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final downloading =
|
final downloading =
|
||||||
state.downloadStatus == StoriesDownloadStatus.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(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
downloading ? 'Downloading All Stories...' : 'Download All Stories',
|
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 '
|
'download all latest stories that have at least one comment '
|
||||||
"for offline reading. (web page won't be downloaded)",
|
"for offline reading. (web page won't be downloaded)",
|
||||||
),
|
),
|
||||||
trailing: downloading
|
trailing: trailingWidget,
|
||||||
? const SizedBox(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
child: CustomCircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.download),
|
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
Connectivity().checkConnectivity().then((res) {
|
||||||
|
if (res != ConnectivityResult.none) {
|
||||||
Wakelock.enable();
|
Wakelock.enable();
|
||||||
context.read<StoriesBloc>().add(StoriesDownload());
|
context.read<StoriesBloc>().add(StoriesDownload());
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||||
import 'package:hacki/cubits/cubits.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/screens.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
@ -31,6 +31,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
body: Column(
|
body: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -100,25 +101,24 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
...state.results
|
...state.results
|
||||||
.map((e) => [
|
.map(
|
||||||
|
(e) => [
|
||||||
FadeIn(
|
FadeIn(
|
||||||
child: StoryTile(
|
child: StoryTile(
|
||||||
showWebPreview:
|
showWebPreview:
|
||||||
prefState.showComplexStoryTile,
|
prefState.showComplexStoryTile,
|
||||||
story: e,
|
story: e,
|
||||||
onTap: () {
|
onTap: () => goToStoryScreen(
|
||||||
HackiApp.navigatorKey.currentState!
|
args: StoryScreenArgs(story: e),
|
||||||
.pushNamed(
|
),
|
||||||
StoryScreen.routeName,
|
),
|
||||||
arguments: StoryScreenArgs(
|
|
||||||
story: e));
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
if (!prefState.showComplexStoryTile)
|
if (!prefState.showComplexStoryTile)
|
||||||
const Divider(
|
const Divider(
|
||||||
height: 0,
|
height: 0,
|
||||||
),
|
),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
.expand((e) => e)
|
.expand((e) => e)
|
||||||
.toList(),
|
.toList(),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:feature_discovery/feature_discovery.dart';
|
import 'package:feature_discovery/feature_discovery.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.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/constants.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/main.dart';
|
import 'package:hacki/main.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/repositories.dart';
|
||||||
@ -30,8 +32,8 @@ enum _MenuAction {
|
|||||||
cancel,
|
cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoryScreenArgs {
|
class StoryScreenArgs extends Equatable {
|
||||||
StoryScreenArgs({
|
const StoryScreenArgs({
|
||||||
required this.story,
|
required this.story,
|
||||||
this.onlyShowTargetComment = false,
|
this.onlyShowTargetComment = false,
|
||||||
this.targetComments,
|
this.targetComments,
|
||||||
@ -40,12 +42,22 @@ class StoryScreenArgs {
|
|||||||
final Story story;
|
final Story story;
|
||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
final List<Comment>? targetComments;
|
final List<Comment>? targetComments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
story,
|
||||||
|
onlyShowTargetComment,
|
||||||
|
targetComments,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoryScreen extends StatefulWidget {
|
class StoryScreen extends StatefulWidget {
|
||||||
const StoryScreen(
|
const StoryScreen({
|
||||||
{Key? key, required this.story, required this.parentComments})
|
Key? key,
|
||||||
: super(key: key);
|
this.splitViewEnabled = false,
|
||||||
|
required this.story,
|
||||||
|
required this.parentComments,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
static const String routeName = '/story';
|
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 Story story;
|
||||||
final List<Comment> parentComments;
|
final List<Comment> parentComments;
|
||||||
|
|
||||||
@ -166,16 +207,12 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
editCubit.onReplySubmittedSuccessfully();
|
editCubit.onReplySubmittedSuccessfully();
|
||||||
context.read<PostCubit>().reset();
|
context.read<PostCubit>().reset();
|
||||||
} else if (postState.status == PostStatus.failure) {
|
} else if (postState.status == PostStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
showSnackBar(
|
||||||
content: Text(
|
content:
|
||||||
'Something went wrong...${(sadFaces..shuffle()).first}',
|
'Something went wrong...${(sadFaces..shuffle()).first}',
|
||||||
),
|
|
||||||
backgroundColor: Colors.orange,
|
|
||||||
action: SnackBarAction(
|
|
||||||
label: 'Okay',
|
label: 'Okay',
|
||||||
onPressed: () =>
|
action: ScaffoldMessenger.of(context).hideCurrentSnackBar,
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar()),
|
);
|
||||||
));
|
|
||||||
context.read<PostCubit>().reset();
|
context.read<PostCubit>().reset();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -190,44 +227,7 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return BlocListener<EditCubit, EditState>(
|
final mainView = SmartRefresher(
|
||||||
listenWhen: (previous, current) {
|
|
||||||
return previous.replyingTo != current.replyingTo ||
|
|
||||||
previous.itemBeingEdited != current.itemBeingEdited;
|
|
||||||
},
|
|
||||||
listener: (context, editState) {
|
|
||||||
if (editState.replyingTo != null ||
|
|
||||||
editState.itemBeingEdited != null) {
|
|
||||||
if (editState.text == null) {
|
|
||||||
commentEditingController.clear();
|
|
||||||
} else {
|
|
||||||
final text = editState.text!;
|
|
||||||
commentEditingController
|
|
||||||
..text = text
|
|
||||||
..selection = TextSelection.fromPosition(
|
|
||||||
TextPosition(offset: text.length));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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,
|
scrollController: scrollController,
|
||||||
enablePullUp: !state.onlyShowTargetComment,
|
enablePullUp: !state.onlyShowTargetComment,
|
||||||
enablePullDown: !state.onlyShowTargetComment,
|
enablePullDown: !state.onlyShowTargetComment,
|
||||||
@ -273,6 +273,7 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
height: topPadding,
|
height: topPadding,
|
||||||
),
|
),
|
||||||
|
if (!widget.splitViewEnabled)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(bottom: 6),
|
padding: EdgeInsets.only(bottom: 6),
|
||||||
child: OfflineBanner(),
|
child: OfflineBanner(),
|
||||||
@ -286,10 +287,7 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
if (widget.story !=
|
if (widget.story !=
|
||||||
context
|
context.read<EditCubit>().state.replyingTo) {
|
||||||
.read<EditCubit>()
|
|
||||||
.state
|
|
||||||
.replyingTo) {
|
|
||||||
commentEditingController.clear();
|
commentEditingController.clear();
|
||||||
}
|
}
|
||||||
editCubit.onReplyTapped(widget.story);
|
editCubit.onReplyTapped(widget.story);
|
||||||
@ -350,8 +348,12 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
widget.story.title,
|
widget.story.title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
color: widget.story.url.isNotEmpty
|
||||||
|
? Colors.orange
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -362,6 +364,21 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
),
|
),
|
||||||
child: SelectableHtml(
|
child: SelectableHtml(
|
||||||
data: widget.story.text,
|
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, _, __, ___) =>
|
onLinkTap: (link, _, __, ___) =>
|
||||||
LinkUtil.launchUrl(link ?? ''),
|
LinkUtil.launchUrl(link ?? ''),
|
||||||
),
|
),
|
||||||
@ -378,9 +395,8 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
),
|
),
|
||||||
if (state.onlyShowTargetComment) ...[
|
if (state.onlyShowTargetComment) ...[
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context
|
onPressed: () =>
|
||||||
.read<CommentsCubit>()
|
context.read<CommentsCubit>().loadAll(widget.story),
|
||||||
.loadAll(widget.story),
|
|
||||||
child: const Text('View all comments'),
|
child: const Text('View all comments'),
|
||||||
),
|
),
|
||||||
const Divider(
|
const Divider(
|
||||||
@ -403,13 +419,11 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
(e) => FadeIn(
|
(e) => FadeIn(
|
||||||
child: CommentTile(
|
child: CommentTile(
|
||||||
comment: e,
|
comment: e,
|
||||||
onlyShowTargetComment:
|
onlyShowTargetComment: state.onlyShowTargetComment,
|
||||||
state.onlyShowTargetComment,
|
|
||||||
targetComments: widget.parentComments.sublist(
|
targetComments: widget.parentComments.sublist(
|
||||||
0, max(widget.parentComments.length - 1, 0)),
|
0, max(widget.parentComments.length - 1, 0)),
|
||||||
myUsername: authState.isLoggedIn
|
myUsername:
|
||||||
? authState.username
|
authState.isLoggedIn ? authState.username : null,
|
||||||
: null,
|
|
||||||
onReplyTapped: (cmt) {
|
onReplyTapped: (cmt) {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
if (cmt.deleted || cmt.dead) {
|
if (cmt.deleted || cmt.dead) {
|
||||||
@ -417,10 +431,7 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cmt !=
|
if (cmt !=
|
||||||
context
|
context.read<EditCubit>().state.replyingTo) {
|
||||||
.read<EditCubit>()
|
|
||||||
.state
|
|
||||||
.replyingTo) {
|
|
||||||
commentEditingController.clear();
|
commentEditingController.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,23 +482,56 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return BlocListener<EditCubit, EditState>(
|
||||||
|
listenWhen: (previous, current) {
|
||||||
|
return previous.replyingTo != current.replyingTo ||
|
||||||
|
previous.itemBeingEdited != current.itemBeingEdited;
|
||||||
|
},
|
||||||
|
listener: (context, editState) {
|
||||||
|
if (editState.replyingTo != null ||
|
||||||
|
editState.itemBeingEdited != null) {
|
||||||
|
if (editState.text == null) {
|
||||||
|
commentEditingController.clear();
|
||||||
|
} else {
|
||||||
|
final text = editState.text!;
|
||||||
|
commentEditingController
|
||||||
|
..text = text
|
||||||
|
..selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: text.length));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commentEditingController.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: widget.splitViewEnabled
|
||||||
|
? Material(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: mainView,
|
||||||
),
|
),
|
||||||
bottomSheet: Offstage(
|
Positioned(
|
||||||
offstage: !editCubit.state.showReplyBox,
|
top: 0,
|
||||||
child: BlocBuilder<PostCubit, PostState>(
|
left: 0,
|
||||||
builder: (context, postState) {
|
right: 0,
|
||||||
return BlocBuilder<EditCubit, EditState>(
|
child: CustomAppBar(
|
||||||
buildWhen: (previous, current) =>
|
backgroundColor: Theme.of(context)
|
||||||
previous.itemBeingEdited !=
|
.canvasColor
|
||||||
current.itemBeingEdited ||
|
.withOpacity(0.6),
|
||||||
previous.replyingTo != current.replyingTo,
|
story: widget.story,
|
||||||
builder: (context, editState) {
|
scrollController: scrollController,
|
||||||
return ReplyBox(
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: ReplyBox(
|
||||||
|
splitViewEnabled: true,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
textEditingController: commentEditingController,
|
textEditingController: commentEditingController,
|
||||||
editing: editState.itemBeingEdited,
|
|
||||||
replyingTo: editState.replyingTo,
|
|
||||||
isLoading: postState.status == PostStatus.loading,
|
|
||||||
onSendTapped: onSendTapped,
|
onSendTapped: onSendTapped,
|
||||||
onCloseTapped: () {
|
onCloseTapped: () {
|
||||||
editCubit.onReplyBoxClosed();
|
editCubit.onReplyBoxClosed();
|
||||||
@ -495,12 +539,32 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
focusNode.unfocus();
|
focusNode.unfocus();
|
||||||
},
|
},
|
||||||
onChanged: editCubit.onTextChanged,
|
onChanged: editCubit.onTextChanged,
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Scaffold(
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
resizeToAvoidBottomInset: true,
|
||||||
|
appBar: CustomAppBar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).canvasColor.withOpacity(0.6),
|
||||||
|
story: widget.story,
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
|
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) {
|
} else if (voteState.status == VoteStatus.failureNotLoggedIn) {
|
||||||
showSnackBar(
|
showSnackBar(
|
||||||
content: 'Not logged in, no voting! (;`O´)o',
|
content: 'Not logged in, no voting! (;`O´)o',
|
||||||
withLoginAction: true,
|
action: onLoginTapped,
|
||||||
|
label: 'Log in',
|
||||||
);
|
);
|
||||||
} else if (voteState.status == VoteStatus.failureBeHumble) {
|
} else if (voteState.status == VoteStatus.failureBeHumble) {
|
||||||
showSnackBar(content: 'No voting on your own post! (;`O´)o');
|
showSnackBar(content: 'No voting on your own post! (;`O´)o');
|
||||||
@ -649,6 +714,7 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
onBlockTapped(item, isBlocked);
|
onBlockTapped(item, isBlocked);
|
||||||
break;
|
break;
|
||||||
case _MenuAction.cancel:
|
case _MenuAction.cancel:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -946,7 +1012,10 @@ class _StoryScreenState extends State<StoryScreen> {
|
|||||||
child: ButtonBar(
|
child: ButtonBar(
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.read<AuthBloc>().add(AuthInitialize());
|
||||||
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Cancel',
|
'Cancel',
|
||||||
style: TextStyle(
|
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:clipboard/clipboard.dart';
|
||||||
import 'package:flutter/material.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_feather_icons/flutter_feather_icons.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/utils/link_util.dart';
|
import 'package:hacki/utils/link_util.dart';
|
||||||
|
|
||||||
class ReplyBox extends StatefulWidget {
|
class ReplyBox extends StatefulWidget {
|
||||||
const ReplyBox({
|
const ReplyBox({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.splitViewEnabled = false,
|
||||||
required this.focusNode,
|
required this.focusNode,
|
||||||
required this.textEditingController,
|
required this.textEditingController,
|
||||||
required this.replyingTo,
|
|
||||||
required this.editing,
|
|
||||||
required this.onSendTapped,
|
required this.onSendTapped,
|
||||||
required this.onCloseTapped,
|
required this.onCloseTapped,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.isLoading,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final bool splitViewEnabled;
|
||||||
final FocusNode focusNode;
|
final FocusNode focusNode;
|
||||||
final TextEditingController textEditingController;
|
final TextEditingController textEditingController;
|
||||||
final Item? replyingTo;
|
|
||||||
final Item? editing;
|
|
||||||
final VoidCallback onSendTapped;
|
final VoidCallback onSendTapped;
|
||||||
final VoidCallback onCloseTapped;
|
final VoidCallback onCloseTapped;
|
||||||
final ValueChanged<String> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
final bool isLoading;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ReplyBoxState createState() => _ReplyBoxState();
|
_ReplyBoxState createState() => _ReplyBoxState();
|
||||||
@ -34,20 +32,44 @@ class ReplyBox extends StatefulWidget {
|
|||||||
class _ReplyBoxState extends State<ReplyBox> {
|
class _ReplyBoxState extends State<ReplyBox> {
|
||||||
bool expanded = false;
|
bool expanded = false;
|
||||||
double? expandedHeight;
|
double? expandedHeight;
|
||||||
double? topPadding;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
expandedHeight ??= MediaQuery.of(context).size.height;
|
expandedHeight ??= MediaQuery.of(context).size.height;
|
||||||
topPadding ??= MediaQuery.of(context).padding.top + kToolbarHeight;
|
return BlocBuilder<EditCubit, EditState>(
|
||||||
return AnimatedContainer(
|
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;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: expanded
|
||||||
|
? 0
|
||||||
|
: widget.splitViewEnabled
|
||||||
|
? MediaQuery.of(context).viewInsets.bottom
|
||||||
|
: 0,
|
||||||
|
),
|
||||||
|
child: AnimatedContainer(
|
||||||
height: expanded ? expandedHeight : 100,
|
height: expanded ? expandedHeight : 100,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
|
if (!context.read<SplitViewCubit>().state.enabled)
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: expanded ? Colors.transparent : Colors.black54,
|
color: expanded
|
||||||
offset: const Offset(0, 20), //(x,y)
|
? Colors.transparent
|
||||||
|
: Colors.black26,
|
||||||
blurRadius: 40,
|
blurRadius: 40,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -55,8 +77,12 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
child: Material(
|
child: Material(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (context.read<SplitViewCubit>().state.enabled)
|
||||||
|
const Divider(
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
height: expanded ? topPadding : 0,
|
height: expanded ? 36 : 0,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@ -67,20 +93,21 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.replyingTo == null
|
replyingTo == null
|
||||||
? 'Editing'
|
? 'Editing'
|
||||||
: 'Replying '
|
: 'Replying '
|
||||||
'${widget.replyingTo?.by}',
|
'${replyingTo.by}',
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (!widget.isLoading) ...[
|
if (!isLoading) ...[
|
||||||
...[
|
...[
|
||||||
if (widget.replyingTo != null)
|
if (replyingTo != null)
|
||||||
AnimatedOpacity(
|
AnimatedOpacity(
|
||||||
opacity: expanded ? 1 : 0,
|
opacity: expanded ? 1 : 0,
|
||||||
duration: const Duration(milliseconds: 300),
|
duration:
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
key: const Key('quote'),
|
key: const Key('quote'),
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@ -88,7 +115,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
onPressed: expanded ? showTextPopup : null,
|
onPressed:
|
||||||
|
expanded ? showTextPopup : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -119,7 +147,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (widget.isLoading)
|
if (isLoading)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
@ -150,7 +178,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
focusNode: widget.focusNode,
|
focusNode: widget.focusNode,
|
||||||
controller: widget.textEditingController,
|
controller: widget.textEditingController,
|
||||||
@ -174,44 +203,54 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showTextPopup() {
|
void showTextPopup() {
|
||||||
|
final replyingTo = context.read<EditCubit>().state.replyingTo;
|
||||||
|
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return Container(
|
return AlertDialog(
|
||||||
margin: const EdgeInsets.only(
|
insetPadding: const EdgeInsets.symmetric(
|
||||||
left: 12,
|
horizontal: 12,
|
||||||
right: 12,
|
vertical: 24,
|
||||||
top: 64,
|
|
||||||
bottom: 64,
|
|
||||||
),
|
),
|
||||||
color: Theme.of(context).canvasColor,
|
contentPadding: EdgeInsets.zero,
|
||||||
child: Padding(
|
content: ConstrainedBox(
|
||||||
padding: const EdgeInsets.only(
|
constraints: const BoxConstraints(
|
||||||
left: 12,
|
maxWidth: 500,
|
||||||
right: 6,
|
maxHeight: 500,
|
||||||
top: 6,
|
|
||||||
bottom: 12,
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Material(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 12,
|
||||||
|
top: 6,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.replyingTo?.by ?? '',
|
replyingTo?.by ?? '',
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Copy All'),
|
child: const Text('Copy All'),
|
||||||
onPressed: () => FlutterClipboard.copy(
|
onPressed: () => FlutterClipboard.copy(
|
||||||
widget.replyingTo?.text ?? '',
|
replyingTo?.text ?? '',
|
||||||
),
|
).then((_) => HapticFeedback.selectionClick()),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@ -221,9 +260,6 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
),
|
),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
|
||||||
width: 6,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -231,12 +267,22 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
isAlwaysShown: true,
|
isAlwaysShown: true,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 6),
|
padding: const EdgeInsets.only(
|
||||||
|
left: 12,
|
||||||
|
right: 6,
|
||||||
|
top: 6,
|
||||||
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: SelectableLinkify(
|
child: SelectableHtml(
|
||||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||||
text: widget.replyingTo?.text ?? '',
|
data: replyingTo?.text ?? '',
|
||||||
onOpen: (link) => LinkUtil.launchUrl(link.url),
|
style: {
|
||||||
|
'a': Style(
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
onLinkTap: (link, _, __, ___) =>
|
||||||
|
LinkUtil.launchUrl(link!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -244,8 +290,7 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
));
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export 'custom_app_bar.dart';
|
||||||
export 'fav_icon_button.dart';
|
export 'fav_icon_button.dart';
|
||||||
export 'link_icon_button.dart';
|
export 'link_icon_button.dart';
|
||||||
export 'pin_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/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
|
|
||||||
class SubmitScreen extends StatefulWidget {
|
class SubmitScreen extends StatefulWidget {
|
||||||
const SubmitScreen({Key? key}) : super(key: key);
|
const SubmitScreen({Key? key}) : super(key: key);
|
||||||
@ -199,13 +200,4 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
|||||||
(textEditingController.text.isNotEmpty ||
|
(textEditingController.text.isNotEmpty ||
|
||||||
urlEditingController.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(
|
child: SelectableLinkify(
|
||||||
key: ObjectKey(comment),
|
key: ObjectKey(comment),
|
||||||
text: comment.text,
|
text: comment.text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: MediaQuery.of(context)
|
||||||
|
.textScaleFactor *
|
||||||
|
15,
|
||||||
|
),
|
||||||
|
linkStyle: TextStyle(
|
||||||
|
fontSize: MediaQuery.of(context)
|
||||||
|
.textScaleFactor *
|
||||||
|
15,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
onOpen: (link) {
|
onOpen: (link) {
|
||||||
if (link.url.contains(
|
if (link.url.contains(
|
||||||
'news.ycombinator.com/item')) {
|
'news.ycombinator.com/item')) {
|
||||||
|
@ -133,6 +133,9 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
|||||||
child: Linkify(
|
child: Linkify(
|
||||||
text: e.text,
|
text: e.text,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
|
linkStyle: const TextStyle(
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
onOpen: (link) =>
|
onOpen: (link) =>
|
||||||
LinkUtil.launchUrl(link.url),
|
LinkUtil.launchUrl(link.url),
|
||||||
),
|
),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
@ -184,7 +185,12 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 ??
|
final loadingWidget = widget.placeholderWidget ??
|
||||||
Container(
|
Container(
|
||||||
height: _height,
|
height: _height,
|
||||||
|
@ -83,22 +83,23 @@ class LinkView extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (showMultiMedia)
|
if (showMultiMedia)
|
||||||
Expanded(
|
Padding(
|
||||||
flex: 2,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
right: 5,
|
right: 5,
|
||||||
top: 5,
|
top: 5,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
),
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: layoutHeight,
|
||||||
|
width: layoutHeight,
|
||||||
child: (imageUri?.isEmpty ?? true) && imagePath != null
|
child: (imageUri?.isEmpty ?? true) && imagePath != null
|
||||||
? Image.asset(
|
? Image.asset(
|
||||||
imagePath!,
|
imagePath!,
|
||||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.cover,
|
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||||
)
|
)
|
||||||
: CachedNetworkImage(
|
: CachedNetworkImage(
|
||||||
imageUrl: imageUri!,
|
imageUrl: imageUri!,
|
||||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.cover,
|
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||||
memCacheHeight: layoutHeight.toInt() * 4,
|
memCacheHeight: layoutHeight.toInt() * 4,
|
||||||
errorWidget: (context, _, dynamic __) {
|
errorWidget: (context, _, dynamic __) {
|
||||||
return Image.asset(
|
return Image.asset(
|
||||||
|
@ -7,22 +7,37 @@ import 'package:hacki/models/models.dart';
|
|||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
|
|
||||||
class StoriesListView extends StatelessWidget {
|
class StoriesListView extends StatefulWidget {
|
||||||
const StoriesListView({
|
const StoriesListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.storyType,
|
required this.storyType,
|
||||||
required this.header,
|
required this.header,
|
||||||
required this.onStoryTapped,
|
required this.onStoryTapped,
|
||||||
required this.refreshController,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final StoryType storyType;
|
final StoryType storyType;
|
||||||
final Widget header;
|
final Widget header;
|
||||||
final ValueChanged<Story> onStoryTapped;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final storyType = widget.storyType;
|
||||||
|
final header = widget.header;
|
||||||
|
final onStoryTapped = widget.onStoryTapped;
|
||||||
|
|
||||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) =>
|
||||||
previous.showComplexStoryTile != current.showComplexStoryTile,
|
previous.showComplexStoryTile != current.showComplexStoryTile,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
@ -26,7 +28,12 @@ class StoryTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (showWebPreview) {
|
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) {
|
if (story.url.isNotEmpty) {
|
||||||
return TapDownWrapper(
|
return TapDownWrapper(
|
||||||
@ -47,9 +54,7 @@ class StoryTile extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Padding(
|
||||||
flex: 2,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
right: 5,
|
right: 5,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
@ -61,7 +66,6 @@ class StoryTile extends StatelessWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 4,
|
flex: 4,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -119,7 +123,7 @@ class StoryTile extends StatelessWidget {
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
removeElevation: true,
|
removeElevation: true,
|
||||||
bodyMaxLines: 4,
|
bodyMaxLines: height == 100 ? 3 : 4,
|
||||||
errorTitle: story.title,
|
errorTitle: story.title,
|
||||||
titleStyle: TextStyle(
|
titleStyle: TextStyle(
|
||||||
color: wasRead
|
color: wasRead
|
||||||
@ -149,7 +153,7 @@ class StoryTile extends StatelessWidget {
|
|||||||
onTap: (_) {},
|
onTap: (_) {},
|
||||||
url: '',
|
url: '',
|
||||||
imagePath: Constants.hackerNewsLogoPath,
|
imagePath: Constants.hackerNewsLogoPath,
|
||||||
bodyMaxLines: 4,
|
bodyMaxLines: height == 100 ? 3 : 4,
|
||||||
titleTextStyle: TextStyle(
|
titleTextStyle: TextStyle(
|
||||||
color: wasRead
|
color: wasRead
|
||||||
? Colors.grey[500]
|
? Colors.grey[500]
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export 'circle_tab_indicator.dart';
|
||||||
export 'comment_tile.dart';
|
export 'comment_tile.dart';
|
||||||
export 'custom_circular_progress_indicator.dart';
|
export 'custom_circular_progress_indicator.dart';
|
||||||
export 'items_list_view.dart';
|
export 'items_list_view.dart';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 0.2.0+31
|
version: 0.2.1+32
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
@ -13,6 +13,7 @@ dependencies:
|
|||||||
cached_network_image: ^3.2.0
|
cached_network_image: ^3.2.0
|
||||||
clipboard: ^0.1.3
|
clipboard: ^0.1.3
|
||||||
collection:
|
collection:
|
||||||
|
connectivity_plus: ^2.2.1
|
||||||
dio: ^4.0.4
|
dio: ^4.0.4
|
||||||
equatable: 2.0.3
|
equatable: 2.0.3
|
||||||
fast_gbk: ^1.0.0
|
fast_gbk: ^1.0.0
|
||||||
@ -38,6 +39,7 @@ dependencies:
|
|||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
path_provider: ^2.0.8
|
path_provider: ^2.0.8
|
||||||
pull_to_refresh: ^2.0.0
|
pull_to_refresh: ^2.0.0
|
||||||
|
responsive_builder: ^0.4.2
|
||||||
sembast: ^3.1.1+1
|
sembast: ^3.1.1+1
|
||||||
shared_preferences: ^2.0.11
|
shared_preferences: ^2.0.11
|
||||||
shimmer: ^2.0.0
|
shimmer: ^2.0.0
|
||||||
|
Reference in New Issue
Block a user