Compare commits

...

23 Commits

Author SHA1 Message Date
f70377d99c updated README.md 2022-03-25 18:19:05 -07:00
e190826fcf updated README.md 2022-03-25 09:37:21 -07:00
6f8dfaca8e updated README.md 2022-03-25 00:55:38 -07:00
889011e249 Merge pull request #10 from Livinglist/v0.2.1
V0.2.1
2022-03-24 19:24:52 -07:00
c141bd7ba5 added lock file. 2022-03-24 12:46:41 -07:00
d10e630c17 fixed UI. 2022-03-23 21:41:47 -07:00
5ae7769a36 Merge branch 'v0.2.1' of github.com:Livinglist/Hacki into v0.2.1 2022-03-23 16:04:49 -07:00
4f9761b2fa fixed inbox view. 2022-03-23 16:04:25 -07:00
c244435d01 updated README.md 2022-03-23 14:32:50 -07:00
db2de7aae4 Update README.md 2022-03-23 14:26:18 -07:00
35d7ffc09e enabled icloud. 2022-03-23 14:23:04 -07:00
dd4e56e31c changed tabbar style. 2022-03-23 13:17:35 -07:00
38f3efbe50 changed look of tab bar and story title. 2022-03-23 10:35:11 -07:00
4ebde84a79 bumped version. 2022-03-22 21:45:17 -07:00
382d2be5ba fixed link preview overflow. 2022-03-22 21:33:37 -07:00
7f66c80577 fixed preview pic size. 2022-03-22 17:40:21 -07:00
21d47cd54c optimized for ipad. 2022-03-22 17:25:13 -07:00
1139fa3c3b fixed copy dialog. 2022-03-22 16:22:31 -07:00
369a1e7bbf improved offline feature. 2022-03-22 16:03:59 -07:00
c3c86a4a44 fixed split view and keyboard. 2022-03-22 14:13:14 -07:00
5f39ad7df8 fixed refresh controller and reply box. 2022-03-22 10:44:30 -07:00
897ee81970 cleaned up code. 2022-03-21 22:08:35 -07:00
12098ce8cc added splitview. 2022-03-21 21:10:02 -07:00
37 changed files with 1235 additions and 801 deletions

1
.gitignore vendored
View File

@ -46,4 +46,3 @@ app.*.map.json
/android/app/release
/ios/Podfile.lock
pubspec.lock

View File

