diff --git a/fastlane/metadata/android/en-US/changelogs/127.txt b/fastlane/metadata/android/en-US/changelogs/127.txt new file mode 100644 index 0000000..623ee97 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/127.txt @@ -0,0 +1,4 @@ +- Ability to use Material 3. +- Ability to search in thread. +- Ability to customize text scale factor. +- Ability to customize app's accent color. \ No newline at end of file diff --git a/lib/config/constants.dart b/lib/config/constants.dart index 5b8b1d5..768322a 100644 --- a/lib/config/constants.dart +++ b/lib/config/constants.dart @@ -20,6 +20,8 @@ abstract class Constants { '$githubLink/issues/new?title=Found+a+bug+in+Hacki&body=Please+describe+the+problem.'; static const String wikipediaLink = 'https://en.wikipedia.org/wiki/'; static const String wiktionaryLink = 'https://en.wiktionary.org/wiki/'; + static const String hackerNewsItemLinkPrefix = + 'https://news.ycombinator.com/item?id='; static const String supportEmail = 'georgefung98@gmail.com'; static const String _imagePath = 'assets/images'; diff --git a/lib/cubits/comments/comments_cubit.dart b/lib/cubits/comments/comments_cubit.dart index 1fe0d8c..243a273 100644 --- a/lib/cubits/comments/comments_cubit.dart +++ b/lib/cubits/comments/comments_cubit.dart @@ -9,6 +9,7 @@ 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'; +import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/models/models.dart'; import 'package:hacki/repositories/repositories.dart'; import 'package:hacki/screens/screens.dart'; @@ -61,6 +62,10 @@ class CommentsCubit extends Cubit { final SembastRepository _sembastRepository; final Logger _logger; + final ItemScrollController itemScrollController = ItemScrollController(); + final ItemPositionsListener itemPositionsListener = + ItemPositionsListener.create(); + /// The [StreamSubscription] for stream (both lazy or eager) /// fetching comments posted directly to the story. StreamSubscription? _streamSubscription; @@ -349,11 +354,20 @@ class CommentsCubit extends Cubit { init(useCommentCache: true); } + void scrollTo({ + required int index, + double alignment = 0.0, + }) { + debugPrint('Scrolling to: $index, alignment: $alignment'); + itemScrollController.scrollTo( + index: index, + alignment: alignment, + duration: Durations.ms400, + ); + } + /// Scroll to next root level comment. - void scrollToNextRoot( - ItemScrollController itemScrollController, - ItemPositionsListener itemPositionsListener, - ) { + void scrollToNextRoot() { final int totalComments = state.comments.length; final List onScreenComments = itemPositionsListener .itemPositions.value @@ -398,10 +412,7 @@ class CommentsCubit extends Cubit { } /// Scroll to previous root level comment. - void scrollToPreviousRoot( - ItemScrollController itemScrollController, - ItemPositionsListener itemPositionsListener, - ) { + void scrollToPreviousRoot() { final List onScreenComments = itemPositionsListener .itemPositions.value // The header is also a part of the list view, @@ -436,6 +447,23 @@ class CommentsCubit extends Cubit { } } + void search(String query) { + resetSearch(); + final String lowercaseQuery = query.toLowerCase(); + for (final int i in 0.to(state.comments.length, inclusive: false)) { + final Comment cmt = state.comments.elementAt(i); + if (cmt.text.toLowerCase().contains(lowercaseQuery)) { + emit( + state.copyWith( + matchedComments: [...state.matchedComments, i], + ), + ); + } + } + } + + void resetSearch() => emit(state.copyWith(matchedComments: [])); + List _sortKids(List kids) { switch (state.order) { case CommentsOrder.natural: diff --git a/lib/cubits/comments/comments_state.dart b/lib/cubits/comments/comments_state.dart index c0ca109..f6914d6 100644 --- a/lib/cubits/comments/comments_state.dart +++ b/lib/cubits/comments/comments_state.dart @@ -12,6 +12,7 @@ class CommentsState extends Equatable { const CommentsState({ required this.item, required this.comments, + required this.matchedComments, required this.status, required this.fetchParentStatus, required this.fetchRootStatus, @@ -28,6 +29,7 @@ class CommentsState extends Equatable { required this.fetchMode, required this.order, }) : comments = [], + matchedComments = [], status = CommentsStatus.idle, fetchParentStatus = CommentsStatus.idle, fetchRootStatus = CommentsStatus.idle, @@ -45,9 +47,13 @@ class CommentsState extends Equatable { final bool isOfflineReading; final int currentPage; + /// Indexes of comments that matches the query for in-thread search. + final List matchedComments; + CommentsState copyWith({ Item? item, List? comments, + List? matchedComments, CommentsStatus? status, CommentsStatus? fetchParentStatus, CommentsStatus? fetchRootStatus, @@ -60,6 +66,7 @@ class CommentsState extends Equatable { return CommentsState( item: item ?? this.item, comments: comments ?? this.comments, + matchedComments: matchedComments ?? this.matchedComments, fetchParentStatus: fetchParentStatus ?? this.fetchParentStatus, fetchRootStatus: fetchRootStatus ?? this.fetchRootStatus, status: status ?? this.status, @@ -86,5 +93,6 @@ class CommentsState extends Equatable { isOfflineReading, currentPage, comments, + matchedComments, ]; } diff --git a/lib/cubits/preference/preference_state.dart b/lib/cubits/preference/preference_state.dart index fdb8333..7793e37 100644 --- a/lib/cubits/preference/preference_state.dart +++ b/lib/cubits/preference/preference_state.dart @@ -70,6 +70,8 @@ class PreferenceState extends Equatable { bool get customTabEnabled => _isOn(); + bool get material3Enabled => _isOn(); + double get textScaleFactor => preferences.singleWhereType().val; diff --git a/lib/extensions/item_action_mixin.dart b/lib/extensions/item_action_mixin.dart index 64a2cca..bf468fd 100644 --- a/lib/extensions/item_action_mixin.dart +++ b/lib/extensions/item_action_mixin.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:hacki/blocs/auth/auth_bloc.dart'; +import 'package:hacki/config/constants.dart'; import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/models/models.dart'; @@ -103,31 +104,26 @@ mixin ItemActionMixin on State { context: context, builder: (BuildContext context) { return SafeArea( - child: ColoredBox( - color: Theme.of(context).canvasColor, - child: Material( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () => context.pop(item.url), - title: const Text('Link to article'), - ), - ListTile( - onTap: () => context.pop( - 'https://news.ycombinator.com/item?id=${item.id}', - ), - title: const Text('Link to HN'), - ), - ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + onTap: () => context.pop(item.url), + title: const Text('Link to article'), ), - ), + ListTile( + onTap: () => context.pop( + '${Constants.hackerNewsItemLinkPrefix}${item.id}', + ), + title: const Text('Link to HN'), + ), + ], ), ); }, ); } else { - linkToShare = 'https://news.ycombinator.com/item?id=${item.id}'; + linkToShare = '${Constants.hackerNewsItemLinkPrefix}${item.id}'; } if (linkToShare != null) { diff --git a/lib/main.dart b/lib/main.dart index 59dcdf0..c775ab1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -233,10 +233,13 @@ class HackiApp extends StatelessWidget { buildWhen: (PreferenceState previous, PreferenceState current) => previous.appColor != current.appColor || previous.font != current.font || - previous.textScaleFactor != current.textScaleFactor, + previous.textScaleFactor != current.textScaleFactor || + previous.material3Enabled != current.material3Enabled, builder: (BuildContext context, PreferenceState state) { return AdaptiveTheme( - key: ValueKey('${state.appColor}${state.font}'), + key: ValueKey( + '''${state.appColor}${state.font}${state.material3Enabled}''', + ), light: ThemeData( primaryColor: state.appColor, colorScheme: ColorScheme.fromSwatch( @@ -287,7 +290,46 @@ class HackiApp extends StatelessWidget { title: 'Hacki', debugShowCheckedModeBanner: false, theme: (isDarkModeEnabled ? darkTheme : theme).copyWith( - useMaterial3: false, + useMaterial3: state.material3Enabled, + dividerTheme: state.material3Enabled + ? DividerThemeData( + color: Palette.grey.withOpacity(0.2), + ) + : null, + switchTheme: state.material3Enabled + ? SwitchThemeData( + trackColor: MaterialStateProperty.resolveWith( + (Set states) { + if (states + .contains(MaterialState.selected)) { + return null; + } else { + return Palette.grey.withOpacity(0.2); + } + }, + ), + ) + : null, + bottomSheetTheme: state.material3Enabled + ? BottomSheetThemeData( + modalElevation: 0, + backgroundColor: isDarkModeEnabled + ? Palette.black + : Palette.white, + shape: const RoundedRectangleBorder(), + ) + : null, + inputDecorationTheme: state.material3Enabled + ? InputDecorationTheme( + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: isDarkModeEnabled + ? Palette.white + : Palette.black, + ), + ), + ) + : null, ), routerConfig: router, ), diff --git a/lib/models/discoverable_feature.dart b/lib/models/discoverable_feature.dart index 1ab0458..923ab27 100644 --- a/lib/models/discoverable_feature.dart +++ b/lib/models/discoverable_feature.dart @@ -25,13 +25,18 @@ enum DiscoverableFeature { 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.''', + '''Tapping on this button will take you to the previous 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.''', + '''Tapping on this button will take you to the next root level comment.\n\nLong press on it to jump to the end of this thread.''', + ), + searchInThread( + featureId: 'search_in_thread', + title: 'Search in Thread', + description: '''Search for comments in this thread.''', ); const DiscoverableFeature({ diff --git a/lib/models/preference.dart b/lib/models/preference.dart index e567b13..99dd697 100644 --- a/lib/models/preference.dart +++ b/lib/models/preference.dart @@ -43,6 +43,7 @@ abstract class Preference extends Equatable with SettingsDisplayable { const ReaderModePreference(), const CustomTabPreference(), const EyeCandyModePreference(), + const Material3Preference(), ], ); @@ -73,6 +74,7 @@ const bool _storyUrlModeDefaultValue = true; const bool _collapseModeDefaultValue = true; const bool _autoScrollModeDefaultValue = false; const bool _customTabModeDefaultValue = false; +const bool _material3ModeDefaultValue = false; const double _textScaleFactorDefaultValue = 1; final int _fetchModeDefaultValue = FetchMode.eager.index; final int _commentsOrderDefaultValue = CommentsOrder.natural.index; @@ -285,6 +287,26 @@ class EyeCandyModePreference extends BooleanPreference { String get subtitle => 'some sort of magic.'; } +class Material3Preference extends BooleanPreference { + const Material3Preference({bool? val}) + : super(val: val ?? _material3ModeDefaultValue); + + @override + Material3Preference copyWith({required bool? val}) { + return Material3Preference(val: val); + } + + @override + String get key => 'material3Mode'; + + @override + String get title => 'Enable Material 3'; + + @override + String get subtitle => + '''experiment feature. Please open an issue on GitHub if you notice anything weird.'''; +} + /// Whether or not to use Custom Tabs for launching URLs. /// If false, default browser will be used. /// diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 8d46bf1..1e173b6 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -49,7 +49,7 @@ class _HomeScreenState extends State super.didPopNext(); if (context.read().deviceScreenType == DeviceScreenType.mobile) { - locator.get().i('resetting comments in CommentCache'); + locator.get().i('Resetting comments in CommentCache'); Future.delayed( Durations.ms500, locator.get().resetComments, diff --git a/lib/screens/item/item_screen.dart b/lib/screens/item/item_screen.dart index 490a60f..5e8c3b7 100644 --- a/lib/screens/item/item_screen.dart +++ b/lib/screens/item/item_screen.dart @@ -142,9 +142,6 @@ class _ItemScreenState extends State 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( @@ -187,6 +184,7 @@ class _ItemScreenState extends State DiscoverableFeature.openStoryInWebView.featureId, DiscoverableFeature.jumpUpButton.featureId, DiscoverableFeature.jumpDownButton.featureId, + DiscoverableFeature.searchInThread.featureId, }, ); }) @@ -272,8 +270,6 @@ class _ItemScreenState extends State children: [ Positioned.fill( child: MainView( - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, scrollOffsetListener: scrollOffsetListener, commentEditingController: commentEditingController, authState: authState, @@ -313,13 +309,10 @@ class _ItemScreenState extends State ); }, ), - Positioned( + const Positioned( right: Dimens.pt12, bottom: Dimens.pt36, - child: CustomFloatingActionButton( - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, - ), + child: CustomFloatingActionButton(), ), Positioned( bottom: Dimens.zero, @@ -348,8 +341,6 @@ class _ItemScreenState extends State fontSizeIconButtonKey: fontSizeIconButtonKey, ), body: MainView( - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, scrollOffsetListener: scrollOffsetListener, commentEditingController: commentEditingController, authState: authState, @@ -358,10 +349,7 @@ class _ItemScreenState extends State onMoreTapped: onMoreTapped, onRightMoreTapped: onRightMoreTapped, ), - floatingActionButton: CustomFloatingActionButton( - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, - ), + floatingActionButton: const CustomFloatingActionButton(), bottomSheet: ReplyBox( textEditingController: commentEditingController, focusNode: focusNode, @@ -437,42 +425,36 @@ class _ItemScreenState extends State context: context, builder: (BuildContext context) { return SafeArea( - child: ColoredBox( - color: Theme.of(context).canvasColor, - child: Material( - color: Palette.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.av_timer), - title: const Text('View ancestors'), - onTap: () { - context.pop(); - onTimeMachineActivated(comment); - }, - enabled: - comment.level > 0 && !(comment.dead || comment.deleted), - ), - ListTile( - leading: const Icon(Icons.list), - title: const Text('View in separate thread'), - onTap: () { - locator.get().requestReview(); - context.pop(); - goToItemScreen( - args: ItemScreenArgs( - item: comment, - useCommentCache: true, - ), - forceNewScreen: true, - ); - }, - enabled: !(comment.dead || comment.deleted), - ), - ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.av_timer), + title: const Text('View ancestors'), + onTap: () { + context.pop(); + onTimeMachineActivated(comment); + }, + enabled: + comment.level > 0 && !(comment.dead || comment.deleted), ), - ), + ListTile( + leading: const Icon(Icons.list), + title: const Text('View in separate thread'), + onTap: () { + locator.get().requestReview(); + context.pop(); + goToItemScreen( + args: ItemScreenArgs( + item: comment, + useCommentCache: true, + ), + forceNewScreen: true, + ); + }, + enabled: !(comment.dead || comment.deleted), + ), + ], ), ); }, diff --git a/lib/screens/item/widgets/custom_app_bar.dart b/lib/screens/item/widgets/custom_app_bar.dart index 5f2de7a..6071597 100644 --- a/lib/screens/item/widgets/custom_app_bar.dart +++ b/lib/screens/item/widgets/custom_app_bar.dart @@ -34,6 +34,7 @@ class CustomAppBar extends AppBar { ), const Spacer(), ], + const InThreadSearchIconButton(), IconButton( key: fontSizeIconButtonKey, icon: Text( diff --git a/lib/screens/item/widgets/custom_floating_action_button.dart b/lib/screens/item/widgets/custom_floating_action_button.dart index ec5a2f5..9c4dbb0 100644 --- a/lib/screens/item/widgets/custom_floating_action_button.dart +++ b/lib/screens/item/widgets/custom_floating_action_button.dart @@ -7,18 +7,12 @@ 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'; class CustomFloatingActionButton extends StatelessWidget { const CustomFloatingActionButton({ - required this.itemScrollController, - required this.itemPositionsListener, super.key, }); - final ItemScrollController itemScrollController; - final ItemPositionsListener itemPositionsListener; - @override Widget build(BuildContext context) { return BlocBuilder( @@ -45,10 +39,8 @@ class CustomFloatingActionButton extends StatelessWidget { color: Palette.white, ), child: InkWell( - onLongPress: () => itemScrollController.scrollTo( - index: 0, - duration: Durations.ms400, - ), + onLongPress: () => + context.read().scrollTo(index: 0), child: FloatingActionButton.small( backgroundColor: Theme.of(context).scaffoldBackgroundColor, @@ -58,10 +50,7 @@ class CustomFloatingActionButton extends StatelessWidget { heroTag: UniqueKey().hashCode, onPressed: () { HapticFeedbackUtil.selection(); - context.read().scrollToPreviousRoot( - itemScrollController, - itemPositionsListener, - ); + context.read().scrollToPreviousRoot(); }, child: Icon( Icons.keyboard_arrow_up, @@ -77,10 +66,9 @@ class CustomFloatingActionButton extends StatelessWidget { color: Palette.white, ), child: InkWell( - onLongPress: () => itemScrollController.scrollTo( - index: state.comments.length, - duration: Durations.ms400, - ), + onLongPress: () => context + .read() + .scrollTo(index: state.comments.length), child: FloatingActionButton.small( backgroundColor: Theme.of(context).scaffoldBackgroundColor, @@ -89,10 +77,7 @@ class CustomFloatingActionButton extends StatelessWidget { heroTag: UniqueKey().hashCode, onPressed: () { HapticFeedbackUtil.selection(); - context.read().scrollToNextRoot( - itemScrollController, - itemPositionsListener, - ); + context.read().scrollToNextRoot(); }, child: Icon( Icons.keyboard_arrow_down, diff --git a/lib/screens/item/widgets/in_thread_search_icon_button.dart b/lib/screens/item/widgets/in_thread_search_icon_button.dart new file mode 100644 index 0000000..b291f88 --- /dev/null +++ b/lib/screens/item/widgets/in_thread_search_icon_button.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hacki/cubits/comments/comments_cubit.dart'; +import 'package:hacki/models/models.dart'; +import 'package:hacki/screens/widgets/widgets.dart'; +import 'package:hacki/styles/styles.dart'; + +class InThreadSearchIconButton extends StatelessWidget { + const InThreadSearchIconButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: context.read(), + child: IconButton( + tooltip: 'Search in thread', + icon: const CustomDescribedFeatureOverlay( + tapTarget: Icon( + Icons.search, + color: Palette.white, + ), + feature: DiscoverableFeature.searchInThread, + child: Icon( + Icons.search, + ), + ), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + backgroundColor: Theme.of(context).canvasColor, + builder: (BuildContext _) { + return BlocProvider.value( + value: context.read(), + child: BlocBuilder( + buildWhen: (CommentsState previous, CommentsState current) => + previous.matchedComments != current.matchedComments, + builder: (BuildContext context, CommentsState state) { + return Container( + height: MediaQuery.of(context).size.height - Dimens.pt120, + color: Theme.of(context).canvasColor, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: Dimens.pt8, + ), + child: TextField( + cursorColor: Theme.of(context).primaryColor, + autocorrect: false, + decoration: InputDecoration( + hintText: 'Search in this thread', + suffixText: state.matchedComments.isEmpty + ? '' + : '${state.matchedComments.length} results', + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).primaryColor, + ), + ), + ), + onChanged: context.read().search, + ), + ), + Expanded( + child: ListView( + children: [ + if (state.matchedComments.isEmpty) + const Padding( + padding: EdgeInsets.only(top: Dimens.pt120), + child: CenteredText.empty(), + ) + else + for (final int i in state.matchedComments) + CommentTile( + comment: state.comments.elementAt(i), + fetchMode: FetchMode.lazy, + actionable: false, + onTap: () { + context.pop(); + context.read().scrollTo( + index: i + 1, + alignment: 0.1, + ); + }, + ), + ], + ), + ), + ], + ), + ); + }, + ), + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/screens/item/widgets/link_icon_button.dart b/lib/screens/item/widgets/link_icon_button.dart index a7b9d82..23ec934 100644 --- a/lib/screens/item/widgets/link_icon_button.dart +++ b/lib/screens/item/widgets/link_icon_button.dart @@ -1,4 +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'; @@ -27,7 +28,7 @@ class LinkIconButton extends StatelessWidget { ), ), onPressed: () => LinkUtil.launch( - 'https://news.ycombinator.com/item?id=$storyId', + '${Constants.hackerNewsItemLinkPrefix}$storyId', context, useHackiForHnLink: false, ), diff --git a/lib/screens/item/widgets/main_view.dart b/lib/screens/item/widgets/main_view.dart index 3d973b3..668b8c0 100644 --- a/lib/screens/item/widgets/main_view.dart +++ b/lib/screens/item/widgets/main_view.dart @@ -18,8 +18,6 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class MainView extends StatelessWidget { const MainView({ - required this.itemScrollController, - required this.itemPositionsListener, required this.scrollOffsetListener, required this.commentEditingController, required this.authState, @@ -30,8 +28,6 @@ class MainView extends StatelessWidget { super.key, }); - final ItemScrollController itemScrollController; - final ItemPositionsListener itemPositionsListener; final ScrollOffsetListener scrollOffsetListener; final TextEditingController commentEditingController; final AuthState authState; @@ -67,8 +63,10 @@ class MainView extends StatelessWidget { }, child: ScrollablePositionedList.builder( physics: const AlwaysScrollableScrollPhysics(), - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, + itemScrollController: + context.read().itemScrollController, + itemPositionsListener: + context.read().itemPositionsListener, itemCount: state.comments.length + 2, padding: EdgeInsets.only(top: topPadding), scrollOffsetListener: scrollOffsetListener, @@ -130,7 +128,6 @@ class MainView extends StatelessWidget { }, onMoreTapped: onMoreTapped, onRightMoreTapped: onRightMoreTapped, - itemScrollController: itemScrollController, ), ); }, @@ -185,7 +182,7 @@ class _ParentItemSection extends StatelessWidget { final ValueChanged onRightMoreTapped; static const double _viewParentButtonWidth = 100; - static const double _viewRootButtonWidth = 80; + static const double _viewRootButtonWidth = 85; @override Widget build(BuildContext context) { diff --git a/lib/screens/item/widgets/more_popup_menu.dart b/lib/screens/item/widgets/more_popup_menu.dart index a97ac8a..ced2bca 100644 --- a/lib/screens/item/widgets/more_popup_menu.dart +++ b/lib/screens/item/widgets/more_popup_menu.dart @@ -66,175 +66,167 @@ class MorePopupMenu extends StatelessWidget { builder: (BuildContext context, VoteState voteState) { final bool upvoted = voteState.vote == Vote.up; final bool downvoted = voteState.vote == Vote.down; - return ColoredBox( - color: Theme.of(context).canvasColor, - child: Material( - color: Palette.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - BlocProvider( - create: (BuildContext context) => - UserCubit()..init(userId: item.by), - child: BlocBuilder( - builder: (BuildContext context, UserState state) { - return Semantics( - excludeSemantics: state.status == Status.inProgress, - child: ListTile( - leading: const Icon( - Icons.account_circle, - ), - title: Text(item.by), - subtitle: Text( - state.user.description, - ), - onTap: () { - context.pop(); - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - semanticLabel: - '''About ${state.user.id}. ${state.user.about}''', - title: Text( - 'About ${state.user.id}', - ), - content: state.user.about.isEmpty - ? const Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - 'empty', - style: TextStyle( - color: Palette.grey, - ), - ), - ], - ) - : SelectableLinkify( - text: HtmlUtil.parseHtml( - state.user.about, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + BlocProvider( + create: (BuildContext context) => + UserCubit()..init(userId: item.by), + child: BlocBuilder( + builder: (BuildContext context, UserState state) { + return Semantics( + excludeSemantics: state.status == Status.inProgress, + child: ListTile( + leading: const Icon( + Icons.account_circle, + ), + title: Text(item.by), + subtitle: Text( + state.user.description, + ), + onTap: () { + context.pop(); + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + semanticLabel: + '''About ${state.user.id}. ${state.user.about}''', + title: Text( + 'About ${state.user.id}', + ), + content: state.user.about.isEmpty + ? const Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + 'empty', + style: TextStyle( + color: Palette.grey, ), - linkStyle: TextStyle( - color: - Theme.of(context).primaryColor, - ), - onOpen: (LinkableElement link) => - LinkUtil.launch( - link.url, - context, - ), - semanticsLabel: state.user.about, ), - actions: [ - TextButton( - onPressed: () { - locator - .get() - .requestReview(); - context.pop(); - onSearchUserTapped(context); - }, - child: const Text( - 'Search', + ], + ) + : SelectableLinkify( + text: HtmlUtil.parseHtml( + state.user.about, ), - ), - TextButton( - onPressed: () { - locator - .get() - .requestReview(); - context.pop(); - }, - child: const Text( - 'Okay', + linkStyle: TextStyle( + color: Theme.of(context).primaryColor, ), + onOpen: (LinkableElement link) => + LinkUtil.launch( + link.url, + context, + ), + semanticsLabel: state.user.about, ), - ], + actions: [ + TextButton( + onPressed: () { + locator + .get() + .requestReview(); + context.pop(); + onSearchUserTapped(context); + }, + child: const Text( + 'Search', + ), ), - ); - }, - ), - ); - }, - ), - ), - ListTile( - leading: Icon( - FeatherIcons.chevronUp, - color: upvoted ? Theme.of(context).primaryColor : null, - ), - title: Text( - upvoted ? 'Upvoted' : 'Upvote', - style: upvoted - ? TextStyle(color: Theme.of(context).primaryColor) - : null, - ), - subtitle: - item is Story ? Text(item.score.toString()) : null, - onTap: context.read().upvote, - ), - ListTile( - leading: Icon( - FeatherIcons.chevronDown, - color: downvoted ? Theme.of(context).primaryColor : null, - ), - title: Text( - downvoted ? 'Downvoted' : 'Downvote', - style: downvoted - ? TextStyle(color: Theme.of(context).primaryColor) - : null, - ), - onTap: context.read().downvote, - ), - BlocBuilder( - builder: (BuildContext context, FavState state) { - final bool isFav = state.favIds.contains(item.id); - return ListTile( - leading: Icon( - isFav ? Icons.favorite : Icons.favorite_border, - color: isFav ? Theme.of(context).primaryColor : null, - ), - title: Text( - isFav ? 'Unfavorite' : 'Favorite', - ), - onTap: () => context.pop(MenuAction.fav), - ); - }, - ), - ListTile( - leading: const Icon(FeatherIcons.share), - title: const Text( - 'Share', - ), - onTap: () => context.pop(MenuAction.share), - ), - ListTile( - leading: const Icon(Icons.local_police), - title: const Text( - 'Flag', - ), - onTap: () => context.pop(MenuAction.flag), - ), - ListTile( - leading: Icon( - isBlocked ? Icons.visibility : Icons.visibility_off, - ), - title: Text( - isBlocked ? 'Unblock' : 'Block', - ), - onTap: () => context.pop(MenuAction.block), - ), - ListTile( - leading: const Icon(Icons.close), - title: const Text( - 'Cancel', - ), - onTap: () => context.pop(MenuAction.cancel), - ), - ], + TextButton( + onPressed: () { + locator + .get() + .requestReview(); + context.pop(); + }, + child: const Text( + 'Okay', + ), + ), + ], + ), + ); + }, + ), + ); + }, + ), ), - ), + ListTile( + leading: Icon( + FeatherIcons.chevronUp, + color: upvoted ? Theme.of(context).primaryColor : null, + ), + title: Text( + upvoted ? 'Upvoted' : 'Upvote', + style: upvoted + ? TextStyle(color: Theme.of(context).primaryColor) + : null, + ), + subtitle: item is Story ? Text(item.score.toString()) : null, + onTap: context.read().upvote, + ), + ListTile( + leading: Icon( + FeatherIcons.chevronDown, + color: downvoted ? Theme.of(context).primaryColor : null, + ), + title: Text( + downvoted ? 'Downvoted' : 'Downvote', + style: downvoted + ? TextStyle(color: Theme.of(context).primaryColor) + : null, + ), + onTap: context.read().downvote, + ), + BlocBuilder( + builder: (BuildContext context, FavState state) { + final bool isFav = state.favIds.contains(item.id); + return ListTile( + leading: Icon( + isFav ? Icons.favorite : Icons.favorite_border, + color: isFav ? Theme.of(context).primaryColor : null, + ), + title: Text( + isFav ? 'Unfavorite' : 'Favorite', + ), + onTap: () => context.pop(MenuAction.fav), + ); + }, + ), + ListTile( + leading: const Icon(FeatherIcons.share), + title: const Text( + 'Share', + ), + onTap: () => context.pop(MenuAction.share), + ), + ListTile( + leading: const Icon(Icons.local_police), + title: const Text( + 'Flag', + ), + onTap: () => context.pop(MenuAction.flag), + ), + ListTile( + leading: Icon( + isBlocked ? Icons.visibility : Icons.visibility_off, + ), + title: Text( + isBlocked ? 'Unblock' : 'Block', + ), + onTap: () => context.pop(MenuAction.block), + ), + ListTile( + leading: const Icon(Icons.close), + title: const Text( + 'Cancel', + ), + onTap: () => context.pop(MenuAction.cancel), + ), + ], ); }, ), @@ -245,6 +237,8 @@ class MorePopupMenu extends StatelessWidget { showModalBottomSheet( context: context, isScrollControlled: true, + backgroundColor: Theme.of(context).canvasColor, + showDragHandle: true, builder: (BuildContext context) { return BlocProvider( create: (_) => SearchCubit() @@ -256,19 +250,10 @@ class MorePopupMenu extends StatelessWidget { child: Container( height: MediaQuery.of(context).size.height - Dimens.pt120, color: Theme.of(context).canvasColor, - margin: const EdgeInsets.only(top: Dimens.pt12), - child: Material( + child: const Material( child: Column( children: [ - Container( - height: Dimens.pt4, - width: Dimens.pt24, - decoration: BoxDecoration( - color: Palette.grey, - borderRadius: BorderRadius.circular(Dimens.pt16), - ), - ), - const Expanded( + Expanded( child: SearchScreen( fromUserDialog: true, ), diff --git a/lib/screens/item/widgets/widgets.dart b/lib/screens/item/widgets/widgets.dart index 7c6a9ee..1bfe38a 100644 --- a/lib/screens/item/widgets/widgets.dart +++ b/lib/screens/item/widgets/widgets.dart @@ -1,6 +1,7 @@ export 'custom_app_bar.dart'; export 'custom_floating_action_button.dart'; export 'fav_icon_button.dart'; +export 'in_thread_search_icon_button.dart'; export 'link_icon_button.dart'; export 'login_dialog.dart'; export 'main_view.dart'; diff --git a/lib/screens/widgets/centered_text.dart b/lib/screens/widgets/centered_text.dart index cd2486a..2a84b2e 100644 --- a/lib/screens/widgets/centered_text.dart +++ b/lib/screens/widgets/centered_text.dart @@ -32,6 +32,12 @@ class CenteredText extends StatelessWidget { text: 'blocked', ); + const CenteredText.empty({Key? key}) + : this( + key: key, + text: 'empty', + ); + final String text; final Color color; diff --git a/lib/screens/widgets/comment_tile.dart b/lib/screens/widgets/comment_tile.dart index 419a03c..8792f0f 100644 --- a/lib/screens/widgets/comment_tile.dart +++ b/lib/screens/widgets/comment_tile.dart @@ -10,7 +10,6 @@ import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/services/services.dart'; import 'package:hacki/styles/styles.dart'; import 'package:hacki/utils/utils.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class CommentTile extends StatelessWidget { const CommentTile({ @@ -27,7 +26,6 @@ class CommentTile extends StatelessWidget { this.selectable = true, this.level = 0, this.onTap, - this.itemScrollController, }); final String? opUsername; @@ -37,7 +35,6 @@ class CommentTile extends StatelessWidget { final bool collapsable; final bool selectable; final FetchMode fetchMode; - final ItemScrollController? itemScrollController; final void Function(Comment)? onReplyTapped; final void Function(Comment, Rect?)? onMoreTapped; @@ -370,14 +367,14 @@ class CommentTile extends StatelessWidget { ..collapse(onStateChanged: HapticFeedbackUtil.selection); if (collapseCubit.state.collapsed && preferenceCubit.state.autoScrollEnabled) { - final List comments = - context.read().state.comments; + final CommentsCubit commentsCubit = context.read(); + final List comments = commentsCubit.state.comments; final int indexOfNextComment = comments.indexOf(comment) + 1; if (indexOfNextComment < comments.length) { Future.delayed( Durations.ms300, () { - itemScrollController?.scrollTo( + commentsCubit.itemScrollController.scrollTo( index: indexOfNextComment, alignment: 0.1, duration: Durations.ms300, diff --git a/lib/screens/widgets/custom_chip.dart b/lib/screens/widgets/custom_chip.dart index ffa8583..9f23e8c 100644 --- a/lib/screens/widgets/custom_chip.dart +++ b/lib/screens/widgets/custom_chip.dart @@ -15,13 +15,19 @@ class CustomChip extends StatelessWidget { @override Widget build(BuildContext context) { + final bool useMaterial3 = Theme.of(context).useMaterial3; return FilterChip( shadowColor: Palette.transparent, selectedShadowColor: Palette.transparent, backgroundColor: Palette.transparent, - shape: StadiumBorder( - side: BorderSide(color: Theme.of(context).primaryColor), - ), + side: useMaterial3 && !selected + ? BorderSide(color: Theme.of(context).colorScheme.onSurface) + : null, + shape: Theme.of(context).useMaterial3 + ? null + : StadiumBorder( + side: BorderSide(color: Theme.of(context).primaryColor), + ), label: Text(label), labelStyle: TextStyle( color: selected ? Theme.of(context).colorScheme.onPrimary : null, diff --git a/lib/screens/widgets/tap_down_wrapper.dart b/lib/screens/widgets/tap_down_wrapper.dart index e044159..791ab42 100644 --- a/lib/screens/widgets/tap_down_wrapper.dart +++ b/lib/screens/widgets/tap_down_wrapper.dart @@ -37,6 +37,7 @@ class _TapDownWrapperState extends State onTapDown: onTapDown, onTapUp: onTapUp, onTapCancel: onTapCancel, + behavior: HitTestBehavior.opaque, child: AnimatedBuilder( animation: CurvedAnimation(parent: controller, curve: Curves.decelerate), diff --git a/pubspec.yaml b/pubspec.yaml index 2d5d279..42d4c53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: hacki description: A Hacker News reader. -version: 2.0.1+126 +version: 2.1.0+127 publish_to: none environment: