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