@ -2,7 +2,6 @@
A simple noiseless Hacker News reader made with Flutter that is just enough.
![iOS](https://img.shields.io/badge/iOS-13%20-blue)
[![App Store](https://img.shields.io/itunes/v/1602043763?label=App%20Store)](https://apps.apple.com/us/app/hacki/id1602043763)
[![Play Store](https://img.shields.io/badge/Play%20Store--yellow)](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US)
[![Visits Badge](https://badges.pufler.dev/visits/livinglist/Hacki)](https://badges.pufler.dev)
@ -31,18 +30,24 @@ Features:
<p align="center">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859621-965080f3-a191-44cd-a2fc-9ac1f489ef84.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859627-48290a22-9679-442b-bae4-97f21546b3ae.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859630-93f7e372-f2e7-4357-86c0-250a3f69c10f.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/148859632-b52a89ca-b8d7-464c-a508-faa86bcc87f8.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/155449312-4208a961-44ac-42b3-968b-9526d4a07787.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/150713047-2710add8-0493-4c42-a710-f96dc77cfde1.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/150918515-0fc4869f-efa3-473f-90af-381daf5e4915.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/152305175-94fa3696-f40f-4f40-b040-f17fc59ff260.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/152301588-070ded9a-117a-48d8-bad4-9f77d54d98df.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/152301590-5383200c-db73-487d-8742-57ccbbbf04e8.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/153973720-8a6aad44-7df3-4deb-8465-8c88b5e5f587.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/157003000-15671cf0-9470-4a89-b123-f63ca70a970f.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799288-6e98352a-fe89-4a2e-8a74-c5782463a1e1.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799297-75b52eac-2066-4df9-bdfc-7c82bf7b81c8.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799302-860c61b8-abba-486a-9592-bc84a6af3232.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159799305-308743d3-1c89-45de-9645-3b6ec789c282.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798176-5212e9bf-296d-4d9b-ab48-19b741684c8a.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798179-72edbe49-7444-4e54-a07c-fc1244447a74.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798182-28397805-a7cc-4124-b65b-c02c80afbbec.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798183-c2984270-ee99-4419-841e-65e98890464f.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798184-2fce5d97-710e-44a7-b99a-3296ebcf273b.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798185-d7c81348-956e-483c-a1bc-5cd872bdad62.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798186-1457ae21-f1aa-40a4-9206-0f3a1e24653e.png">
<img width="200" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/159798187-4404adea-b2bc-472e-8568-2379e6db01a4.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162711-a9146326-9645-4db6-a04e-1f82e6133e40.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162726-ef1d3f2a-5179-417c-8a5f-0cddb52249da.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162733-906c4afd-39a8-48ae-946a-8019b327eaa0.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160162735-f2b25119-4702-4308-b2f5-281a2a2c5901.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160163024-6dcd65b6-bada-4c1c-95af-387fd4f42fb2.png">
<img width="400" alt="Screen Shot 2020-03-03 at 1 22 57 PM" src="https://user-images.githubusercontent.com/7277662/160163033-7bcf7038-b9aa-4ce4-8b58-64578eae8531.png">
</p>

View File

@ -14,6 +14,7 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
D1B8F07E58D19B7A04062545 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A738A80ED7D259B7B74BB44E /* Pods_Runner.framework */; };
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@ -47,6 +48,8 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9F471B54216646F2690E5F66 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
A738A80ED7D259B7B74BB44E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E575B6F027EBC6DA002B1508 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -55,6 +58,7 @@
buildActionMask = 2147483647;
files = (
D1B8F07E58D19B7A04062545 /* Pods_Runner.framework in Frameworks */,
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -94,6 +98,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@ -109,6 +114,7 @@
B3F4F49CF582C662A01499C0 /* Frameworks */ = {
isa = PBXGroup;
children = (
E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
A738A80ED7D259B7B74BB44E /* Pods_Runner.framework */,
);
name = Frameworks;
@ -354,6 +360,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
@ -365,13 +372,13 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.0;
MARKETING_VERSION = 0.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
@ -489,6 +496,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
@ -500,14 +508,14 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.0;
MARKETING_VERSION = 0.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@ -518,6 +526,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
@ -529,13 +538,13 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.0;
MARKETING_VERSION = 0.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;

View 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>

View File

@ -48,6 +48,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
));
} else {
emit(state.copyWith(
status: AuthStatus.loaded,
isLoggedIn: false,
));
}

View File

@ -1,4 +1,4 @@
class Constants {
abstract class Constants {
static const String endUserAgreementLink =
'https://www.termsfeed.com/live/c1417f5c-a48b-4bd7-93b2-9cd4577bfc45';
static const String hackerNewsLogoLink =

View File

@ -22,8 +22,10 @@ class CustomRouter {
/// Nested routing for bottom navigation bar.
static Route onGenerateNestedRoute(RouteSettings settings) {
switch (settings.name) {
case HomeScreen.routeName:
return HomeScreen.route();
case StoryScreen.routeName:
return StoryScreen.route(settings.arguments! as StoryScreenArgs);
case SubmitScreen.routeName:
return SubmitScreen.route();
default:
return _errorRoute();
}

View File

@ -24,6 +24,13 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
final CacheRepository _cacheRepository;
final StoriesRepository _storiesRepository;
@override
void emit(CommentsState state) {
if (!isClosed) {
super.emit(state);
}
}
Future<void> init({
bool onlyShowTargetComment = false,
Comment? targetComment,
@ -63,11 +70,10 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
}
}
}
if (!isClosed) {
emit(state.copyWith(
status: CommentsStatus.loaded,
));
}
emit(state.copyWith(
status: CommentsStatus.loaded,
));
} else {
final comment = state.item as Comment;
@ -93,11 +99,9 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
}
}
if (!isClosed) {
emit(state.copyWith(
status: CommentsStatus.loaded,
));
}
emit(state.copyWith(
status: CommentsStatus.loaded,
));
}
}
@ -160,7 +164,7 @@ class CommentsCubit<T extends Item> extends Cubit<CommentsState> {
}
void _onCommentFetched(Comment? comment) {
if (comment != null && !isClosed) {
if (comment != null) {
_cacheService.cacheComment(comment);
emit(state.copyWith(comments: List.from(state.comments)..add(comment)));
}

View File

@ -9,5 +9,6 @@ export 'pin/pin_cubit.dart';
export 'post/post_cubit.dart';
export 'preference/preference_cubit.dart';
export 'search/search_cubit.dart';
export 'split_view/split_view_cubit.dart';
export 'submit/submit_cubit.dart';
export 'vote/vote_cubit.dart';

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

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

View File

@ -1,2 +1,4 @@
export 'date_time_extension.dart';
export 'object_extension.dart';
export 'state_extension.dart';
export 'widget_extension.dart';

View File

@ -0,0 +1,7 @@
import 'dart:developer' as dev;
extension ObjectExtension on Object {
void log({String identifier = ''}) {
dev.log('$identifier ${toString()}');
}
}

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

View File

@ -87,6 +87,10 @@ class HackiApp extends StatelessWidget {
lazy: false,
create: (context) => CacheCubit(),
),
BlocProvider<SplitViewCubit>(
lazy: false,
create: (context) => SplitViewCubit(),
),
],
child: AdaptiveTheme(
light: ThemeData(

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/item.dart';
@ -63,6 +65,12 @@ class Comment extends Item {
'score': score,
};
@override
String toString() {
final prettyString = const JsonEncoder.withIndent(' ').convert(this);
return 'Comment $prettyString';
}
@override
List<Object?> get props => [
id,

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/item.dart';
@ -96,6 +98,12 @@ class Story extends Item {
};
}
@override
String toString() {
final prettyString = const JsonEncoder.withIndent(' ').convert(this);
return 'Story $prettyString';
}
@override
List<Object?> get props => [
id,

View File

@ -1,3 +1,5 @@
import 'dart:convert';
class User {
User({
required this.about,
@ -26,4 +28,10 @@ class User {
final int delay;
final String id;
final int karma;
@override
String toString() {
final prettyString = const JsonEncoder.withIndent(' ').convert(this);
return 'User $prettyString';
}
}

View File

@ -18,7 +18,7 @@ import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/services/services.dart';
import 'package:hacki/utils/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:responsive_builder/responsive_builder.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@ -39,11 +39,6 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
final cacheService = locator.get<CacheService>();
final refreshControllerTop = RefreshController();
final refreshControllerNew = RefreshController();
final refreshControllerAsk = RefreshController();
final refreshControllerShow = RefreshController();
final refreshControllerJobs = RefreshController();
late final TabController tabController;
int currentIndex = 0;
@ -78,7 +73,7 @@ class _HomeScreenState extends State<HomeScreen>
@override
Widget build(BuildContext context) {
return BlocBuilder<PreferenceCubit, PreferenceState>(
final homeScreen = BlocBuilder<PreferenceCubit, PreferenceState>(
buildWhen: (previous, current) =>
previous.showComplexStoryTile != current.showComplexStoryTile,
builder: (context, preferenceState) {
@ -136,105 +131,113 @@ class _HomeScreenState extends State<HomeScreen>
child: Scaffold(
resizeToAvoidBottomInset: false,
appBar: PreferredSize(
preferredSize: const Size(0, 48),
preferredSize: const Size(0, 40),
child: Column(
children: [
SizedBox(
height: MediaQuery.of(context).padding.top,
height: MediaQuery.of(context).padding.top - 8,
),
TabBar(
isScrollable: true,
controller: tabController,
indicatorColor: Colors.orange,
tabs: [
Tab(
child: Text(
'TOP',
style: TextStyle(
fontSize: 14,
color: currentIndex == 0
? Colors.orange
: Colors.grey,
Theme(
data: ThemeData(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
primaryColor: Theme.of(context).primaryColor,
),
child: TabBar(
isScrollable: true,
controller: tabController,
indicatorColor: Colors.orange,
indicator: CircleTabIndicator(
color: Colors.orange, radius: 2),
indicatorPadding: const EdgeInsets.only(bottom: 8),
onTap: (_) {
HapticFeedback.selectionClick();
},
tabs: [
Tab(
child: Text(
'TOP',
style: TextStyle(
fontSize: currentIndex == 0 ? 14 : 10,
color: currentIndex == 0
? Colors.orange
: Colors.grey,
),
),
),
),
Tab(
child: Text(
'NEW',
style: TextStyle(
fontSize: 14,
color: currentIndex == 1
? Colors.orange
: Colors.grey,
Tab(
child: Text(
'NEW',
style: TextStyle(
fontSize: currentIndex == 1 ? 14 : 10,
color: currentIndex == 1
? Colors.orange
: Colors.grey,
),
),
),
),
Tab(
child: Text(
'ASK',
style: TextStyle(
fontSize: 14,
color: currentIndex == 2
? Colors.orange
: Colors.grey,
Tab(
child: Text(
'ASK',
style: TextStyle(
fontSize: currentIndex == 2 ? 14 : 10,
color: currentIndex == 2
? Colors.orange
: Colors.grey,
),
),
),
),
Tab(
child: Text(
'SHOW',
style: TextStyle(
fontSize: 13,
color: currentIndex == 3
? Colors.orange
: Colors.grey,
Tab(
child: Text(
'SHOW',
style: TextStyle(
fontSize: currentIndex == 3 ? 14 : 10,
color: currentIndex == 3
? Colors.orange
: Colors.grey,
),
),
),
),
Tab(
child: Text(
'JOBS',
style: TextStyle(
fontSize: 14,
color: currentIndex == 4
? Colors.orange
: Colors.grey,
Tab(
child: Text(
'JOBS',
style: TextStyle(
fontSize: currentIndex == 4 ? 14 : 10,
color: currentIndex == 4
? Colors.orange
: Colors.grey,
),
),
),
),
Tab(
child: DescribedFeatureOverlay(
barrierDismissible: false,
overflowMode: OverflowMode.extendBackground,
targetColor: Theme.of(context).primaryColor,
tapTarget: const Icon(
Icons.person,
size: 16,
color: Colors.white,
),
featureId: Constants.featureLogIn,
title: const Text(''),
description: const Text(
'Log in using your Hacker News account '
'to check out stories and comments you have '
'posted in the past, and get in-app '
'notification when there is new reply to '
'your comments or stories.',
style: TextStyle(fontSize: 16),
),
child: BlocBuilder<NotificationCubit,
NotificationState>(
builder: (context, state) {
if (state.unreadCommentsIds.isEmpty) {
return Icon(
Icons.person,
size: 16,
color: currentIndex == 5
? Colors.orange
: Colors.grey,
);
} else {
Tab(
child: DescribedFeatureOverlay(
barrierDismissible: false,
overflowMode: OverflowMode.extendBackground,
targetColor: Theme.of(context).primaryColor,
tapTarget: const Icon(
Icons.person,
size: 16,
color: Colors.white,
),
featureId: Constants.featureLogIn,
title: const Text(''),
description: const Text(
'Log in using your Hacker News account '
'to check out stories and comments you have '
'posted in the past, and get in-app '
'notification when there is new reply to '
'your comments or stories.',
style: TextStyle(fontSize: 16),
),
child: BlocBuilder<NotificationCubit,
NotificationState>(
buildWhen: (previous, current) =>
previous.unreadCommentsIds.length !=
current.unreadCommentsIds.length,
builder: (context, state) {
return Badge(
showBadge:
state.unreadCommentsIds.isNotEmpty,
borderRadius: BorderRadius.circular(100),
badgeContent: Container(
height: 3,
@ -245,18 +248,18 @@ class _HomeScreenState extends State<HomeScreen>
),
child: Icon(
Icons.person,
size: 16,
size: currentIndex == 5 ? 16 : 12,
color: currentIndex == 5
? Colors.orange
: Colors.grey,
),
);
}
},
},
),
),
),
),
],
],
),
),
],
),
@ -266,34 +269,34 @@ class _HomeScreenState extends State<HomeScreen>
controller: tabController,
children: [
StoriesListView(
key: const ValueKey(StoryType.top),
storyType: StoryType.top,
header: pinnedStories,
onStoryTapped: onStoryTapped,
refreshController: refreshControllerTop,
),
StoriesListView(
key: const ValueKey(StoryType.latest),
storyType: StoryType.latest,
header: pinnedStories,
onStoryTapped: onStoryTapped,
refreshController: refreshControllerNew,
),
StoriesListView(
key: const ValueKey(StoryType.ask),
storyType: StoryType.ask,
header: pinnedStories,
onStoryTapped: onStoryTapped,
refreshController: refreshControllerAsk,
),
StoriesListView(
key: const ValueKey(StoryType.show),
storyType: StoryType.show,
header: pinnedStories,
onStoryTapped: onStoryTapped,
refreshController: refreshControllerShow,
),
StoriesListView(
key: const ValueKey(StoryType.jobs),
storyType: StoryType.jobs,
header: pinnedStories,
onStoryTapped: onStoryTapped,
refreshController: refreshControllerJobs,
),
const ProfileScreen(),
],
@ -304,6 +307,61 @@ class _HomeScreenState extends State<HomeScreen>
);
},
);
return ScreenTypeLayout.builder(
mobile: (context) {
context.read<SplitViewCubit>().disableSplitView();
return homeScreen;
},
tablet: (context) {
return ResponsiveBuilder(
builder: (context, sizeInfo) {
context.read<SplitViewCubit>().enableSplitView();
var homeScreenWidth = 428.0;
if (sizeInfo.screenSize.width < homeScreenWidth * 2) {
homeScreenWidth = 345.0;
}
return Stack(
children: [
Positioned(
left: 0,
top: 0,
bottom: 0,
width: homeScreenWidth,
child: homeScreen,
),
Positioned(
right: 0,
top: 0,
bottom: 0,
left: homeScreenWidth,
child: BlocBuilder<SplitViewCubit, SplitViewState>(
buildWhen: (previous, current) =>
previous.storyScreenArgs != current.storyScreenArgs,
builder: (context, state) {
if (state.storyScreenArgs != null) {
return StoryScreen.build(state.storyScreenArgs!);
}
return Material(
child: Container(
color: Theme.of(context).canvasColor,
child: const Center(
child: Text('Tap on story tile to view comments.'),
),
),
);
},
),
),
],
);
},
);
},
);
}
void onStoryTapped(Story story) {
@ -311,14 +369,22 @@ class _HomeScreenState extends State<HomeScreen>
final useReader = context.read<PreferenceCubit>().state.useReader;
final offlineReading = context.read<StoriesBloc>().state.offlineReading;
final firstTimeReading = cacheService.isFirstTimeReading(story.id);
final splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
// If a story is a job story and it has a link to the job posting,
// it would be better to just navigate to the web page.
final isJobWithLink = story.type == 'job' && story.url.isNotEmpty;
if (!isJobWithLink) {
HackiApp.navigatorKey.currentState!.pushNamed(StoryScreen.routeName,
arguments: StoryScreenArgs(story: story));
final args = StoryScreenArgs(story: story);
if (splitViewEnabled) {
context.read<SplitViewCubit>().updateStoryScreenArgs(args);
} else {
HackiApp.navigatorKey.currentState?.pushNamed(
StoryScreen.routeName,
arguments: args,
);
}
}
if (!offlineReading &&

View File

@ -9,6 +9,7 @@ import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
@ -121,9 +122,8 @@ class _ProfileScreenState extends State<ProfileScreen>
},
onTap: (item) {
if (item is Story) {
HackiApp.navigatorKey.currentState!.pushNamed(
StoryScreen.routeName,
arguments: StoryScreenArgs(story: item));
goToStoryScreen(
args: StoryScreenArgs(story: item));
} else if (item is Comment) {
onCommentTapped(item);
}
@ -167,11 +167,8 @@ class _ProfileScreenState extends State<ProfileScreen>
onLoadMore: () {
context.read<FavCubit>().loadMore();
},
onTap: (story) {
HackiApp.navigatorKey.currentState!.pushNamed(
StoryScreen.routeName,
arguments: StoryScreenArgs(story: story));
},
onTap: (story) => goToStoryScreen(
args: StoryScreenArgs(story: story)),
);
},
),
@ -188,27 +185,38 @@ class _ProfileScreenState extends State<ProfileScreen>
top: 50,
child: Offstage(
offstage: pageType != _PageType.notification,
child: InboxView(
refreshController: refreshControllerNotification,
unreadCommentsIds:
notificationState.unreadCommentsIds,
comments: notificationState.comments,
onCommentTapped: (cmt) {
onCommentTapped(cmt, then: () {
context.read<NotificationCubit>().markAsRead(cmt);
});
},
onMarkAllAsReadTapped: () {
context.read<NotificationCubit>().markAllAsRead();
},
onLoadMore: () {
context.read<NotificationCubit>().loadMore();
},
onRefresh: () {
HapticFeedback.lightImpact();
context.read<NotificationCubit>().refresh();
},
),
child: notificationState.comments.isEmpty
? const CenteredMessageView(
content:
'New replies to your comments or stories '
'will show up here.',
)
: InboxView(
refreshController:
refreshControllerNotification,
unreadCommentsIds:
notificationState.unreadCommentsIds,
comments: notificationState.comments,
onCommentTapped: (cmt) {
onCommentTapped(cmt, then: () {
context
.read<NotificationCubit>()
.markAsRead(cmt);
});
},
onMarkAllAsReadTapped: () {
context
.read<NotificationCubit>()
.markAllAsRead();
},
onLoadMore: () {
context.read<NotificationCubit>().loadMore();
},
onRefresh: () {
HapticFeedback.lightImpact();
context.read<NotificationCubit>().refresh();
},
),
),
),
Positioned.fill(
@ -360,7 +368,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showAboutDialog(
context: context,
applicationName: 'Hacki',
applicationVersion: 'v0.2.0',
applicationVersion: 'v0.2.1',
applicationIcon: Image.asset(
Constants.hackiIconPath,
height: 50,
@ -447,18 +455,10 @@ class _ProfileScreenState extends State<ProfileScreen>
HackiApp.navigatorKey.currentState
?.pushNamed(SubmitScreen.routeName);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'You need to log in first.',
),
backgroundColor: Colors.orange,
action: SnackBarAction(
label: 'Log in',
textColor: Colors.black,
onPressed: onLoginTapped,
),
),
showSnackBar(
content: 'You need to log in first.',
label: 'Log in',
action: onLoginTapped,
);
}
},
@ -554,11 +554,8 @@ class _ProfileScreenState extends State<ProfileScreen>
void showThemeSettingDialog({bool useTrueDarkMode = false}) {
if (useTrueDarkMode) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Can't choose theme when using true dark mode."),
backgroundColor: Colors.orange,
),
showSnackBar(
content: "Can't choose theme when using true dark mode.",
);
return;
}
@ -601,18 +598,14 @@ class _ProfileScreenState extends State<ProfileScreen>
.fetchParentStoryWithComments(id: comment.parent)
.then((tuple) {
if (tuple != null && mounted) {
HackiApp.navigatorKey.currentState!
.pushNamed(
StoryScreen.routeName,
arguments: StoryScreenArgs(
story: tuple.item1,
targetComments: tuple.item2.isEmpty
? [comment]
: [comment, ...tuple.item2],
onlyShowTargetComment: true,
),
)
.then((_) => then?.call());
goToStoryScreen(
args: StoryScreenArgs(
story: tuple.item1,
targetComments:
tuple.item2.isEmpty ? [comment] : [comment, ...tuple.item2],
onlyShowTargetComment: true,
),
)?.then((_) => then?.call());
}
});
});
@ -630,12 +623,7 @@ class _ProfileScreenState extends State<ProfileScreen>
listener: (context, state) {
if (state.isLoggedIn) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Logged in successfully!'),
backgroundColor: Colors.orange,
),
);
showSnackBar(content: 'Logged in successfully!');
}
},
builder: (context, state) {
@ -759,7 +747,10 @@ class _ProfileScreenState extends State<ProfileScreen>
child: ButtonBar(
children: [
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () {
Navigator.pop(context);
context.read<AuthBloc>().add(AuthInitialize());
},
child: const Text(
'Cancel',
style: TextStyle(

View File

@ -104,6 +104,13 @@ class InboxView extends StatelessWidget {
? textColor
: Colors.grey,
),
linkStyle: TextStyle(
color:
unreadCommentsIds.contains(e.id)
? Colors.orange
: Colors.orange
.withOpacity(0.6),
),
maxLines: 4,
onOpen: (link) =>
LinkUtil.launchUrl(link.url),

View File

@ -1,3 +1,4 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/blocs/blocs.dart';
@ -22,6 +23,22 @@ class OfflineListTile extends StatelessWidget {
builder: (context, state) {
final downloading =
state.downloadStatus == StoriesDownloadStatus.downloading;
final downloaded =
state.downloadStatus == StoriesDownloadStatus.finished;
final trailingWidget = () {
if (downloading) {
return const SizedBox(
height: 24,
width: 24,
child: CustomCircularProgressIndicator(),
);
} else if (downloaded) {
return const Icon(Icons.check_circle);
}
return const Icon(Icons.download);
}();
return ListTile(
title: Text(
downloading ? 'Downloading All Stories...' : 'Download All Stories',
@ -30,17 +47,15 @@ class OfflineListTile extends StatelessWidget {
'download all latest stories that have at least one comment '
"for offline reading. (web page won't be downloaded)",
),
trailing: downloading
? const SizedBox(
height: 24,
width: 24,
child: CustomCircularProgressIndicator(),
)
: const Icon(Icons.download),
trailing: trailingWidget,
isThreeLine: true,
onTap: () {
Wakelock.enable();
context.read<StoriesBloc>().add(StoriesDownload());
Connectivity().checkConnectivity().then((res) {
if (res != ConnectivityResult.none) {
Wakelock.enable();
context.read<StoriesBloc>().add(StoriesDownload());
}
});
},
);
},

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/main.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/screens/screens.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/utils/utils.dart';
@ -31,6 +31,7 @@ class _SearchScreenState extends State<SearchScreen> {
},
builder: (context, state) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -100,25 +101,24 @@ class _SearchScreenState extends State<SearchScreen> {
child: ListView(
children: [
...state.results
.map((e) => [
FadeIn(
child: StoryTile(
showWebPreview:
prefState.showComplexStoryTile,
story: e,
onTap: () {
HackiApp.navigatorKey.currentState!
.pushNamed(
StoryScreen.routeName,
arguments: StoryScreenArgs(
story: e));
}),
),
if (!prefState.showComplexStoryTile)
const Divider(
height: 0,
.map(
(e) => [
FadeIn(
child: StoryTile(
showWebPreview:
prefState.showComplexStoryTile,
story: e,
onTap: () => goToStoryScreen(
args: StoryScreenArgs(story: e),
),
])
),
),
if (!prefState.showComplexStoryTile)
const Divider(
height: 0,
),
],
)
.expand((e) => e)
.toList(),
const SizedBox(

View File

@ -1,5 +1,6 @@
import 'dart:math';
import 'package:equatable/equatable.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@ -13,6 +14,7 @@ import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/main.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
@ -30,8 +32,8 @@ enum _MenuAction {
cancel,
}
class StoryScreenArgs {
StoryScreenArgs({
class StoryScreenArgs extends Equatable {
const StoryScreenArgs({
required this.story,
this.onlyShowTargetComment = false,
this.targetComments,
@ -40,12 +42,22 @@ class StoryScreenArgs {
final Story story;
final bool onlyShowTargetComment;
final List<Comment>? targetComments;
@override
List<Object?> get props => [
story,
onlyShowTargetComment,
targetComments,
];
}
class StoryScreen extends StatefulWidget {
const StoryScreen(
{Key? key, required this.story, required this.parentComments})
: super(key: key);
const StoryScreen({
Key? key,
this.splitViewEnabled = false,
required this.story,
required this.parentComments,
}) : super(key: key);
static const String routeName = '/story';
@ -78,6 +90,35 @@ class StoryScreen extends StatefulWidget {
);
}
static Widget build(StoryScreenArgs args) {
return MultiBlocProvider(
key: ValueKey(args),
providers: [
BlocProvider<PostCubit>(
create: (context) => PostCubit(),
),
BlocProvider<CommentsCubit>(
create: (context) => CommentsCubit<Story>(
offlineReading: context.read<StoriesBloc>().state.offlineReading,
item: args.story,
)..init(
onlyShowTargetComment: args.onlyShowTargetComment,
targetComment: args.targetComments?.last,
),
),
BlocProvider<EditCubit>(
create: (context) => EditCubit(),
),
],
child: StoryScreen(
story: args.story,
parentComments: args.targetComments ?? [],
splitViewEnabled: true,
),
);
}
final bool splitViewEnabled;
final Story story;
final List<Comment> parentComments;
@ -166,16 +207,12 @@ class _StoryScreenState extends State<StoryScreen> {
editCubit.onReplySubmittedSuccessfully();
context.read<PostCubit>().reset();
} else if (postState.status == PostStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
'Something went wrong...${(sadFaces..shuffle()).first}',
),
backgroundColor: Colors.orange,
action: SnackBarAction(
label: 'Okay',
onPressed: () =>
ScaffoldMessenger.of(context).hideCurrentSnackBar()),
));
showSnackBar(
content:
'Something went wrong...${(sadFaces..shuffle()).first}',
label: 'Okay',
action: ScaffoldMessenger.of(context).hideCurrentSnackBar,
);
context.read<PostCubit>().reset();
}
},
@ -190,6 +227,263 @@ class _StoryScreenState extends State<StoryScreen> {
}
},
builder: (context, state) {
final mainView = SmartRefresher(
scrollController: scrollController,
enablePullUp: !state.onlyShowTargetComment,
enablePullDown: !state.onlyShowTargetComment,
header: WaterDropMaterialHeader(
backgroundColor: Colors.orange,
offset: topPadding,
),
footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading,
builder: (context, mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = const Text('');
} else if (mode == LoadStatus.loading) {
body = const Text('');
} else if (mode == LoadStatus.failed) {
body = const Text(
'',
);
} else if (mode == LoadStatus.canLoading) {
body = const Text(
'',
);
} else {
body = const Text('');
}
return SizedBox(
height: 55,
child: Center(child: body),
);
},
),
controller: refreshController,
onRefresh: () {
HapticFeedback.lightImpact();
locator.get<CacheService>().resetComments();
context.read<CommentsCubit>().refresh();
},
onLoading: () {},
child: ListView(
primary: false,
children: [
SizedBox(
height: topPadding,
),
if (!widget.splitViewEnabled)
const Padding(
padding: EdgeInsets.only(bottom: 6),
child: OfflineBanner(),
),
Slidable(
startActionPane: ActionPane(
motion: const BehindMotion(),
children: [
SlidableAction(
onPressed: (_) {
HapticFeedback.lightImpact();
if (widget.story !=
context.read<EditCubit>().state.replyingTo) {
commentEditingController.clear();
}
editCubit.onReplyTapped(widget.story);
focusNode.requestFocus();
},
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
icon: Icons.message,
),
SlidableAction(
onPressed: (_) => onMorePressed(widget.story),
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
icon: Icons.more_horiz,
),
],
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 6,
right: 6,
),
child: Row(
children: [
Text(
widget.story.by,
style: const TextStyle(
color: Colors.orange,
),
),
const Spacer(),
Text(
widget.story.postedDate,
style: const TextStyle(
color: Colors.grey,
),
),
],
),
),
InkWell(
onTap: () => LinkUtil.launchUrl(
widget.story.url,
useReader: context
.read<PreferenceCubit>()
.state
.useReader,
),
child: Padding(
padding: const EdgeInsets.only(
left: 6,
right: 6,
bottom: 12,
top: 12,
),
child: Text(
widget.story.title,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
color: widget.story.url.isNotEmpty
? Colors.orange
: null,
),
),
),
),
if (widget.story.text.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
child: SelectableHtml(
data: widget.story.text,
style: {
'body': Style(
fontSize: FontSize(
MediaQuery.of(context).textScaleFactor *
15,
),
),
'a': Style(
fontSize: FontSize(
MediaQuery.of(context).textScaleFactor *
15,
),
color: Colors.orange,
),
},
onLinkTap: (link, _, __, ___) =>
LinkUtil.launchUrl(link ?? ''),
),
),
],
),
),
if (widget.story.text.isNotEmpty)
const SizedBox(
height: 8,
),
const Divider(
height: 0,
),
if (state.onlyShowTargetComment) ...[
TextButton(
onPressed: () =>
context.read<CommentsCubit>().loadAll(widget.story),
child: const Text('View all comments'),
),
const Divider(
height: 0,
),
],
if (state.comments.isEmpty &&
state.status == CommentsStatus.loaded) ...[
const SizedBox(
height: 240,
),
const Center(
child: Text(
'Nothing yet',
style: TextStyle(color: Colors.grey),
),
),
],
...state.comments.map(
(e) => FadeIn(
child: CommentTile(
comment: e,
onlyShowTargetComment: state.onlyShowTargetComment,
targetComments: widget.parentComments.sublist(
0, max(widget.parentComments.length - 1, 0)),
myUsername:
authState.isLoggedIn ? authState.username : null,
onReplyTapped: (cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) {
return;
}
if (cmt !=
context.read<EditCubit>().state.replyingTo) {
commentEditingController.clear();
}
editCubit.onReplyTapped(cmt);
focusNode.requestFocus();
},
onEditTapped: (cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) {
return;
}
commentEditingController.clear();
editCubit.onEditTapped(cmt);
focusNode.requestFocus();
},
onMoreTapped: onMorePressed,
onStoryLinkTapped: (link) {
final regex = RegExp(r'\d+$');
final match = regex.stringMatch(link) ?? '';
final id = int.tryParse(match);
if (id != null) {
throttle.run(() {
locator
.get<StoriesRepository>()
.fetchParentStory(id: id)
.then((story) {
if (mounted) {
if (story != null) {
HackiApp.navigatorKey.currentState!
.pushNamed(
StoryScreen.routeName,
arguments:
StoryScreenArgs(story: story),
);
} else {}
}
});
});
} else {
LinkUtil.launchUrl(link);
}
},
),
),
),
const SizedBox(
height: 240,
),
],
),
);
return BlocListener<EditCubit, EditState>(
listenWhen: (previous, current) {
return previous.replyingTo != current.replyingTo ||
@ -211,297 +505,67 @@ class _StoryScreenState extends State<StoryScreen> {
commentEditingController.clear();
}
},
child: Scaffold(
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: true,
appBar: AppBar(
backgroundColor:
Theme.of(context).canvasColor.withOpacity(0.6),
elevation: 0,
actions: [
ScrollUpIconButton(
scrollController: scrollController,
),
PinIconButton(story: widget.story),
FavIconButton(storyId: widget.story.id),
LinkIconButton(storyId: widget.story.id),
],
),
body: SmartRefresher(
scrollController: scrollController,
enablePullUp: !state.onlyShowTargetComment,
enablePullDown: !state.onlyShowTargetComment,
header: WaterDropMaterialHeader(
backgroundColor: Colors.orange,
offset: topPadding,
),
footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading,
builder: (context, mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = const Text('');
} else if (mode == LoadStatus.loading) {
body = const Text('');
} else if (mode == LoadStatus.failed) {
body = const Text(
'',
);
} else if (mode == LoadStatus.canLoading) {
body = const Text(
'',
);
} else {
body = const Text('');
}
return SizedBox(
height: 55,
child: Center(child: body),
);
},
),
controller: refreshController,
onRefresh: () {
HapticFeedback.lightImpact();
locator.get<CacheService>().resetComments();
context.read<CommentsCubit>().refresh();
},
onLoading: () {},
child: ListView(
primary: false,
children: [
SizedBox(
height: topPadding,
),
const Padding(
padding: EdgeInsets.only(bottom: 6),
child: OfflineBanner(),
),
Slidable(
startActionPane: ActionPane(
motion: const BehindMotion(),
children: [
SlidableAction(
onPressed: (_) {
HapticFeedback.lightImpact();
if (widget.story !=
context
.read<EditCubit>()
.state
.replyingTo) {
commentEditingController.clear();
}
editCubit.onReplyTapped(widget.story);
focusNode.requestFocus();
},
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
icon: Icons.message,
),
SlidableAction(
onPressed: (_) => onMorePressed(widget.story),
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
icon: Icons.more_horiz,
),
],
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 6,
right: 6,
),
child: Row(
children: [
Text(
widget.story.by,
style: const TextStyle(
color: Colors.orange,
),
),
const Spacer(),
Text(
widget.story.postedDate,
style: const TextStyle(
color: Colors.grey,
),
),
],
),
),
InkWell(
onTap: () => LinkUtil.launchUrl(
widget.story.url,
useReader: context
.read<PreferenceCubit>()
.state
.useReader,
),
child: Padding(
padding: const EdgeInsets.only(
left: 6,
right: 6,
bottom: 12,
top: 12,
),
child: Text(
widget.story.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold),
),
),
),
if (widget.story.text.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
child: SelectableHtml(
data: widget.story.text,
onLinkTap: (link, _, __, ___) =>
LinkUtil.launchUrl(link ?? ''),
),
),
],
),
),
if (widget.story.text.isNotEmpty)
const SizedBox(
height: 8,
),
const Divider(
height: 0,
),
if (state.onlyShowTargetComment) ...[
TextButton(
onPressed: () => context
.read<CommentsCubit>()
.loadAll(widget.story),
child: const Text('View all comments'),
),
const Divider(
height: 0,
),
],
if (state.comments.isEmpty &&
state.status == CommentsStatus.loaded) ...[
const SizedBox(
height: 240,
),
const Center(
child: Text(
'Nothing yet',
style: TextStyle(color: Colors.grey),
child: widget.splitViewEnabled
? Material(
child: Stack(
children: [
Positioned.fill(
child: mainView,
),
),
],
...state.comments.map(
(e) => FadeIn(
child: CommentTile(
comment: e,
onlyShowTargetComment:
state.onlyShowTargetComment,
targetComments: widget.parentComments.sublist(
0, max(widget.parentComments.length - 1, 0)),
myUsername: authState.isLoggedIn
? authState.username
: null,
onReplyTapped: (cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) {
return;
}
if (cmt !=
context
.read<EditCubit>()
.state
.replyingTo) {
Positioned(
top: 0,
left: 0,
right: 0,
child: CustomAppBar(
backgroundColor: Theme.of(context)
.canvasColor
.withOpacity(0.6),
story: widget.story,
scrollController: scrollController,
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: ReplyBox(
splitViewEnabled: true,
focusNode: focusNode,
textEditingController: commentEditingController,
onSendTapped: onSendTapped,
onCloseTapped: () {
editCubit.onReplyBoxClosed();
commentEditingController.clear();
}
editCubit.onReplyTapped(cmt);
focusNode.requestFocus();
},
onEditTapped: (cmt) {
HapticFeedback.lightImpact();
if (cmt.deleted || cmt.dead) {
return;
}
commentEditingController.clear();
editCubit.onEditTapped(cmt);
focusNode.requestFocus();
},
onMoreTapped: onMorePressed,
onStoryLinkTapped: (link) {
final regex = RegExp(r'\d+$');
final match = regex.stringMatch(link) ?? '';
final id = int.tryParse(match);
if (id != null) {
throttle.run(() {
locator
.get<StoriesRepository>()
.fetchParentStory(id: id)
.then((story) {
if (mounted) {
if (story != null) {
HackiApp.navigatorKey.currentState!
.pushNamed(
StoryScreen.routeName,
arguments:
StoryScreenArgs(story: story),
);
} else {}
}
});
});
} else {
LinkUtil.launchUrl(link);
}
},
focusNode.unfocus();
},
onChanged: editCubit.onTextChanged,
),
),
),
],
),
const SizedBox(
height: 240,
)
: Scaffold(
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: true,
appBar: CustomAppBar(
backgroundColor:
Theme.of(context).canvasColor.withOpacity(0.6),
story: widget.story,
scrollController: scrollController,
),
],
),
),
bottomSheet: Offstage(
offstage: !editCubit.state.showReplyBox,
child: BlocBuilder<PostCubit, PostState>(
builder: (context, postState) {
return BlocBuilder<EditCubit, EditState>(
buildWhen: (previous, current) =>
previous.itemBeingEdited !=
current.itemBeingEdited ||
previous.replyingTo != current.replyingTo,
builder: (context, editState) {
return ReplyBox(
focusNode: focusNode,
textEditingController: commentEditingController,
editing: editState.itemBeingEdited,
replyingTo: editState.replyingTo,
isLoading: postState.status == PostStatus.loading,
onSendTapped: onSendTapped,
onCloseTapped: () {
editCubit.onReplyBoxClosed();
commentEditingController.clear();
focusNode.unfocus();
},
onChanged: editCubit.onTextChanged,
);
body: mainView,
bottomSheet: ReplyBox(
focusNode: focusNode,
textEditingController: commentEditingController,
onSendTapped: onSendTapped,
onCloseTapped: () {
editCubit.onReplyBoxClosed();
commentEditingController.clear();
focusNode.unfocus();
},
);
},
),
),
),
onChanged: editCubit.onTextChanged,
),
),
);
},
),
@ -546,7 +610,8 @@ class _StoryScreenState extends State<StoryScreen> {
} else if (voteState.status == VoteStatus.failureNotLoggedIn) {
showSnackBar(
content: 'Not logged in, no voting! (;O´)o',
withLoginAction: true,
action: onLoginTapped,
label: 'Log in',
);
} else if (voteState.status == VoteStatus.failureBeHumble) {
showSnackBar(content: 'No voting on your own post! (;O´)o');
@ -649,6 +714,7 @@ class _StoryScreenState extends State<StoryScreen> {
onBlockTapped(item, isBlocked);
break;
case _MenuAction.cancel:
break;
}
}
});
@ -946,7 +1012,10 @@ class _StoryScreenState extends State<StoryScreen> {
child: ButtonBar(
children: [
TextButton(
onPressed: () => Navigator.pop(context),
onPressed: () {
Navigator.pop(context);
context.read<AuthBloc>().add(AuthInitialize());
},
child: const Text(
'Cancel',
style: TextStyle(
@ -993,22 +1062,4 @@ class _StoryScreenState extends State<StoryScreen> {
},
);
}
void showSnackBar({required String content, bool withLoginAction = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
content,
),
backgroundColor: Colors.orange,
action: withLoginAction
? SnackBarAction(
label: 'Log in',
textColor: Colors.black,
onPressed: onLoginTapped,
)
: null,
),
);
}
}

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

View File

@ -1,31 +1,29 @@
import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:hacki/models/item.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/utils/link_util.dart';
class ReplyBox extends StatefulWidget {
const ReplyBox({
Key? key,
this.splitViewEnabled = false,
required this.focusNode,
required this.textEditingController,
required this.replyingTo,
required this.editing,
required this.onSendTapped,
required this.onCloseTapped,
required this.onChanged,
required this.isLoading,
}) : super(key: key);
final bool splitViewEnabled;
final FocusNode focusNode;
final TextEditingController textEditingController;
final Item? replyingTo;
final Item? editing;
final VoidCallback onSendTapped;
final VoidCallback onCloseTapped;
final ValueChanged<String> onChanged;
final bool isLoading;
@override
_ReplyBoxState createState() => _ReplyBoxState();
@ -34,219 +32,266 @@ class ReplyBox extends StatefulWidget {
class _ReplyBoxState extends State<ReplyBox> {
bool expanded = false;
double? expandedHeight;
double? topPadding;
@override
Widget build(BuildContext context) {
expandedHeight ??= MediaQuery.of(context).size.height;
topPadding ??= MediaQuery.of(context).padding.top + kToolbarHeight;
return AnimatedContainer(
height: expanded ? expandedHeight : 100,
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: expanded ? Colors.transparent : Colors.black54,
offset: const Offset(0, 20), //(x,y)
blurRadius: 40,
),
],
),
child: Material(
child: Column(
children: [
AnimatedContainer(
height: expanded ? topPadding : 0,
duration: const Duration(milliseconds: 200),
),
Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Text(
widget.replyingTo == null
? 'Editing'
: 'Replying '
'${widget.replyingTo?.by}',
style: const TextStyle(color: Colors.grey),
),
),
const Spacer(),
if (!widget.isLoading) ...[
...[
if (widget.replyingTo != null)
AnimatedOpacity(
opacity: expanded ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: IconButton(
key: const Key('quote'),
icon: const Icon(
FeatherIcons.code,
color: Colors.orange,
size: 18,
),
onPressed: expanded ? showTextPopup : null,
),
),
IconButton(
key: const Key('expand'),
icon: Icon(
expanded
? FeatherIcons.minimize2
: FeatherIcons.maximize2,
color: Colors.orange,
size: 18,
),
onPressed: () {
setState(() {
expanded = !expanded;
});
},
),
],
IconButton(
key: const Key('close'),
icon: const Icon(
Icons.close,
color: Colors.orange,
),
onPressed: () {
widget.onCloseTapped();
expanded = false;
},
),
],
if (widget.isLoading)
const Padding(
padding: EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
child: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: Colors.orange,
strokeWidth: 2,
),
),
)
else
IconButton(
key: const Key('send'),
icon: const Icon(
Icons.send,
color: Colors.orange,
),
onPressed: () {
widget.onSendTapped();
expanded = false;
},
),
],
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextField(
focusNode: widget.focusNode,
controller: widget.textEditingController,
maxLines: 100,
decoration: const InputDecoration(
alignLabelWithHint: true,
contentPadding: EdgeInsets.zero,
hintText: '...',
hintStyle: TextStyle(
color: Colors.grey,
),
focusedBorder: InputBorder.none,
border: InputBorder.none,
),
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
onChanged: widget.onChanged,
),
),
),
],
),
),
);
}
return BlocBuilder<EditCubit, EditState>(
buildWhen: (previous, current) =>
previous.showReplyBox != current.showReplyBox,
builder: (context, editState) {
return Offstage(
offstage: !editState.showReplyBox,
child: BlocBuilder<PostCubit, PostState>(
builder: (context, postState) {
return BlocBuilder<EditCubit, EditState>(
buildWhen: (previous, current) =>
previous.itemBeingEdited != current.itemBeingEdited ||
previous.replyingTo != current.replyingTo,
builder: (context, editState) {
final replyingTo = editState.replyingTo;
final isLoading = postState.status == PostStatus.loading;
void showTextPopup() {
showDialog<void>(
context: context,
barrierDismissible: true,
builder: (_) {
return Container(
margin: const EdgeInsets.only(
left: 12,
right: 12,
top: 64,
bottom: 64,
),
color: Theme.of(context).canvasColor,
child: Padding(
padding: const EdgeInsets.only(
left: 12,
right: 6,
top: 6,
bottom: 12,
),
child: Column(
children: [
Material(
child: Row(
children: [
Text(
widget.replyingTo?.by ?? '',
style: const TextStyle(color: Colors.grey),
return Padding(
padding: EdgeInsets.only(
bottom: expanded
? 0
: widget.splitViewEnabled
? MediaQuery.of(context).viewInsets.bottom
: 0,
),
child: AnimatedContainer(
height: expanded ? expandedHeight : 100,
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
boxShadow: [
if (!context.read<SplitViewCubit>().state.enabled)
BoxShadow(
color: expanded
? Colors.transparent
: Colors.black26,
blurRadius: 40,
),
],
),
const Spacer(),
TextButton(
child: const Text('Copy All'),
onPressed: () => FlutterClipboard.copy(
widget.replyingTo?.text ?? '',
),
),
IconButton(
icon: const Icon(
Icons.close,
color: Colors.orange,
size: 18,
),
onPressed: () => Navigator.pop(context),
),
const SizedBox(
width: 6,
)
],
),
),
Expanded(
child: Scrollbar(
isAlwaysShown: true,
child: Padding(
padding: const EdgeInsets.only(right: 6),
child: SingleChildScrollView(
child: SelectableLinkify(
scrollPhysics: const NeverScrollableScrollPhysics(),
text: widget.replyingTo?.text ?? '',
onOpen: (link) => LinkUtil.launchUrl(link.url),
child: Material(
child: Column(
children: [
if (context.read<SplitViewCubit>().state.enabled)
const Divider(
height: 0,
),
AnimatedContainer(
height: expanded ? 36 : 0,
duration: const Duration(milliseconds: 200),
),
Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Text(
replyingTo == null
? 'Editing'
: 'Replying '
'${replyingTo.by}',
style: const TextStyle(color: Colors.grey),
),
),
const Spacer(),
if (!isLoading) ...[
...[
if (replyingTo != null)
AnimatedOpacity(
opacity: expanded ? 1 : 0,
duration:
const Duration(milliseconds: 300),
child: IconButton(
key: const Key('quote'),
icon: const Icon(
FeatherIcons.code,
color: Colors.orange,
size: 18,
),
onPressed:
expanded ? showTextPopup : null,
),
),
IconButton(
key: const Key('expand'),
icon: Icon(
expanded
? FeatherIcons.minimize2
: FeatherIcons.maximize2,
color: Colors.orange,
size: 18,
),
onPressed: () {
setState(() {
expanded = !expanded;
});
},
),
],
IconButton(
key: const Key('close'),
icon: const Icon(
Icons.close,
color: Colors.orange,
),
onPressed: () {
widget.onCloseTapped();
expanded = false;
},
),
],
if (isLoading)
const Padding(
padding: EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
child: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: Colors.orange,
strokeWidth: 2,
),
),
)
else
IconButton(
key: const Key('send'),
icon: const Icon(
Icons.send,
color: Colors.orange,
),
onPressed: () {
widget.onSendTapped();
expanded = false;
},
),
],
),
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16),
child: TextField(
focusNode: widget.focusNode,
controller: widget.textEditingController,
maxLines: 100,
decoration: const InputDecoration(
alignLabelWithHint: true,
contentPadding: EdgeInsets.zero,
hintText: '...',
hintStyle: TextStyle(
color: Colors.grey,
),
focusedBorder: InputBorder.none,
border: InputBorder.none,
),
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
onChanged: widget.onChanged,
),
),
),
],
),
),
),
),
),
],
),
);
},
);
},
),
);
},
);
}
void showTextPopup() {
final replyingTo = context.read<EditCubit>().state.replyingTo;
showDialog<void>(
context: context,
barrierDismissible: true,
builder: (_) {
return AlertDialog(
insetPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 24,
),
contentPadding: EdgeInsets.zero,
content: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500,
maxHeight: 500,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 12,
top: 6,
),
child: Row(
children: [
Text(
replyingTo?.by ?? '',
style: const TextStyle(color: Colors.grey),
),
const Spacer(),
TextButton(
child: const Text('Copy All'),
onPressed: () => FlutterClipboard.copy(
replyingTo?.text ?? '',
).then((_) => HapticFeedback.selectionClick()),
),
IconButton(
icon: const Icon(
Icons.close,
color: Colors.orange,
size: 18,
),
onPressed: () => Navigator.pop(context),
),
],
),
),
Expanded(
child: Scrollbar(
isAlwaysShown: true,
child: Padding(
padding: const EdgeInsets.only(
left: 12,
right: 6,
top: 6,
),
child: SingleChildScrollView(
child: SelectableHtml(
scrollPhysics: const NeverScrollableScrollPhysics(),
data: replyingTo?.text ?? '',
style: {
'a': Style(
color: Colors.orange,
),
},
onLinkTap: (link, _, __, ___) =>
LinkUtil.launchUrl(link!),
),
),
),
),
),
],
),
));
},
);
}
}

View File

@ -1,3 +1,4 @@
export 'custom_app_bar.dart';
export 'fav_icon_button.dart';
export 'link_icon_button.dart';
export 'pin_icon_button.dart';

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
class SubmitScreen extends StatefulWidget {
const SubmitScreen({Key? key}) : super(key: key);
@ -199,13 +200,4 @@ class _SubmitScreenState extends State<SubmitScreen> {
(textEditingController.text.isNotEmpty ||
urlEditingController.text.isNotEmpty);
}
void showSnackBar({required String content}) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
content,
),
backgroundColor: Colors.orange,
));
}
}

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

View File

@ -189,6 +189,17 @@ class CommentTile extends StatelessWidget {
child: SelectableLinkify(
key: ObjectKey(comment),
text: comment.text,
style: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
15,
),
linkStyle: TextStyle(
fontSize: MediaQuery.of(context)
.textScaleFactor *
15,
color: Colors.orange,
),
onOpen: (link) {
if (link.url.contains(
'news.ycombinator.com/item')) {

View File

@ -133,6 +133,9 @@ class ItemsListView<T extends Item> extends StatelessWidget {
child: Linkify(
text: e.text,
maxLines: 4,
linkStyle: const TextStyle(
color: Colors.orange,
),
onOpen: (link) =>
LinkUtil.launchUrl(link.url),
),

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart';
@ -184,7 +185,12 @@ class _LinkPreviewState extends State<LinkPreview> {
@override
Widget build(BuildContext context) {
final _height = (MediaQuery.of(context).size.height) * 0.15;
final screenWidth = MediaQuery.of(context).size.width;
final showSmallerPreviewPic =
Platform.isIOS && screenWidth > 428.0 && screenWidth < 850;
final _height = showSmallerPreviewPic
? 100.0
: (MediaQuery.of(context).size.height * 0.14).clamp(118.0, 140.0);
final loadingWidget = widget.placeholderWidget ??
Container(
height: _height,

View File

@ -83,22 +83,23 @@ class LinkView extends StatelessWidget {
child: Row(
children: <Widget>[
if (showMultiMedia)
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(
right: 5,
top: 5,
bottom: 5,
),
Padding(
padding: const EdgeInsets.only(
right: 5,
top: 5,
bottom: 5,
),
child: SizedBox(
height: layoutHeight,
width: layoutHeight,
child: (imageUri?.isEmpty ?? true) && imagePath != null
? Image.asset(
imagePath!,
fit: isIcon ? BoxFit.scaleDown : BoxFit.cover,
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
)
: CachedNetworkImage(
imageUrl: imageUri!,
fit: isIcon ? BoxFit.scaleDown : BoxFit.cover,
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
memCacheHeight: layoutHeight.toInt() * 4,
errorWidget: (context, _, dynamic __) {
return Image.asset(

View File

@ -7,22 +7,37 @@ import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class StoriesListView extends StatelessWidget {
class StoriesListView extends StatefulWidget {
const StoriesListView({
Key? key,
required this.storyType,
required this.header,
required this.onStoryTapped,
required this.refreshController,
}) : super(key: key);
final StoryType storyType;
final Widget header;
final ValueChanged<Story> onStoryTapped;
final RefreshController refreshController;
@override
State<StoriesListView> createState() => _StoriesListViewState();
}
class _StoriesListViewState extends State<StoriesListView> {
final refreshController = RefreshController();
@override
void dispose() {
super.dispose();
refreshController.dispose();
}
@override
Widget build(BuildContext context) {
final storyType = widget.storyType;
final header = widget.header;
final onStoryTapped = widget.onStoryTapped;
return BlocBuilder<PreferenceCubit, PreferenceState>(
buildWhen: (previous, current) =>
previous.showComplexStoryTile != current.showComplexStoryTile,

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/config/constants.dart';
@ -26,7 +28,12 @@ class StoryTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (showWebPreview) {
final height = (MediaQuery.of(context).size.height) * 0.15;
final screenWidth = MediaQuery.of(context).size.width;
final showSmallerPreviewPic =
Platform.isIOS && screenWidth > 428.0 && screenWidth < 850;
final height = showSmallerPreviewPic
? 100.0
: (MediaQuery.of(context).size.height * 0.14).clamp(118.0, 140.0);
if (story.url.isNotEmpty) {
return TapDownWrapper(
@ -47,19 +54,16 @@ class StoryTile extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(
right: 5,
bottom: 5,
top: 5,
),
child: Container(
height: height,
width: height,
color: Colors.white,
),
Padding(
padding: const EdgeInsets.only(
right: 5,
bottom: 5,
top: 5,
),
child: Container(
height: height,
width: height,
color: Colors.white,
),
),
Expanded(
@ -119,7 +123,7 @@ class StoryTile extends StatelessWidget {
backgroundColor: Colors.transparent,
borderRadius: 0,
removeElevation: true,
bodyMaxLines: 4,
bodyMaxLines: height == 100 ? 3 : 4,
errorTitle: story.title,
titleStyle: TextStyle(
color: wasRead
@ -149,7 +153,7 @@ class StoryTile extends StatelessWidget {
onTap: (_) {},
url: '',
imagePath: Constants.hackerNewsLogoPath,
bodyMaxLines: 4,
bodyMaxLines: height == 100 ? 3 : 4,
titleTextStyle: TextStyle(
color: wasRead
? Colors.grey[500]

View File

@ -1,3 +1,4 @@
export 'circle_tab_indicator.dart';
export 'comment_tile.dart';
export 'custom_circular_progress_indicator.dart';
export 'items_list_view.dart';

View File

@ -1,6 +1,6 @@
name: hacki
description: A Hacker News reader.
version: 0.2.0+31
version: 0.2.1+32
publish_to: none
environment:
@ -13,6 +13,7 @@ dependencies:
cached_network_image: ^3.2.0
clipboard: ^0.1.3
collection:
connectivity_plus: ^2.2.1
dio: ^4.0.4
equatable: 2.0.3
fast_gbk: ^1.0.0
@ -38,6 +39,7 @@ dependencies:
path: ^1.8.0
path_provider: ^2.0.8
pull_to_refresh: ^2.0.0
responsive_builder: ^0.4.2
sembast: ^3.1.1+1
shared_preferences: ^2.0.11
shimmer: ^2.0.0