mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
6c8047ebac | |||
00a0135867 | |||
1db7be7a2c | |||
ff400f9c40 |
@ -34,15 +34,6 @@ abstract class Constants {
|
|||||||
static const String logFilename = 'hacki_log.txt';
|
static const String logFilename = 'hacki_log.txt';
|
||||||
static const String previousLogFileName = 'old_hacki_log.txt';
|
static const String previousLogFileName = 'old_hacki_log.txt';
|
||||||
|
|
||||||
/// Feature ids for feature discovery.
|
|
||||||
static const String featureAddStoryToFavList = 'add_story_to_fav_list';
|
|
||||||
static const String featureOpenStoryInWebView = 'open_story_in_web_view';
|
|
||||||
static const String featureLogIn = 'log_in';
|
|
||||||
static const String featurePinToTop = 'pin_to_top';
|
|
||||||
static const String featureJumpUpButton = 'jump_up_button_with_long_press';
|
|
||||||
static const String featureJumpDownButton =
|
|
||||||
'jump_down_button_with_long_press';
|
|
||||||
|
|
||||||
static final String happyFace = <String>[
|
static final String happyFace = <String>[
|
||||||
'(๑•̀ㅂ•́)و✧',
|
'(๑•̀ㅂ•́)و✧',
|
||||||
'( ͡• ͜ʖ ͡•)',
|
'( ͡• ͜ʖ ͡•)',
|
||||||
|
@ -18,6 +18,8 @@ class EditCubit extends HydratedCubit<EditState> {
|
|||||||
final DraftCache _draftCache;
|
final DraftCache _draftCache;
|
||||||
final Debouncer _debouncer;
|
final Debouncer _debouncer;
|
||||||
|
|
||||||
|
void reset() => emit(const EditState.init());
|
||||||
|
|
||||||
void onReplyTapped(Item item) {
|
void onReplyTapped(Item item) {
|
||||||
emit(
|
emit(
|
||||||
EditState(
|
EditState(
|
||||||
@ -36,14 +38,6 @@ class EditCubit extends HydratedCubit<EditState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onReplyBoxClosed() {
|
|
||||||
emit(const EditState.init());
|
|
||||||
}
|
|
||||||
|
|
||||||
void onScrolled() {
|
|
||||||
emit(const EditState.init());
|
|
||||||
}
|
|
||||||
|
|
||||||
void onReplySubmittedSuccessfully() {
|
void onReplySubmittedSuccessfully() {
|
||||||
if (state.replyingTo != null) {
|
if (state.replyingTo != null) {
|
||||||
_draftCache.removeDraft(replyingTo: state.replyingTo!.id);
|
_draftCache.removeDraft(replyingTo: state.replyingTo!.id);
|
||||||
@ -65,9 +59,14 @@ class EditCubit extends HydratedCubit<EditState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteDraft() => clear();
|
void deleteDraft() {
|
||||||
|
// Remove draft in storage.
|
||||||
bool called = false;
|
clear();
|
||||||
|
// Reset cached state.
|
||||||
|
_cachedState = const EditState.init();
|
||||||
|
// Reset to init state;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EditState? fromJson(Map<String, dynamic> json) {
|
EditState? fromJson(Map<String, dynamic> json) {
|
||||||
@ -96,6 +95,7 @@ class EditCubit extends HydratedCubit<EditState> {
|
|||||||
Map<String, dynamic>? toJson(EditState state) {
|
Map<String, dynamic>? toJson(EditState state) {
|
||||||
EditState selected = state;
|
EditState selected = state;
|
||||||
|
|
||||||
|
// Override previous draft only when current draft is not empty.
|
||||||
if (state.replyingTo == null ||
|
if (state.replyingTo == null ||
|
||||||
(state.replyingTo?.id != _cachedState.replyingTo?.id &&
|
(state.replyingTo?.id != _cachedState.replyingTo?.id &&
|
||||||
state.text.isNullOrEmpty)) {
|
state.text.isNullOrEmpty)) {
|
||||||
|
@ -16,6 +16,7 @@ class PostCubit extends Cubit<PostState> {
|
|||||||
|
|
||||||
Future<void> post({required String text, required int to}) async {
|
Future<void> post({required String text, required int to}) async {
|
||||||
emit(state.copyWith(status: Status.inProgress));
|
emit(state.copyWith(status: Status.inProgress));
|
||||||
|
|
||||||
final bool successful = await _postRepository.comment(
|
final bool successful = await _postRepository.comment(
|
||||||
parentId: to,
|
parentId: to,
|
||||||
text: text,
|
text: text,
|
||||||
@ -42,4 +43,13 @@ class PostCubit extends Cubit<PostState> {
|
|||||||
void reset() {
|
void reset() {
|
||||||
emit(state.copyWith(status: Status.idle));
|
emit(state.copyWith(status: Status.idle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('For debugging only')
|
||||||
|
Future<bool> getFakeResult() async {
|
||||||
|
final bool result = await Future<bool>.delayed(
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
() => true,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,10 +36,7 @@ extension StateExtension on State {
|
|||||||
if (splitViewEnabled && !forceNewScreen) {
|
if (splitViewEnabled && !forceNewScreen) {
|
||||||
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
context.read<SplitViewCubit>().updateItemScreenArgs(args);
|
||||||
} else {
|
} else {
|
||||||
context.push(
|
context.push('/${ItemScreen.routeName}', extra: args);
|
||||||
'/${ItemScreen.routeName}',
|
|
||||||
extra: args,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Future<void>.value();
|
return Future<void>.value();
|
||||||
|
@ -13,6 +13,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
|
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
|
||||||
import 'package:hacki/blocs/blocs.dart';
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/config/custom_router.dart';
|
import 'package:hacki/config/custom_router.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';
|
||||||
@ -26,6 +27,7 @@ import 'package:logger/logger.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
|
|
||||||
// For receiving payload event from local notifications.
|
// For receiving payload event from local notifications.
|
||||||
@ -143,6 +145,8 @@ Future<void> main({bool testing = false}) async {
|
|||||||
|
|
||||||
HydratedBloc.storage = storage;
|
HydratedBloc.storage = storage;
|
||||||
|
|
||||||
|
VisibilityDetectorController.instance.updateInterval = Durations.ms200;
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
HackiApp(
|
HackiApp(
|
||||||
savedThemeMode: savedThemeMode,
|
savedThemeMode: savedThemeMode,
|
||||||
|
47
lib/models/discoverable_feature.dart
Normal file
47
lib/models/discoverable_feature.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
enum DiscoverableFeature {
|
||||||
|
addStoryToFavList(
|
||||||
|
featureId: 'add_story_to_fav_list',
|
||||||
|
title: 'Fav a Story',
|
||||||
|
description: '''Add it to your favorites''',
|
||||||
|
),
|
||||||
|
openStoryInWebView(
|
||||||
|
featureId: 'open_story_in_web_view',
|
||||||
|
title: 'Open in Browser',
|
||||||
|
description: '''You can tap here to open this story in browser.''',
|
||||||
|
),
|
||||||
|
login(
|
||||||
|
featureId: 'log_in',
|
||||||
|
title: 'Log in for more',
|
||||||
|
description:
|
||||||
|
'''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.''',
|
||||||
|
),
|
||||||
|
pinToTop(
|
||||||
|
featureId: 'pin_to_top',
|
||||||
|
title: 'Pin a Story',
|
||||||
|
description:
|
||||||
|
'''Pin this story to the top of your home screen so that you can come back later.''',
|
||||||
|
),
|
||||||
|
jumpUpButton(
|
||||||
|
featureId: 'jump_up_button_with_long_press',
|
||||||
|
title: 'Shortcut',
|
||||||
|
description:
|
||||||
|
'''Tapping on this button will take you to the previous off-screen root level comment.\n\nLong press on it to jump to the very beginning of this thread.''',
|
||||||
|
),
|
||||||
|
jumpDownButton(
|
||||||
|
featureId: 'jump_down_button_with_long_press',
|
||||||
|
title: 'Shortcut',
|
||||||
|
description:
|
||||||
|
'''Tapping on this button will take you to the next off-screen root level comment.\n\nLong press on it to jump to the end of this thread.''',
|
||||||
|
);
|
||||||
|
|
||||||
|
const DiscoverableFeature({
|
||||||
|
required this.featureId,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Feature ids for feature discovery.
|
||||||
|
final String featureId;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
export 'comments_order.dart';
|
export 'comments_order.dart';
|
||||||
|
export 'discoverable_feature.dart';
|
||||||
export 'export_destination.dart';
|
export 'export_destination.dart';
|
||||||
export 'fetch_mode.dart';
|
export 'fetch_mode.dart';
|
||||||
export 'font.dart';
|
export 'font.dart';
|
||||||
|
@ -61,14 +61,6 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// This is for testing only.
|
|
||||||
// FeatureDiscovery.clearPreferences(context, <String>[
|
|
||||||
// Constants.featureLogIn,
|
|
||||||
// Constants.featureAddStoryToFavList,
|
|
||||||
// Constants.featureOpenStoryInWebView,
|
|
||||||
// Constants.featurePinToTop,
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
ReceiveSharingIntent.getInitialText().then(onShareExtensionTapped);
|
ReceiveSharingIntent.getInitialText().then(onShareExtensionTapped);
|
||||||
|
|
||||||
intentDataStreamSubscription =
|
intentDataStreamSubscription =
|
||||||
@ -89,7 +81,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
FeatureDiscovery.discoverFeatures(
|
FeatureDiscovery.discoverFeatures(
|
||||||
context,
|
context,
|
||||||
<String>{
|
<String>{
|
||||||
Constants.featureLogIn,
|
DiscoverableFeature.login.featureId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -316,4 +308,14 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('For debugging only')
|
||||||
|
void clearFeatureDiscoveryPreferences(BuildContext context) {
|
||||||
|
FeatureDiscovery.clearPreferences(context, <String>[
|
||||||
|
DiscoverableFeature.login.featureId,
|
||||||
|
DiscoverableFeature.addStoryToFavList.featureId,
|
||||||
|
DiscoverableFeature.openStoryInWebView.featureId,
|
||||||
|
DiscoverableFeature.pinToTop.featureId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ class _TabletStoryView extends StatelessWidget {
|
|||||||
child: ColoredBox(
|
child: ColoredBox(
|
||||||
color: Theme.of(context).canvasColor,
|
color: Theme.of(context).canvasColor,
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Text('Tap on story tile to view comments.'),
|
child: Text('Tap on story tile to view its comments.'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
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';
|
||||||
@ -30,7 +32,7 @@ class ItemScreenArgs extends Equatable {
|
|||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
final List<Comment>? targetComments;
|
final List<Comment>? targetComments;
|
||||||
|
|
||||||
/// when a user is trying to view a sub-thread from a main thread, we don't
|
/// when the user is trying to view a sub-thread from a main thread, we don't
|
||||||
/// need to fetch comments from [StoriesRepository] since we have some, if not
|
/// need to fetch comments from [StoriesRepository] since we have some, if not
|
||||||
/// all, comments cached in [CommentCache].
|
/// all, comments cached in [CommentCache].
|
||||||
final bool useCommentCache;
|
final bool useCommentCache;
|
||||||
@ -138,9 +140,12 @@ class ItemScreen extends StatefulWidget {
|
|||||||
class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||||
final TextEditingController commentEditingController =
|
final TextEditingController commentEditingController =
|
||||||
TextEditingController();
|
TextEditingController();
|
||||||
|
final FocusNode focusNode = FocusNode();
|
||||||
final ItemScrollController itemScrollController = ItemScrollController();
|
final ItemScrollController itemScrollController = ItemScrollController();
|
||||||
final ItemPositionsListener itemPositionsListener =
|
final ItemPositionsListener itemPositionsListener =
|
||||||
ItemPositionsListener.create();
|
ItemPositionsListener.create();
|
||||||
|
final ScrollOffsetListener scrollOffsetListener =
|
||||||
|
ScrollOffsetListener.create();
|
||||||
final Throttle storyLinkTapThrottle = Throttle(
|
final Throttle storyLinkTapThrottle = Throttle(
|
||||||
delay: _storyLinkTapThrottleDelay,
|
delay: _storyLinkTapThrottleDelay,
|
||||||
);
|
);
|
||||||
@ -148,6 +153,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
delay: _featureDiscoveryDismissThrottleDelay,
|
delay: _featureDiscoveryDismissThrottleDelay,
|
||||||
);
|
);
|
||||||
final GlobalKey fontSizeIconButtonKey = GlobalKey();
|
final GlobalKey fontSizeIconButtonKey = GlobalKey();
|
||||||
|
StreamSubscription<double>? scrollOffsetSubscription;
|
||||||
|
|
||||||
static const Duration _storyLinkTapThrottleDelay = Durations.twoSeconds;
|
static const Duration _storyLinkTapThrottleDelay = Durations.twoSeconds;
|
||||||
static const Duration _featureDiscoveryDismissThrottleDelay =
|
static const Duration _featureDiscoveryDismissThrottleDelay =
|
||||||
@ -157,10 +163,16 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
void didPop() {
|
void didPop() {
|
||||||
super.didPop();
|
super.didPop();
|
||||||
if (context.read<EditCubit>().state.text.isNullOrEmpty) {
|
if (context.read<EditCubit>().state.text.isNullOrEmpty) {
|
||||||
context.read<EditCubit>().onReplyBoxClosed();
|
context.read<EditCubit>().reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPushNext() {
|
||||||
|
super.didPushNext();
|
||||||
|
focusNode.unfocus();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -169,11 +181,11 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
FeatureDiscovery.discoverFeatures(
|
FeatureDiscovery.discoverFeatures(
|
||||||
context,
|
context,
|
||||||
<String>{
|
<String>{
|
||||||
Constants.featurePinToTop,
|
DiscoverableFeature.pinToTop.featureId,
|
||||||
Constants.featureAddStoryToFavList,
|
DiscoverableFeature.addStoryToFavList.featureId,
|
||||||
Constants.featureOpenStoryInWebView,
|
DiscoverableFeature.openStoryInWebView.featureId,
|
||||||
Constants.featureJumpUpButton,
|
DiscoverableFeature.jumpUpButton.featureId,
|
||||||
Constants.featureJumpDownButton,
|
DiscoverableFeature.jumpDownButton.featureId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -187,6 +199,9 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
.subscribe(this, route);
|
.subscribe(this, route);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
scrollOffsetSubscription =
|
||||||
|
scrollOffsetListener.changes.listen(removeReplyBoxFocusOnScroll);
|
||||||
|
|
||||||
commentEditingController.text = context.read<EditCubit>().state.text ?? '';
|
commentEditingController.text = context.read<EditCubit>().state.text ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,6 +210,8 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
commentEditingController.dispose();
|
commentEditingController.dispose();
|
||||||
storyLinkTapThrottle.dispose();
|
storyLinkTapThrottle.dispose();
|
||||||
featureDiscoveryDismissThrottle.dispose();
|
featureDiscoveryDismissThrottle.dispose();
|
||||||
|
focusNode.dispose();
|
||||||
|
scrollOffsetSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,7 +226,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
BlocListener<PostCubit, PostState>(
|
BlocListener<PostCubit, PostState>(
|
||||||
listener: (BuildContext context, PostState postState) {
|
listener: (BuildContext context, PostState postState) {
|
||||||
if (postState.status == Status.success) {
|
if (postState.status == Status.success) {
|
||||||
context.pop();
|
|
||||||
final String verb =
|
final String verb =
|
||||||
context.read<EditCubit>().state.replyingTo == null
|
context.read<EditCubit>().state.replyingTo == null
|
||||||
? 'updated'
|
? 'updated'
|
||||||
@ -220,7 +236,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
context.read<EditCubit>().onReplySubmittedSuccessfully();
|
context.read<EditCubit>().onReplySubmittedSuccessfully();
|
||||||
context.read<PostCubit>().reset();
|
context.read<PostCubit>().reset();
|
||||||
} else if (postState.status == Status.failure) {
|
} else if (postState.status == Status.failure) {
|
||||||
context.pop();
|
|
||||||
showErrorSnackBar();
|
showErrorSnackBar();
|
||||||
context.read<PostCubit>().reset();
|
context.read<PostCubit>().reset();
|
||||||
}
|
}
|
||||||
@ -258,13 +273,13 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
child: MainView(
|
child: MainView(
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
|
scrollOffsetListener: scrollOffsetListener,
|
||||||
commentEditingController: commentEditingController,
|
commentEditingController: commentEditingController,
|
||||||
authState: authState,
|
authState: authState,
|
||||||
topPadding: topPadding,
|
topPadding: topPadding,
|
||||||
splitViewEnabled: widget.splitViewEnabled,
|
splitViewEnabled: widget.splitViewEnabled,
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
onReplyTapped: showReplyBox,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocBuilder<SplitViewCubit, SplitViewState>(
|
BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||||
@ -303,6 +318,18 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: Dimens.zero,
|
||||||
|
left: Dimens.zero,
|
||||||
|
right: Dimens.zero,
|
||||||
|
child: ReplyBox(
|
||||||
|
splitViewEnabled: true,
|
||||||
|
focusNode: focusNode,
|
||||||
|
textEditingController: commentEditingController,
|
||||||
|
onSendTapped: onSendTapped,
|
||||||
|
onChanged: context.read<EditCubit>().onTextChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -319,18 +346,24 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
body: MainView(
|
body: MainView(
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
|
scrollOffsetListener: scrollOffsetListener,
|
||||||
commentEditingController: commentEditingController,
|
commentEditingController: commentEditingController,
|
||||||
authState: authState,
|
authState: authState,
|
||||||
topPadding: topPadding,
|
topPadding: topPadding,
|
||||||
splitViewEnabled: widget.splitViewEnabled,
|
splitViewEnabled: widget.splitViewEnabled,
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
onReplyTapped: showReplyBox,
|
|
||||||
),
|
),
|
||||||
floatingActionButton: CustomFloatingActionButton(
|
floatingActionButton: CustomFloatingActionButton(
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
),
|
),
|
||||||
|
bottomSheet: ReplyBox(
|
||||||
|
textEditingController: commentEditingController,
|
||||||
|
focusNode: focusNode,
|
||||||
|
onSendTapped: onSendTapped,
|
||||||
|
onChanged: context.read<EditCubit>().onTextChanged,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -338,31 +371,11 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showReplyBox() {
|
void removeReplyBoxFocusOnScroll(double _) {
|
||||||
showModalBottomSheet<void>(
|
focusNode.unfocus();
|
||||||
context: context,
|
if (commentEditingController.text.isEmpty) {
|
||||||
isScrollControlled: true,
|
context.read<EditCubit>().reset();
|
||||||
enableDrag: false,
|
}
|
||||||
builder: (BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
ReplyBox(
|
|
||||||
textEditingController: commentEditingController,
|
|
||||||
onSendTapped: onSendTapped,
|
|
||||||
onCloseTapped: () {
|
|
||||||
context.read<EditCubit>().onReplyBoxClosed();
|
|
||||||
commentEditingController.clear();
|
|
||||||
},
|
|
||||||
onChanged: context.read<EditCubit>().onTextChanged,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: MediaQuery.of(context).viewInsets.bottom,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFontSizeTapped() {
|
void onFontSizeTapped() {
|
||||||
|
@ -3,8 +3,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/screens/widgets/custom_described_feature_overlay.dart';
|
import 'package:hacki/models/discoverable_feature.dart';
|
||||||
import 'package:hacki/styles/palette.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
@ -22,80 +23,88 @@ class CustomFloatingActionButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<CommentsCubit, CommentsState>(
|
return BlocBuilder<CommentsCubit, CommentsState>(
|
||||||
builder: (BuildContext context, CommentsState state) {
|
builder: (BuildContext context, CommentsState state) {
|
||||||
return Column(
|
return BlocBuilder<EditCubit, EditState>(
|
||||||
mainAxisSize: MainAxisSize.min,
|
buildWhen: (EditState previous, EditState current) =>
|
||||||
children: <Widget>[
|
previous.showReplyBox != current.showReplyBox,
|
||||||
CustomDescribedFeatureOverlay(
|
builder: (BuildContext context, EditState editState) {
|
||||||
featureId: Constants.featureJumpUpButton,
|
return AnimatedPadding(
|
||||||
contentLocation: ContentLocation.above,
|
padding: editState.showReplyBox
|
||||||
tapTarget: const Icon(
|
? const EdgeInsets.only(
|
||||||
Icons.keyboard_arrow_up,
|
bottom: Dimens.replyBoxCollapsedHeight,
|
||||||
color: Palette.white,
|
)
|
||||||
),
|
: EdgeInsets.zero,
|
||||||
title: const Text('Shortcut'),
|
duration: Durations.ms200,
|
||||||
description: const Text(
|
child: Column(
|
||||||
'''Tapping on this button will take you to the previous off-screen root level comment.\n\nLong press on it to jump to the very beginning of this thread.''',
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: <Widget>[
|
||||||
child: InkWell(
|
CustomDescribedFeatureOverlay(
|
||||||
onLongPress: () => itemScrollController.scrollTo(
|
feature: DiscoverableFeature.jumpUpButton,
|
||||||
index: 0,
|
contentLocation: ContentLocation.above,
|
||||||
duration: Durations.ms400,
|
tapTarget: const Icon(
|
||||||
),
|
Icons.keyboard_arrow_up,
|
||||||
child: FloatingActionButton.small(
|
color: Palette.white,
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onLongPress: () => itemScrollController.scrollTo(
|
||||||
|
index: 0,
|
||||||
|
duration: Durations.ms400,
|
||||||
|
),
|
||||||
|
child: FloatingActionButton.small(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
|
||||||
/// Randomly generated string as heroTag to prevent
|
/// Randomly generated string as heroTag to prevent
|
||||||
/// default [FloatingActionButton] animation.
|
/// default [FloatingActionButton] animation.
|
||||||
heroTag: UniqueKey().hashCode,
|
heroTag: UniqueKey().hashCode,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedbackUtil.selection();
|
HapticFeedbackUtil.selection();
|
||||||
context.read<CommentsCubit>().scrollToPreviousRoot(
|
context.read<CommentsCubit>().scrollToPreviousRoot(
|
||||||
itemScrollController,
|
itemScrollController,
|
||||||
itemPositionsListener,
|
itemPositionsListener,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.keyboard_arrow_up,
|
Icons.keyboard_arrow_up,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
CustomDescribedFeatureOverlay(
|
||||||
),
|
feature: DiscoverableFeature.jumpDownButton,
|
||||||
),
|
tapTarget: const Icon(
|
||||||
CustomDescribedFeatureOverlay(
|
Icons.keyboard_arrow_down,
|
||||||
featureId: Constants.featureJumpDownButton,
|
color: Palette.white,
|
||||||
tapTarget: const Icon(
|
),
|
||||||
Icons.keyboard_arrow_down,
|
child: InkWell(
|
||||||
color: Palette.white,
|
onLongPress: () => itemScrollController.scrollTo(
|
||||||
),
|
index: state.comments.length,
|
||||||
title: const Text('Shortcut'),
|
duration: Durations.ms400,
|
||||||
description: const Text(
|
),
|
||||||
'''Tapping on this button will take you to the next off-screen root level comment.\n\nLong press on it to jump to the end of this thread.''',
|
child: FloatingActionButton.small(
|
||||||
),
|
backgroundColor:
|
||||||
child: InkWell(
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
onLongPress: () => itemScrollController.scrollTo(
|
|
||||||
index: state.comments.length,
|
|
||||||
duration: Durations.ms400,
|
|
||||||
),
|
|
||||||
child: FloatingActionButton.small(
|
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
|
|
||||||
/// Same as above.
|
/// Same as above.
|
||||||
heroTag: UniqueKey().hashCode,
|
heroTag: UniqueKey().hashCode,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedbackUtil.selection();
|
HapticFeedbackUtil.selection();
|
||||||
context.read<CommentsCubit>().scrollToNextRoot(
|
context.read<CommentsCubit>().scrollToNextRoot(
|
||||||
itemScrollController,
|
itemScrollController,
|
||||||
itemPositionsListener,
|
itemPositionsListener,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.keyboard_arrow_down,
|
Icons.keyboard_arrow_down,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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/config/constants.dart';
|
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/models/discoverable_feature.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
@ -26,12 +26,7 @@ class FavIconButton extends StatelessWidget {
|
|||||||
isFav ? Icons.favorite : Icons.favorite_border,
|
isFav ? Icons.favorite : Icons.favorite_border,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
),
|
),
|
||||||
featureId: Constants.featureAddStoryToFavList,
|
feature: DiscoverableFeature.addStoryToFavList,
|
||||||
title: const Text('Fav a Story'),
|
|
||||||
description: const Text(
|
|
||||||
'Add it to your favorites.',
|
|
||||||
style: TextStyle(fontSize: TextDimens.pt16),
|
|
||||||
),
|
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isFav ? Icons.favorite : Icons.favorite_border,
|
isFav ? Icons.favorite : Icons.favorite_border,
|
||||||
color: isFav ? Palette.orange : Theme.of(context).iconTheme.color,
|
color: isFav ? Palette.orange : Theme.of(context).iconTheme.color,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/models/discoverable_feature.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
@ -21,12 +21,7 @@ class LinkIconButton extends StatelessWidget {
|
|||||||
Icons.stream,
|
Icons.stream,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
),
|
),
|
||||||
featureId: Constants.featureOpenStoryInWebView,
|
feature: DiscoverableFeature.openStoryInWebView,
|
||||||
title: Text('Open in Browser'),
|
|
||||||
description: Text(
|
|
||||||
'''You can tap here to open this story in browser.''',
|
|
||||||
style: TextStyle(fontSize: TextDimens.pt16),
|
|
||||||
),
|
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.stream,
|
Icons.stream,
|
||||||
),
|
),
|
||||||
|
@ -19,25 +19,25 @@ class MainView extends StatelessWidget {
|
|||||||
const MainView({
|
const MainView({
|
||||||
required this.itemScrollController,
|
required this.itemScrollController,
|
||||||
required this.itemPositionsListener,
|
required this.itemPositionsListener,
|
||||||
|
required this.scrollOffsetListener,
|
||||||
required this.commentEditingController,
|
required this.commentEditingController,
|
||||||
required this.authState,
|
required this.authState,
|
||||||
required this.topPadding,
|
required this.topPadding,
|
||||||
required this.splitViewEnabled,
|
required this.splitViewEnabled,
|
||||||
required this.onMoreTapped,
|
required this.onMoreTapped,
|
||||||
required this.onRightMoreTapped,
|
required this.onRightMoreTapped,
|
||||||
required this.onReplyTapped,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ItemScrollController itemScrollController;
|
final ItemScrollController itemScrollController;
|
||||||
final ItemPositionsListener itemPositionsListener;
|
final ItemPositionsListener itemPositionsListener;
|
||||||
|
final ScrollOffsetListener scrollOffsetListener;
|
||||||
final TextEditingController commentEditingController;
|
final TextEditingController commentEditingController;
|
||||||
final AuthState authState;
|
final AuthState authState;
|
||||||
final double topPadding;
|
final double topPadding;
|
||||||
final bool splitViewEnabled;
|
final bool splitViewEnabled;
|
||||||
final void Function(Item item, Rect? rect) onMoreTapped;
|
final void Function(Item item, Rect? rect) onMoreTapped;
|
||||||
final ValueChanged<Comment> onRightMoreTapped;
|
final ValueChanged<Comment> onRightMoreTapped;
|
||||||
final VoidCallback onReplyTapped;
|
|
||||||
|
|
||||||
static const int _loadingIndicatorOpacityAnimationDuration = 300;
|
static const int _loadingIndicatorOpacityAnimationDuration = 300;
|
||||||
static const double _trailingBoxHeight = 240;
|
static const double _trailingBoxHeight = 240;
|
||||||
@ -49,101 +49,90 @@ class MainView extends StatelessWidget {
|
|||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: BlocBuilder<CommentsCubit, CommentsState>(
|
child: BlocBuilder<CommentsCubit, CommentsState>(
|
||||||
builder: (BuildContext context, CommentsState state) {
|
builder: (BuildContext context, CommentsState state) {
|
||||||
return Scrollbar(
|
return RefreshIndicator(
|
||||||
interactive: true,
|
displacement: 100,
|
||||||
child: RefreshIndicator(
|
onRefresh: () async {
|
||||||
displacement: 100,
|
HapticFeedbackUtil.light();
|
||||||
onRefresh: () async {
|
|
||||||
HapticFeedbackUtil.light();
|
|
||||||
|
|
||||||
if (context.read<StoriesBloc>().state.isOfflineReading ==
|
if (context.read<StoriesBloc>().state.isOfflineReading ==
|
||||||
false &&
|
false &&
|
||||||
state.onlyShowTargetComment == false) {
|
state.onlyShowTargetComment == false) {
|
||||||
unawaited(context.read<CommentsCubit>().refresh());
|
unawaited(context.read<CommentsCubit>().refresh());
|
||||||
|
|
||||||
if (state.item.isPoll) {
|
if (state.item.isPoll) {
|
||||||
context.read<PollCubit>().refresh();
|
context.read<PollCubit>().refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ScrollablePositionedList.builder(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
itemPositionsListener: itemPositionsListener,
|
||||||
|
itemCount: state.comments.length + 2,
|
||||||
|
padding: EdgeInsets.only(top: topPadding),
|
||||||
|
scrollOffsetListener: scrollOffsetListener,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return _ParentItemSection(
|
||||||
|
commentEditingController: commentEditingController,
|
||||||
|
state: state,
|
||||||
|
authState: authState,
|
||||||
|
topPadding: topPadding,
|
||||||
|
splitViewEnabled: splitViewEnabled,
|
||||||
|
onMoreTapped: onMoreTapped,
|
||||||
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
|
);
|
||||||
|
} else if (index == state.comments.length + 1) {
|
||||||
|
if ((state.status == CommentsStatus.allLoaded &&
|
||||||
|
state.comments.isNotEmpty) ||
|
||||||
|
state.onlyShowTargetComment) {
|
||||||
|
return SizedBox(
|
||||||
|
height: _trailingBoxHeight,
|
||||||
|
child: Center(
|
||||||
|
child: Text(Constants.happyFace),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
child: ScrollablePositionedList.builder(
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
itemScrollController: itemScrollController,
|
|
||||||
itemPositionsListener: itemPositionsListener,
|
|
||||||
itemCount: state.comments.length + 2,
|
|
||||||
padding: EdgeInsets.only(top: topPadding),
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
if (index == 0) {
|
|
||||||
return _ParentItemSection(
|
|
||||||
commentEditingController: commentEditingController,
|
|
||||||
state: state,
|
|
||||||
authState: authState,
|
|
||||||
topPadding: topPadding,
|
|
||||||
splitViewEnabled: splitViewEnabled,
|
|
||||||
onMoreTapped: onMoreTapped,
|
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
|
||||||
onReplyTapped: onReplyTapped,
|
|
||||||
);
|
|
||||||
} else if (index == state.comments.length + 1) {
|
|
||||||
if ((state.status == CommentsStatus.allLoaded &&
|
|
||||||
state.comments.isNotEmpty) ||
|
|
||||||
state.onlyShowTargetComment) {
|
|
||||||
return SizedBox(
|
|
||||||
height: _trailingBoxHeight,
|
|
||||||
child: Center(
|
|
||||||
child: Text(Constants.happyFace),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
index = index - 1;
|
index = index - 1;
|
||||||
final Comment comment = state.comments.elementAt(index);
|
final Comment comment = state.comments.elementAt(index);
|
||||||
return FadeIn(
|
return FadeIn(
|
||||||
key: ValueKey<String>('${comment.id}-FadeIn'),
|
key: ValueKey<String>('${comment.id}-FadeIn'),
|
||||||
child: CommentTile(
|
child: CommentTile(
|
||||||
comment: comment,
|
comment: comment,
|
||||||
level: comment.level,
|
level: comment.level,
|
||||||
opUsername: state.item.by,
|
opUsername: state.item.by,
|
||||||
fetchMode: state.fetchMode,
|
fetchMode: state.fetchMode,
|
||||||
onReplyTapped: (Comment cmt) {
|
onReplyTapped: (Comment cmt) {
|
||||||
HapticFeedbackUtil.light();
|
HapticFeedbackUtil.light();
|
||||||
if (cmt.deleted || cmt.dead) {
|
if (cmt.deleted || cmt.dead) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmt.id !=
|
if (cmt.id !=
|
||||||
context
|
context.read<EditCubit>().state.replyingTo?.id) {
|
||||||
.read<EditCubit>()
|
|
||||||
.state
|
|
||||||
.replyingTo
|
|
||||||
?.id) {
|
|
||||||
commentEditingController.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
context.read<EditCubit>().onReplyTapped(cmt);
|
|
||||||
|
|
||||||
onReplyTapped();
|
|
||||||
},
|
|
||||||
onEditTapped: (Comment cmt) {
|
|
||||||
HapticFeedbackUtil.light();
|
|
||||||
if (cmt.deleted || cmt.dead) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
commentEditingController.clear();
|
commentEditingController.clear();
|
||||||
context.read<EditCubit>().onEditTapped(cmt);
|
}
|
||||||
|
|
||||||
onReplyTapped();
|
context.read<EditCubit>().onReplyTapped(cmt);
|
||||||
},
|
},
|
||||||
onMoreTapped: onMoreTapped,
|
onEditTapped: (Comment cmt) {
|
||||||
onRightMoreTapped: onRightMoreTapped,
|
HapticFeedbackUtil.light();
|
||||||
itemScrollController: itemScrollController,
|
if (cmt.deleted || cmt.dead) {
|
||||||
),
|
return;
|
||||||
);
|
}
|
||||||
},
|
commentEditingController.clear();
|
||||||
),
|
context.read<EditCubit>().onEditTapped(cmt);
|
||||||
|
},
|
||||||
|
onMoreTapped: onMoreTapped,
|
||||||
|
onRightMoreTapped: onRightMoreTapped,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -184,7 +173,6 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
required this.splitViewEnabled,
|
required this.splitViewEnabled,
|
||||||
required this.onMoreTapped,
|
required this.onMoreTapped,
|
||||||
required this.onRightMoreTapped,
|
required this.onRightMoreTapped,
|
||||||
required this.onReplyTapped,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController commentEditingController;
|
final TextEditingController commentEditingController;
|
||||||
@ -194,7 +182,6 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
final bool splitViewEnabled;
|
final bool splitViewEnabled;
|
||||||
final void Function(Item item, Rect? rect) onMoreTapped;
|
final void Function(Item item, Rect? rect) onMoreTapped;
|
||||||
final ValueChanged<Comment> onRightMoreTapped;
|
final ValueChanged<Comment> onRightMoreTapped;
|
||||||
final VoidCallback onReplyTapped;
|
|
||||||
|
|
||||||
static const double _viewParentButtonWidth = 100;
|
static const double _viewParentButtonWidth = 100;
|
||||||
static const double _viewRootButtonWidth = 80;
|
static const double _viewRootButtonWidth = 80;
|
||||||
@ -225,8 +212,6 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
commentEditingController.clear();
|
commentEditingController.clear();
|
||||||
}
|
}
|
||||||
context.read<EditCubit>().onReplyTapped(state.item);
|
context.read<EditCubit>().onReplyTapped(state.item);
|
||||||
|
|
||||||
onReplyTapped();
|
|
||||||
},
|
},
|
||||||
backgroundColor: Palette.orange,
|
backgroundColor: Palette.orange,
|
||||||
foregroundColor: Palette.white,
|
foregroundColor: Palette.white,
|
||||||
@ -352,8 +337,8 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.only(
|
||||||
horizontal: Dimens.pt10,
|
left: Dimens.pt8,
|
||||||
),
|
),
|
||||||
child: ItemText(
|
child: ItemText(
|
||||||
item: state.item,
|
item: state.item,
|
||||||
|
@ -2,7 +2,6 @@ import 'dart:math';
|
|||||||
|
|
||||||
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/config/constants.dart';
|
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
@ -33,14 +32,7 @@ class PinIconButton extends StatelessWidget {
|
|||||||
pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
),
|
),
|
||||||
featureId: Constants.featurePinToTop,
|
feature: DiscoverableFeature.pinToTop,
|
||||||
title: const Text('Pin a Story'),
|
|
||||||
description: const Text(
|
|
||||||
'Pin this story to the top of your '
|
|
||||||
'home screen so that you can come'
|
|
||||||
' back later.',
|
|
||||||
style: TextStyle(fontSize: TextDimens.pt16),
|
|
||||||
),
|
|
||||||
child: Icon(
|
child: Icon(
|
||||||
pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
||||||
color: pinned
|
color: pinned
|
||||||
|
@ -14,18 +14,18 @@ import 'package:hacki/utils/utils.dart';
|
|||||||
|
|
||||||
class ReplyBox extends StatefulWidget {
|
class ReplyBox extends StatefulWidget {
|
||||||
const ReplyBox({
|
const ReplyBox({
|
||||||
|
required this.focusNode,
|
||||||
required this.textEditingController,
|
required this.textEditingController,
|
||||||
required this.onSendTapped,
|
required this.onSendTapped,
|
||||||
required this.onCloseTapped,
|
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
super.key,
|
super.key,
|
||||||
this.splitViewEnabled = false,
|
this.splitViewEnabled = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool splitViewEnabled;
|
final bool splitViewEnabled;
|
||||||
|
final FocusNode focusNode;
|
||||||
final TextEditingController textEditingController;
|
final TextEditingController textEditingController;
|
||||||
final VoidCallback onSendTapped;
|
final VoidCallback onSendTapped;
|
||||||
final VoidCallback onCloseTapped;
|
|
||||||
final ValueChanged<String> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -40,9 +40,17 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
expandedHeight ??= MediaQuery.of(context).size.height -
|
expandedHeight ??= MediaQuery.of(context).size.height;
|
||||||
MediaQuery.of(context).viewInsets.bottom;
|
return BlocConsumer<EditCubit, EditState>(
|
||||||
return BlocBuilder<EditCubit, EditState>(
|
listenWhen: (EditState previous, EditState current) =>
|
||||||
|
previous.showReplyBox != current.showReplyBox,
|
||||||
|
listener: (BuildContext context, EditState editState) {
|
||||||
|
if (editState.showReplyBox) {
|
||||||
|
widget.focusNode.requestFocus();
|
||||||
|
} else {
|
||||||
|
widget.focusNode.unfocus();
|
||||||
|
}
|
||||||
|
},
|
||||||
buildWhen: (EditState previous, EditState current) =>
|
buildWhen: (EditState previous, EditState current) =>
|
||||||
previous.showReplyBox != current.showReplyBox ||
|
previous.showReplyBox != current.showReplyBox ||
|
||||||
previous.itemBeingEdited != current.itemBeingEdited ||
|
previous.itemBeingEdited != current.itemBeingEdited ||
|
||||||
@ -62,7 +70,9 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
: Dimens.zero,
|
: Dimens.zero,
|
||||||
),
|
),
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
height: expanded ? expandedHeight : collapsedHeight,
|
height: editState.showReplyBox
|
||||||
|
? (expanded ? expandedHeight : collapsedHeight)
|
||||||
|
: Dimens.zero,
|
||||||
duration: Durations.ms200,
|
duration: Durations.ms200,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
@ -82,8 +92,8 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
height: Dimens.zero,
|
height: Dimens.zero,
|
||||||
),
|
),
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
height: expanded ? Dimens.pt36 : Dimens.zero,
|
height: expanded ? Dimens.pt40 : Dimens.zero,
|
||||||
duration: Durations.ms200,
|
duration: Durations.ms300,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@ -145,7 +155,9 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
color: Palette.orange,
|
color: Palette.orange,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pop();
|
setState(() {
|
||||||
|
expanded = false;
|
||||||
|
});
|
||||||
|
|
||||||
final EditState state =
|
final EditState state =
|
||||||
context.read<EditCubit>().state;
|
context.read<EditCubit>().state;
|
||||||
@ -153,17 +165,13 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
state.text.isNotNullOrEmpty) {
|
state.text.isNotNullOrEmpty) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
builder: (BuildContext context) =>
|
builder: (BuildContext context) =>
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
title: const Text('Save draft?'),
|
title: const Text('Abort editing?'),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: context.pop,
|
||||||
context
|
|
||||||
.read<EditCubit>()
|
|
||||||
.deleteDraft();
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'No',
|
'No',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@ -172,15 +180,18 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.pop(),
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
onCloseTapped();
|
||||||
|
},
|
||||||
child: const Text('Yes'),
|
child: const Text('Yes'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
onCloseTapped();
|
||||||
}
|
}
|
||||||
widget.onCloseTapped();
|
|
||||||
expanded = false;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -214,39 +225,32 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: TextField(
|
||||||
padding: EdgeInsets.only(
|
focusNode: widget.focusNode,
|
||||||
left: Dimens.pt16,
|
controller: widget.textEditingController,
|
||||||
right: Dimens.pt16,
|
expands: true,
|
||||||
bottom: expanded
|
maxLines: null,
|
||||||
// This padding here prevents keyboard
|
decoration: const InputDecoration(
|
||||||
// overlapping with TextField.
|
alignLabelWithHint: true,
|
||||||
? MediaQuery.of(context).viewInsets.bottom +
|
contentPadding: EdgeInsets.only(
|
||||||
Dimens.pt16
|
left: Dimens.pt10,
|
||||||
: Dimens.zero,
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
controller: widget.textEditingController,
|
|
||||||
autofocus: true,
|
|
||||||
expands: true,
|
|
||||||
maxLines: null,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
hintText: '...',
|
|
||||||
hintStyle: TextStyle(
|
|
||||||
color: Palette.grey,
|
|
||||||
),
|
|
||||||
focusedBorder: InputBorder.none,
|
|
||||||
border: InputBorder.none,
|
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.multiline,
|
hintText: '...',
|
||||||
textCapitalization: TextCapitalization.sentences,
|
hintStyle: TextStyle(
|
||||||
textInputAction: TextInputAction.newline,
|
color: Palette.grey,
|
||||||
onChanged: widget.onChanged,
|
),
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
textInputAction: TextInputAction.newline,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt8,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -258,10 +262,22 @@ class _ReplyBoxState extends State<ReplyBox> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onCloseTapped() {
|
||||||
|
context.read<EditCubit>().deleteDraft();
|
||||||
|
widget.textEditingController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void showTextPopup() {
|
void showTextPopup() {
|
||||||
final Item? replyingTo = context.read<EditCubit>().state.replyingTo;
|
final Item? replyingTo = context.read<EditCubit>().state.replyingTo;
|
||||||
|
|
||||||
if (replyingTo == null) return;
|
if (replyingTo == null) {
|
||||||
|
return;
|
||||||
|
} else if (replyingTo is Story) {
|
||||||
|
final ItemScreenArgs args = ItemScreenArgs(item: replyingTo);
|
||||||
|
context.push('/${ItemScreen.routeName}', extra: args);
|
||||||
|
expanded = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -2,24 +2,22 @@ import 'dart:async';
|
|||||||
|
|
||||||
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:hacki/models/discoverable_feature.dart';
|
||||||
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
|
||||||
class CustomDescribedFeatureOverlay extends StatelessWidget {
|
class CustomDescribedFeatureOverlay extends StatelessWidget {
|
||||||
const CustomDescribedFeatureOverlay({
|
const CustomDescribedFeatureOverlay({
|
||||||
required this.featureId,
|
required this.feature,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.tapTarget,
|
required this.tapTarget,
|
||||||
required this.title,
|
|
||||||
required this.description,
|
|
||||||
super.key,
|
super.key,
|
||||||
this.contentLocation = ContentLocation.trivial,
|
this.contentLocation = ContentLocation.trivial,
|
||||||
this.onComplete,
|
this.onComplete,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String featureId;
|
final DiscoverableFeature feature;
|
||||||
final Widget tapTarget;
|
final Widget tapTarget;
|
||||||
final Widget title;
|
|
||||||
final Widget description;
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final ContentLocation contentLocation;
|
final ContentLocation contentLocation;
|
||||||
final VoidCallback? onComplete;
|
final VoidCallback? onComplete;
|
||||||
@ -27,12 +25,15 @@ class CustomDescribedFeatureOverlay extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DescribedFeatureOverlay(
|
return DescribedFeatureOverlay(
|
||||||
featureId: featureId,
|
featureId: feature.featureId,
|
||||||
overflowMode: OverflowMode.extendBackground,
|
overflowMode: OverflowMode.extendBackground,
|
||||||
targetColor: Theme.of(context).primaryColor,
|
targetColor: Theme.of(context).primaryColor,
|
||||||
tapTarget: tapTarget,
|
tapTarget: tapTarget,
|
||||||
title: title,
|
title: Text(feature.title),
|
||||||
description: description,
|
description: Text(
|
||||||
|
feature.description,
|
||||||
|
style: const TextStyle(fontSize: TextDimens.pt16),
|
||||||
|
),
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
contentLocation: contentLocation,
|
contentLocation: contentLocation,
|
||||||
onBackgroundTap: () {
|
onBackgroundTap: () {
|
||||||
|
@ -83,16 +83,7 @@ class _CustomTabBarState extends State<CustomTabBar> {
|
|||||||
size: TextDimens.pt16,
|
size: TextDimens.pt16,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
),
|
),
|
||||||
featureId: Constants.featureLogIn,
|
feature: DiscoverableFeature.login,
|
||||||
title: const Text('Log in for more'),
|
|
||||||
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: TextDimens.pt16),
|
|
||||||
),
|
|
||||||
child: BlocBuilder<NotificationCubit, NotificationState>(
|
child: BlocBuilder<NotificationCubit, NotificationState>(
|
||||||
buildWhen: (
|
buildWhen: (
|
||||||
NotificationState previous,
|
NotificationState previous,
|
||||||
|
@ -130,10 +130,11 @@ class _StoriesListViewState extends State<StoriesListView> {
|
|||||||
),
|
),
|
||||||
child: OptionalWrapper(
|
child: OptionalWrapper(
|
||||||
enabled: context
|
enabled: context
|
||||||
.read<PreferenceCubit>()
|
.read<PreferenceCubit>()
|
||||||
.state
|
.state
|
||||||
.storyMarkingMode
|
.storyMarkingMode
|
||||||
.shouldDetectScrollingPast,
|
.shouldDetectScrollingPast &&
|
||||||
|
!context.read<StoriesBloc>().hasRead(story),
|
||||||
wrapper: (Widget child) => VisibilityDetector(
|
wrapper: (Widget child) => VisibilityDetector(
|
||||||
key: ValueKey<int>(story.id),
|
key: ValueKey<int>(story.id),
|
||||||
onVisibilityChanged: (VisibilityInfo info) {
|
onVisibilityChanged: (VisibilityInfo info) {
|
||||||
|
@ -20,6 +20,8 @@ abstract class Dimens {
|
|||||||
static const double pt64 = 64;
|
static const double pt64 = 64;
|
||||||
static const double pt100 = 100;
|
static const double pt100 = 100;
|
||||||
static const double pt120 = 120;
|
static const double pt120 = 120;
|
||||||
|
|
||||||
|
static const double replyBoxCollapsedHeight = 140;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class TextDimens {
|
abstract class TextDimens {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 1.9.1+122
|
version: 1.9.2+123
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
Reference in New Issue
Block a user