From b10bfd47d92bd29fbe81f1d8f80e20d8b3f081d4 Mon Sep 17 00:00:00 2001 From: livinglist Date: Wed, 1 Apr 2026 03:12:32 -0700 Subject: [PATCH 1/8] update --- lib/config/paths.dart | 13 +- .../{custom_router.dart => router.dart} | 6 + lib/cubits/comments/comments_cubit.dart | 2 +- lib/main.dart | 2 +- lib/models/discoverable_feature.dart | 8 +- lib/models/preference.dart | 2 +- lib/screens/home/home_screen.dart | 2 +- lib/screens/item/item_screen.dart | 2 +- lib/screens/item/widgets/custom_app_bar.dart | 4 +- lib/screens/item/widgets/main_view.dart | 1 + ..._icon_button.dart => settings_button.dart} | 25 +- lib/screens/item/widgets/widgets.dart | 2 +- lib/screens/profile/profile_screen.dart | 6 +- lib/screens/profile/widgets/widgets.dart | 5 - lib/screens/screens.dart | 1 + .../settings_screen.dart} | 917 +++++++++--------- .../widgets/enter_offline_mode_list_tile.dart | 0 .../widgets/offline_list_tile.dart | 0 .../widgets/tab_bar_settings.dart | 0 .../widgets/text_scale_factor_settings.dart | 0 lib/screens/settings/widgets/widgets.dart | 4 + .../custom_linkify/custom_linkify.dart | 2 +- .../widgets/tips/item_screen_tips.dart | 1 - lib/screens/widgets/tips/tips.dart | 1 + lib/services/dialog_proxy.dart | 2 +- lib/utils/link_utils.dart | 2 +- lib/utils/utils.dart | 1 + lib/utils/widget_utils.dart | 70 ++ 28 files changed, 586 insertions(+), 495 deletions(-) rename lib/config/{custom_router.dart => router.dart} (95%) rename lib/screens/item/widgets/{link_icon_button.dart => settings_button.dart} (53%) rename lib/screens/{profile/widgets/settings.dart => settings/settings_screen.dart} (55%) rename lib/screens/{profile => settings}/widgets/enter_offline_mode_list_tile.dart (100%) rename lib/screens/{profile => settings}/widgets/offline_list_tile.dart (100%) rename lib/screens/{profile => settings}/widgets/tab_bar_settings.dart (100%) rename lib/screens/{profile => settings}/widgets/text_scale_factor_settings.dart (100%) create mode 100644 lib/screens/settings/widgets/widgets.dart create mode 100644 lib/utils/widget_utils.dart diff --git a/lib/config/paths.dart b/lib/config/paths.dart index aaf2de8f..f74a45e6 100644 --- a/lib/config/paths.dart +++ b/lib/config/paths.dart @@ -1,12 +1,13 @@ import 'package:hacki/screens/screens.dart'; -abstract class Paths { +abstract final class Paths { static const LogsPaths logs = LogsPaths._(); static const HomePaths home = HomePaths._(); static const ItemPaths item = ItemPaths._(); static const SharePaths share = SharePaths._(); static const QrCodePaths qrCode = QrCodePaths._(); static const WebViewPaths webView = WebViewPaths._(); + static const SettingsPaths settings = SettingsPaths._(); } class HomePaths with RootPaths { @@ -21,6 +22,16 @@ class ItemPaths with RootPaths { String get landing => rootPath(ItemScreen.routeName); String get submit => rootPath(SubmitScreen.routeName); + + String get settings => '$landing$settingsSegment'; + + static const String settingsSegment = '/${SettingsScreen.routeName}'; +} + +class SettingsPaths with RootPaths { + const SettingsPaths._(); + + String get landing => rootPath(SettingsScreen.routeName); } class SharePaths with RootPaths { diff --git a/lib/config/custom_router.dart b/lib/config/router.dart similarity index 95% rename from lib/config/custom_router.dart rename to lib/config/router.dart index 425b218d..735b2692 100644 --- a/lib/config/custom_router.dart +++ b/lib/config/router.dart @@ -31,6 +31,12 @@ final GoRouter router = GoRouter( } return ItemScreen.phone(args); }, + routes: [ + GoRoute( + path: SettingsScreen.routeName, + builder: (_, __) => const SettingsScreen(), + ), + ], ), GoRoute( path: '${ItemScreen.routeName}/:itemId', diff --git a/lib/cubits/comments/comments_cubit.dart b/lib/cubits/comments/comments_cubit.dart index ab132846..b7a791a2 100644 --- a/lib/cubits/comments/comments_cubit.dart +++ b/lib/cubits/comments/comments_cubit.dart @@ -8,9 +8,9 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:hacki/config/constants.dart'; -import 'package:hacki/config/custom_router.dart'; import 'package:hacki/config/locator.dart'; import 'package:hacki/config/paths.dart'; +import 'package:hacki/config/router.dart'; import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/models/models.dart'; diff --git a/lib/main.dart b/lib/main.dart index 95c1918b..40122ed6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,9 +13,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.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/config/paths.dart'; +import 'package:hacki/config/router.dart'; import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/services/fetcher.dart'; diff --git a/lib/models/discoverable_feature.dart b/lib/models/discoverable_feature.dart index fad135f2..5acbf15b 100644 --- a/lib/models/discoverable_feature.dart +++ b/lib/models/discoverable_feature.dart @@ -4,10 +4,10 @@ enum DiscoverableFeature { 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.''', + settingsShortcutOnItemScreen( + featureId: 'settings_shortcut_on_item_screen', + title: 'Go to Settings', + description: '''You can now go to settings page directly from a thread.''', ), login( featureId: 'log_in', diff --git a/lib/models/preference.dart b/lib/models/preference.dart index a96f12d5..6c62dc85 100644 --- a/lib/models/preference.dart +++ b/lib/models/preference.dart @@ -4,7 +4,7 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_material_color_picker/flutter_material_color_picker.dart'; -import 'package:hacki/config/custom_router.dart'; +import 'package:hacki/config/router.dart'; import 'package:hacki/models/models.dart'; import 'package:hacki/styles/palette.dart'; import 'package:responsive_builder/responsive_builder.dart'; diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 4006f411..e33d6735 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -325,7 +325,7 @@ class _HomeScreenState extends State FeatureDiscovery.clearPreferences(context, [ DiscoverableFeature.login.featureId, DiscoverableFeature.addStoryToFavList.featureId, - DiscoverableFeature.openStoryInWebView.featureId, + DiscoverableFeature.settingsShortcutOnItemScreen.featureId, DiscoverableFeature.pinToTop.featureId, ]); } diff --git a/lib/screens/item/item_screen.dart b/lib/screens/item/item_screen.dart index 7a65a0f3..03fa5cb5 100644 --- a/lib/screens/item/item_screen.dart +++ b/lib/screens/item/item_screen.dart @@ -201,7 +201,7 @@ class _ItemScreenState extends State DiscoverableFeature.searchInThread.featureId, DiscoverableFeature.pinToTop.featureId, DiscoverableFeature.addStoryToFavList.featureId, - DiscoverableFeature.openStoryInWebView.featureId, + DiscoverableFeature.settingsShortcutOnItemScreen.featureId, DiscoverableFeature.jumpUpButton.featureId, DiscoverableFeature.jumpDownButton.featureId, }, diff --git a/lib/screens/item/widgets/custom_app_bar.dart b/lib/screens/item/widgets/custom_app_bar.dart index a4074d59..63b1d36a 100644 --- a/lib/screens/item/widgets/custom_app_bar.dart +++ b/lib/screens/item/widgets/custom_app_bar.dart @@ -70,9 +70,7 @@ class CustomAppBar extends AppBar { FavIconButton( storyId: item.id, ), - LinkIconButton( - storyId: item.id, - ), + const SettingsButton(), ], ); } diff --git a/lib/screens/item/widgets/main_view.dart b/lib/screens/item/widgets/main_view.dart index 467c0d5e..38d7be66 100644 --- a/lib/screens/item/widgets/main_view.dart +++ b/lib/screens/item/widgets/main_view.dart @@ -89,6 +89,7 @@ class MainView extends StatelessWidget { context.read().itemPositionsListener, itemCount: state.comments.length + 2, scrollOffsetListener: scrollOffsetListener, + minCacheExtent: WidgetUtils.preferredCacheExtent, itemBuilder: (BuildContext context, int index) { if (index == 0) { return Material( diff --git a/lib/screens/item/widgets/link_icon_button.dart b/lib/screens/item/widgets/settings_button.dart similarity index 53% rename from lib/screens/item/widgets/link_icon_button.dart rename to lib/screens/item/widgets/settings_button.dart index e521dba3..e62bfc3e 100644 --- a/lib/screens/item/widgets/link_icon_button.dart +++ b/lib/screens/item/widgets/settings_button.dart @@ -1,39 +1,32 @@ import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter/material.dart'; -import 'package:hacki/config/constants.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hacki/config/paths.dart'; import 'package:hacki/models/discoverable_feature.dart'; import 'package:hacki/screens/widgets/widgets.dart'; -import 'package:hacki/utils/utils.dart'; -class LinkIconButton extends StatelessWidget { - const LinkIconButton({ - required this.storyId, +class SettingsButton extends StatelessWidget { + const SettingsButton({ super.key, }); - final int storyId; - @override Widget build(BuildContext context) { return IconButton( - tooltip: 'Open this story in browser', + tooltip: 'Go to settings', icon: CustomDescribedFeatureOverlay( tapTarget: Icon( - Icons.stream, + Icons.settings, color: Theme.of(context).colorScheme.onPrimaryContainer, ), - feature: DiscoverableFeature.openStoryInWebView, + feature: DiscoverableFeature.settingsShortcutOnItemScreen, contentLocation: ContentLocation.below, child: Icon( - Icons.stream, + Icons.settings, color: Theme.of(context).colorScheme.onSurface, ), ), - onPressed: () => LinkUtils.launch( - '${Constants.hackerNewsItemLinkPrefix}$storyId', - context, - shouldUseHackiForHnLink: false, - ), + onPressed: () => context.push(Paths.item.settings), ); } } diff --git a/lib/screens/item/widgets/widgets.dart b/lib/screens/item/widgets/widgets.dart index 515b6ba1..0ac53f8e 100644 --- a/lib/screens/item/widgets/widgets.dart +++ b/lib/screens/item/widgets/widgets.dart @@ -4,11 +4,11 @@ export 'fav_icon_button.dart'; export 'in_thread_search_icon_button.dart'; export 'item_screen_background.dart'; export 'lazy_fetch_load_button.dart'; -export 'link_icon_button.dart'; export 'login_dialog.dart'; export 'main_view.dart'; export 'more_popup_menu.dart'; export 'pin_icon_button.dart'; export 'poll_view.dart'; export 'reply_box.dart'; +export 'settings_button.dart'; export 'time_machine_dialog.dart'; diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index 693baa27..0caefef1 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -190,9 +190,9 @@ class _ProfileScreenState extends State ), ), ), - Settings( - authState: authState, - pageType: pageType, + Visibility( + visible: pageType == PageType.settings, + child: const SettingsView(), ), Align( alignment: Alignment.topLeft, diff --git a/lib/screens/profile/widgets/widgets.dart b/lib/screens/profile/widgets/widgets.dart index 14ffaf50..8aa1d6a6 100644 --- a/lib/screens/profile/widgets/widgets.dart +++ b/lib/screens/profile/widgets/widgets.dart @@ -1,8 +1,3 @@ export 'centered_message_view.dart'; -export 'enter_offline_mode_list_tile.dart'; export 'favorites_screen.dart'; export 'inbox_view.dart'; -export 'offline_list_tile.dart'; -export 'settings.dart'; -export 'tab_bar_settings.dart'; -export 'text_scale_factor_settings.dart'; diff --git a/lib/screens/screens.dart b/lib/screens/screens.dart index a3148d0e..3ec49448 100644 --- a/lib/screens/screens.dart +++ b/lib/screens/screens.dart @@ -5,6 +5,7 @@ export 'profile/profile_screen.dart'; export 'profile/qr_code_scanner_screen.dart'; export 'profile/qr_code_view_screen.dart'; export 'search/search_screen.dart'; +export 'settings/settings_screen.dart'; export 'share/share_screen.dart'; export 'submit/submit_screen.dart'; export 'web_view/web_view_screen.dart'; diff --git a/lib/screens/profile/widgets/settings.dart b/lib/screens/settings/settings_screen.dart similarity index 55% rename from lib/screens/profile/widgets/settings.dart rename to lib/screens/settings/settings_screen.dart index 037e67df..4940177f 100644 --- a/lib/screens/profile/widgets/settings.dart +++ b/lib/screens/settings/settings_screen.dart @@ -17,18 +17,15 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.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/config/paths.dart'; +import 'package:hacki/config/router.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/profile/models/page_type.dart'; -import 'package:hacki/screens/profile/widgets/enter_offline_mode_list_tile.dart'; -import 'package:hacki/screens/profile/widgets/offline_list_tile.dart'; -import 'package:hacki/screens/profile/widgets/tab_bar_settings.dart'; -import 'package:hacki/screens/profile/widgets/text_scale_factor_settings.dart'; +import 'package:hacki/screens/home/home_screen.dart'; +import 'package:hacki/screens/settings/widgets/widgets.dart'; import 'package:hacki/screens/widgets/widgets.dart'; import 'package:hacki/styles/styles.dart'; import 'package:hacki/utils/utils.dart'; @@ -36,516 +33,530 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; -class Settings extends StatefulWidget { - const Settings({ - required this.authState, - required this.pageType, +class SettingsScreen extends StatefulWidget { + const SettingsScreen({ super.key, }); - final AuthState authState; - final PageType? pageType; + static const String routeName = 'settings'; @override - State createState() => _SettingsState(); + State createState() => _SettingsScreenState(); } -class _SettingsState extends State with ItemActionMixin, Loggable { +class _SettingsScreenState extends State with ItemActionMixin { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Settings'), + ), + body: const SettingsView(), + ); + } +} + +class SettingsView extends StatefulWidget { + const SettingsView({ + super.key, + }); + + @override + State createState() => _SettingsViewState(); +} + +class _SettingsViewState extends State + with ItemActionMixin, Loggable { @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, PreferenceState preferenceState) { + final AuthState authState = context.read().state; + final bool isLoggedIn = authState.isLoggedIn; return Positioned.fill( top: preferenceState.isHackerNewsThemeEnabled ? Dimens.pt64 : Dimens.pt50, - child: Visibility( - visible: widget.pageType == PageType.settings, - child: SingleChildScrollView( - child: Column( - children: [ - ListTile( - leading: Icon( - Icons.person, - color: widget.authState.isLoggedIn - ? Theme.of(context).colorScheme.primary - : null, - ), - title: Text( - widget.authState.isLoggedIn ? 'Log Out' : 'Log In', - ), - subtitle: widget.authState.isLoggedIn - ? Text(widget.authState.username) + child: SingleChildScrollView( + child: Column( + children: [ + ListTile( + leading: Icon( + Icons.person, + color: isLoggedIn + ? Theme.of(context).colorScheme.primary : null, - onTap: () { - if (widget.authState.isLoggedIn) { - onLogoutTapped(); - } else { - onLoginTapped(); - } - }, ), - const EnterOfflineModeListTile(), - const OfflineListTile(), - const SizedBox( - height: Dimens.pt8, + title: Text( + isLoggedIn ? 'Log Out' : 'Log In', ), - OverflowBar( - alignment: MainAxisAlignment.spaceBetween, - overflowSpacing: Dimens.pt12, - children: [ - Padding( - padding: const EdgeInsets.only( - left: Dimens.pt16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Default fetch mode'), - DropdownMenu( - initialSelection: preferenceState.fetchMode, - dropdownMenuEntries: FetchMode.values - .map( - (FetchMode val) => - DropdownMenuEntry( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (FetchMode? fetchMode) { - if (fetchMode != null) { - HapticFeedbackUtils.selection(); - context.read().update( - FetchModePreference( - val: fetchMode.index, - ), - ); - } - }, - ), - ], - ), + subtitle: isLoggedIn + ? Text( + context.read().state.username, + ) + : null, + onTap: () { + if (isLoggedIn) { + onLogoutTapped(); + } else { + onLoginTapped(); + } + }, + ), + const EnterOfflineModeListTile(), + const OfflineListTile(), + const SizedBox( + height: Dimens.pt8, + ), + OverflowBar( + alignment: MainAxisAlignment.spaceBetween, + overflowSpacing: Dimens.pt12, + children: [ + Padding( + padding: const EdgeInsets.only( + left: Dimens.pt16, ), - Padding( - padding: const EdgeInsets.only( - left: Dimens.pt16, - right: Dimens.pt16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Default comments order'), - DropdownMenu( - initialSelection: preferenceState.order, - dropdownMenuEntries: CommentsOrder.values - .map( - (CommentsOrder val) => - DropdownMenuEntry( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (CommentsOrder? order) { - if (order != null) { - HapticFeedbackUtils.selection(); - context.read().update( - CommentsOrderPreference( - val: order.index, - ), - ); - } - }, - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Default fetch mode'), + DropdownMenu( + initialSelection: preferenceState.fetchMode, + dropdownMenuEntries: FetchMode.values + .map( + (FetchMode val) => + DropdownMenuEntry( + value: val, + label: val.description, + ), + ) + .toList(), + onSelected: (FetchMode? fetchMode) { + if (fetchMode != null) { + HapticFeedbackUtils.selection(); + context.read().update( + FetchModePreference( + val: fetchMode.index, + ), + ); + } + }, + ), + ], ), - ], - ), - const SizedBox( - height: Dimens.pt12, - ), - OverflowBar( - alignment: MainAxisAlignment.spaceBetween, - overflowSpacing: Dimens.pt12, - children: [ - Padding( - padding: const EdgeInsets.only( - left: Dimens.pt16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Date time display of comments', - ), - DropdownMenu( - initialSelection: - preferenceState.displayDateFormat, - dropdownMenuEntries: DateDisplayFormat.values - .map( - (DateDisplayFormat val) => - DropdownMenuEntry( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (DateDisplayFormat? order) { - if (order != null) { - HapticFeedbackUtils.selection(); - context.read().update( - DateFormatPreference( - val: order.index, - ), - ); - DateDisplayFormat.clearCache(); - } - }, - ), - ], - ), + ), + Padding( + padding: const EdgeInsets.only( + left: Dimens.pt16, + right: Dimens.pt16, ), - Padding( - padding: const EdgeInsets.only( - left: Dimens.pt16, - right: Dimens.pt16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Data source', - ), - BlocSelector( - selector: (StoriesState state) => - state.statusByType.values.any( - (Status status) => status == Status.inProgress, - ), - builder: ( - BuildContext context, - bool isInProgress, - ) { - return DropdownMenu( - initialSelection: preferenceState.dataSource, - dropdownMenuEntries: - HackerNewsDataSource.values - .map( - (HackerNewsDataSource val) => - DropdownMenuEntry< - HackerNewsDataSource>( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (HackerNewsDataSource? source) { - if (source != null) { - HapticFeedbackUtils.selection(); - context.read().update( - HackerNewsDataSourcePreference( - val: source.index, - ), - ); - } - }, - ); - }, - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Default comments order'), + DropdownMenu( + initialSelection: preferenceState.order, + dropdownMenuEntries: CommentsOrder.values + .map( + (CommentsOrder val) => + DropdownMenuEntry( + value: val, + label: val.description, + ), + ) + .toList(), + onSelected: (CommentsOrder? order) { + if (order != null) { + HapticFeedbackUtils.selection(); + context.read().update( + CommentsOrderPreference( + val: order.index, + ), + ); + } + }, + ), + ], ), - ], - ), - const SizedBox( - height: Dimens.pt12, - ), - const TabBarSettings(), - const TextScaleFactorSettings(), - const Divider(), - StoryTile( - shouldShowWebPreview: - preferenceState.isRichStoryTileEnabled, - shouldShowMetadata: preferenceState.isMetadataEnabled, - shouldShowUrl: preferenceState.isUrlEnabled, - shouldShowFavicon: preferenceState.isFaviconEnabled, - shouldShowPreviewImage: - preferenceState.isStoryTilePreviewImageEnabled, - isExpandedTileEnabled: - preferenceState.isExpandedTileEnabled, - isIndexedStoryTileEnabled: - preferenceState.isIndexedStoryTileEnabled, - isImageLeftAligned: - preferenceState.isPreviewImageLeftAligned, - index: 0, - story: Story.placeholder(), - onTap: () => LinkUtils.launch( - Constants.guidelineLink, - context, ), - ), - const Divider(), - for (final Preference preference - in preferenceState.settingsPreferences) ...[ - if (preference is DividerPlaceholder) - SizedBox( - height: Dimens.pt36, - child: Flex( - mainAxisAlignment: MainAxisAlignment.center, - direction: Axis.horizontal, - children: [ - SizedBoxes.pt12, - const Flexible( - child: Divider(), - ), - SizedBoxes.pt12, - Text(preference.label), - SizedBoxes.pt12, - const Flexible( - child: Divider(), - ), - SizedBoxes.pt12, - ], - ), - ) - else if (preference is PreviewImageAlignmentPreference) - FadeIn( - child: ListTile( - enabled: preference.dependencies - .satisfy(preferenceState.preferences), - title: Text(preference.title), - trailing: SegmentedButton( - showSelectedIcon: false, - segments: const >[ - ButtonSegment( - value: true, - label: Text('Left'), - ), - ButtonSegment( - value: false, - label: Text('Right'), - ), - ], - selected: { - preferenceState.isOn( - preference as BooleanPreference, - ), + ], + ), + const SizedBox( + height: Dimens.pt12, + ), + OverflowBar( + alignment: MainAxisAlignment.spaceBetween, + overflowSpacing: Dimens.pt12, + children: [ + Padding( + padding: const EdgeInsets.only( + left: Dimens.pt16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Date time display of comments', + ), + DropdownMenu( + initialSelection: preferenceState.displayDateFormat, + dropdownMenuEntries: DateDisplayFormat.values + .map( + (DateDisplayFormat val) => + DropdownMenuEntry( + value: val, + label: val.description, + ), + ) + .toList(), + onSelected: (DateDisplayFormat? order) { + if (order != null) { + HapticFeedbackUtils.selection(); + context.read().update( + DateFormatPreference( + val: order.index, + ), + ); + DateDisplayFormat.clearCache(); + } }, - onSelectionChanged: preference.dependencies - .satisfy(preferenceState.preferences) - ? (Set val) { - HapticFeedbackUtils.light(); + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: Dimens.pt16, + right: Dimens.pt16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Data source', + ), + BlocSelector( + selector: (StoriesState state) => + state.statusByType.values.any( + (Status status) => status == Status.inProgress, + ), + builder: ( + BuildContext context, + bool isInProgress, + ) { + return DropdownMenu( + initialSelection: preferenceState.dataSource, + dropdownMenuEntries: HackerNewsDataSource.values + .map( + (HackerNewsDataSource val) => + DropdownMenuEntry< + HackerNewsDataSource>( + value: val, + label: val.description, + ), + ) + .toList(), + onSelected: (HackerNewsDataSource? source) { + if (source != null) { + HapticFeedbackUtils.selection(); context.read().update( - preference.copyWith(val: val.single), + HackerNewsDataSourcePreference( + val: source.index, + ), ); } - : null, + }, + ); + }, ), - ), - ) - else - SwitchListTile( - key: ValueKey(preference.key), + ], + ), + ), + ], + ), + const SizedBox( + height: Dimens.pt12, + ), + const TabBarSettings(), + const TextScaleFactorSettings(), + const Divider(), + StoryTile( + shouldShowWebPreview: preferenceState.isRichStoryTileEnabled, + shouldShowMetadata: preferenceState.isMetadataEnabled, + shouldShowUrl: preferenceState.isUrlEnabled, + shouldShowFavicon: preferenceState.isFaviconEnabled, + shouldShowPreviewImage: + preferenceState.isStoryTilePreviewImageEnabled, + isExpandedTileEnabled: preferenceState.isExpandedTileEnabled, + isIndexedStoryTileEnabled: + preferenceState.isIndexedStoryTileEnabled, + isImageLeftAligned: preferenceState.isPreviewImageLeftAligned, + index: 0, + story: Story.placeholder(), + onTap: () => LinkUtils.launch( + Constants.guidelineLink, + context, + ), + ), + const Divider(), + for (final Preference preference + in preferenceState.settingsPreferences) ...[ + if (preference is DividerPlaceholder) + SizedBox( + height: Dimens.pt36, + child: Flex( + mainAxisAlignment: MainAxisAlignment.center, + direction: Axis.horizontal, + children: [ + SizedBoxes.pt12, + const Flexible( + child: Divider(), + ), + SizedBoxes.pt12, + Text(preference.label), + SizedBoxes.pt12, + const Flexible( + child: Divider(), + ), + SizedBoxes.pt12, + ], + ), + ) + else if (preference is PreviewImageAlignmentPreference) + FadeIn( + child: ListTile( + enabled: preference.dependencies + .satisfy(preferenceState.preferences), title: Text(preference.title), - subtitle: preference.subtitle.isNotEmpty - ? Text(preference.subtitle) - : null, - value: preferenceState.isOn( - preference as BooleanPreference, + trailing: SegmentedButton( + showSelectedIcon: false, + segments: const >[ + ButtonSegment( + value: true, + label: Text('Left'), + ), + ButtonSegment( + value: false, + label: Text('Right'), + ), + ], + selected: { + preferenceState.isOn( + preference as BooleanPreference, + ), + }, + onSelectionChanged: preference.dependencies + .satisfy(preferenceState.preferences) + ? (Set val) { + HapticFeedbackUtils.light(); + context.read().update( + preference.copyWith(val: val.single), + ); + } + : null, ), - onChanged: preference.dependencies - .satisfy(preferenceState.preferences) - ? (bool val) { - HapticFeedbackUtils.light(); + ), + ) + else + SwitchListTile( + key: ValueKey(preference.key), + title: Text(preference.title), + subtitle: preference.subtitle.isNotEmpty + ? Text(preference.subtitle) + : null, + value: preferenceState.isOn( + preference as BooleanPreference, + ), + onChanged: preference.dependencies + .satisfy(preferenceState.preferences) + ? (bool val) { + HapticFeedbackUtils.light(); - context - .read() - .update(preference.copyWith(val: val)); + context + .read() + .update(preference.copyWith(val: val)); - if (preference - is MarkReadStoriesModePreference && - val == false) { - context - .read() - .add(ClearAllReadStories()); - } + if (preference is MarkReadStoriesModePreference && + val == false) { + context + .read() + .add(ClearAllReadStories()); } - : null, - activeThumbColor: Theme.of(context).colorScheme.primary, - ), - if (preference - is MarkReadStoriesModePreference) ...[ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: Dimens.pt16, - ), - child: DropdownMenu( - enabled: preferenceState.isMarkReadStoriesEnabled, - label: Text(StoryMarkingModePreference().title), - initialSelection: preferenceState.storyMarkingMode, - onSelected: (StoryMarkingMode? storyMarkingMode) { - if (storyMarkingMode != null) { - HapticFeedbackUtils.selection(); - context.read().update( - StoryMarkingModePreference( - val: storyMarkingMode.index, - ), - ); } - }, - dropdownMenuEntries: StoryMarkingMode.values - .map( - (StoryMarkingMode val) => - DropdownMenuEntry( - value: val, - label: val.label, - ), - ) - .toList(), - inputDecorationTheme: const InputDecorationTheme( - disabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Palette.grey, + : null, + activeThumbColor: Theme.of(context).colorScheme.primary, + ), + if (preference is MarkReadStoriesModePreference) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: Dimens.pt16, + ), + child: DropdownMenu( + enabled: preferenceState.isMarkReadStoriesEnabled, + label: Text(StoryMarkingModePreference().title), + initialSelection: preferenceState.storyMarkingMode, + onSelected: (StoryMarkingMode? storyMarkingMode) { + if (storyMarkingMode != null) { + HapticFeedbackUtils.selection(); + context.read().update( + StoryMarkingModePreference( + val: storyMarkingMode.index, + ), + ); + } + }, + dropdownMenuEntries: StoryMarkingMode.values + .map( + (StoryMarkingMode val) => + DropdownMenuEntry( + value: val, + label: val.label, ), + ) + .toList(), + inputDecorationTheme: const InputDecorationTheme( + disabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Palette.grey, ), ), - expandedInsets: EdgeInsets.zero, ), + expandedInsets: EdgeInsets.zero, ), - SizedBoxes.pt12, - const Divider(), - ], - if (preference is DividerPreference) const Divider(), - ], - ListTile( - title: const Text( - 'Accent Color', ), - onTap: showColorPicker, + SizedBoxes.pt12, + const Divider(), + ], + if (preference is DividerPreference) const Divider(), + ], + ListTile( + title: const Text( + 'Accent Color', ), - ListTile( - title: const Text( - 'Font', - ), - onTap: showFontSettingDialog, + onTap: showColorPicker, + ), + ListTile( + title: const Text( + 'Font', ), - ListTile( - title: const Text( - 'Theme', - ), - onTap: showThemeSettingDialog, + onTap: showFontSettingDialog, + ), + ListTile( + title: const Text( + 'Theme', ), - const Divider(), - ListTile( - title: const Text( - 'Filter Keywords', - ), - onTap: onFilterKeywordsTapped, + onTap: showThemeSettingDialog, + ), + const Divider(), + ListTile( + title: const Text( + 'Filter Keywords', ), - ListTile( - title: const Text( - 'Export Favorites', - ), - onTap: onExportFavoritesTapped, + onTap: onFilterKeywordsTapped, + ), + ListTile( + title: const Text( + 'Export Favorites', ), - ListTile( - title: const Text( - 'Import Favorites', - ), - onTap: () => - onImportFavoritesTapped(context.read()), + onTap: onExportFavoritesTapped, + ), + ListTile( + title: const Text( + 'Import Favorites', ), - ListTile( - title: const Text( - 'Clear Favorites', - ), - onTap: showClearFavoritesDialog, + onTap: () => + onImportFavoritesTapped(context.read()), + ), + ListTile( + title: const Text( + 'Clear Favorites', + ), + onTap: showClearFavoritesDialog, + ), + ListTile( + title: const Text( + 'Clear Cache', ), + onTap: showClearCacheDialog, + ), + ListTile( + title: const Text('Restore Default Settings'), + onTap: showRestoreDefaultSettingsDialog, + ), + if (preferenceState.isDevModeEnabled) ...[ ListTile( title: const Text( - 'Clear Cache', + 'Logs', ), - onTap: showClearCacheDialog, + onTap: () { + context.go(Paths.logs.landing); + }, ), ListTile( - title: const Text('Restore Default Settings'), - onTap: showRestoreDefaultSettingsDialog, - ), - if (preferenceState.isDevModeEnabled) ...[ - ListTile( - title: const Text( - 'Logs', - ), - onTap: () { - context.go(Paths.logs.landing); - }, - ), - ListTile( - title: const Text( - 'Reset Feature Discovery', - ), - onTap: () { - HapticFeedbackUtils.light(); - FeatureDiscovery.clearPreferences( - context, - DiscoverableFeature.values - .map((DiscoverableFeature f) => f.featureId), - ); - }, - ), - ListTile( - title: const Text( - 'Reset Tips', - ), - onTap: () { - HapticFeedbackUtils.light(); - context.read().reset(); - }, - ), - ], - const Divider(), - ListTile( - title: const Text('Feature Request'), - onTap: () => LinkUtils.launch( - Constants.githubLink, - context, + title: const Text( + 'Reset Feature Discovery', ), - ), - ListTile( - title: const Text('Rate Hacki : )'), onTap: () { - LinkUtils.launch( - Platform.isIOS - ? Constants.appStoreLink - : Constants.googlePlayLink, + HapticFeedbackUtils.light(); + FeatureDiscovery.clearPreferences( context, + DiscoverableFeature.values + .map((DiscoverableFeature f) => f.featureId), ); }, ), ListTile( - title: const Text('About'), - subtitle: Text( - Constants.magicWord, + title: const Text( + 'Reset Tips', ), - onTap: showAboutHackiDialog, - onLongPress: () { - final DevMode updatedDevMode = - DevMode(val: !preferenceState.isDevModeEnabled); - context.read().update(updatedDevMode); - HapticFeedbackUtils.heavy(); - if (updatedDevMode.val) { - showSnackBar(content: 'You are a dev now.'); - } else { - showSnackBar(content: 'Dev mode disabled.'); - } + onTap: () { + HapticFeedbackUtils.light(); + context.read().reset(); }, ), - const SizedBox( - height: Dimens.pt200, - ), ], - ), + const Divider(), + ListTile( + title: const Text('Feature Request'), + onTap: () => LinkUtils.launch( + Constants.githubLink, + context, + ), + ), + ListTile( + title: const Text('Rate Hacki : )'), + onTap: () { + LinkUtils.launch( + Platform.isIOS + ? Constants.appStoreLink + : Constants.googlePlayLink, + context, + ); + }, + ), + ListTile( + title: const Text('About'), + subtitle: Text( + Constants.magicWord, + ), + onTap: showAboutHackiDialog, + onLongPress: () { + context.go(HomeScreen.routeName); + final DevMode updatedDevMode = + DevMode(val: !preferenceState.isDevModeEnabled); + context.read().update(updatedDevMode); + HapticFeedbackUtils.heavy(); + if (updatedDevMode.val) { + showSnackBar(content: 'You are a dev now.'); + } else { + showSnackBar(content: 'Dev mode disabled.'); + } + }, + ), + const SizedBox( + height: Dimens.pt200, + ), + ], ), ), ); diff --git a/lib/screens/profile/widgets/enter_offline_mode_list_tile.dart b/lib/screens/settings/widgets/enter_offline_mode_list_tile.dart similarity index 100% rename from lib/screens/profile/widgets/enter_offline_mode_list_tile.dart rename to lib/screens/settings/widgets/enter_offline_mode_list_tile.dart diff --git a/lib/screens/profile/widgets/offline_list_tile.dart b/lib/screens/settings/widgets/offline_list_tile.dart similarity index 100% rename from lib/screens/profile/widgets/offline_list_tile.dart rename to lib/screens/settings/widgets/offline_list_tile.dart diff --git a/lib/screens/profile/widgets/tab_bar_settings.dart b/lib/screens/settings/widgets/tab_bar_settings.dart similarity index 100% rename from lib/screens/profile/widgets/tab_bar_settings.dart rename to lib/screens/settings/widgets/tab_bar_settings.dart diff --git a/lib/screens/profile/widgets/text_scale_factor_settings.dart b/lib/screens/settings/widgets/text_scale_factor_settings.dart similarity index 100% rename from lib/screens/profile/widgets/text_scale_factor_settings.dart rename to lib/screens/settings/widgets/text_scale_factor_settings.dart diff --git a/lib/screens/settings/widgets/widgets.dart b/lib/screens/settings/widgets/widgets.dart new file mode 100644 index 00000000..5e354b2f --- /dev/null +++ b/lib/screens/settings/widgets/widgets.dart @@ -0,0 +1,4 @@ +export 'enter_offline_mode_list_tile.dart'; +export 'offline_list_tile.dart'; +export 'tab_bar_settings.dart'; +export 'text_scale_factor_settings.dart'; diff --git a/lib/screens/widgets/custom_linkify/custom_linkify.dart b/lib/screens/widgets/custom_linkify/custom_linkify.dart index b92d5dec..2985c98d 100644 --- a/lib/screens/widgets/custom_linkify/custom_linkify.dart +++ b/lib/screens/widgets/custom_linkify/custom_linkify.dart @@ -2,7 +2,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hacki/config/custom_router.dart'; +import 'package:hacki/config/router.dart'; import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/extensions/context_extension.dart'; import 'package:hacki/models/models.dart'; diff --git a/lib/screens/widgets/tips/item_screen_tips.dart b/lib/screens/widgets/tips/item_screen_tips.dart index 842e6a5e..559c3b85 100644 --- a/lib/screens/widgets/tips/item_screen_tips.dart +++ b/lib/screens/widgets/tips/item_screen_tips.dart @@ -63,7 +63,6 @@ class _ItemScreenTipsState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBoxes.pt12, Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/screens/widgets/tips/tips.dart b/lib/screens/widgets/tips/tips.dart index f86c91a0..95ae79e9 100644 --- a/lib/screens/widgets/tips/tips.dart +++ b/lib/screens/widgets/tips/tips.dart @@ -1 +1,2 @@ +export 'item_screen_tips.dart'; export 'share_screen_tips.dart'; diff --git a/lib/services/dialog_proxy.dart b/lib/services/dialog_proxy.dart index fdc58a22..4adbfb9c 100644 --- a/lib/services/dialog_proxy.dart +++ b/lib/services/dialog_proxy.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:hacki/blocs/stories/stories_bloc.dart'; -import 'package:hacki/config/custom_router.dart'; import 'package:hacki/config/locator.dart'; +import 'package:hacki/config/router.dart'; import 'package:hacki/models/item/item.dart'; import 'package:hacki/screens/item/widgets/time_machine_dialog.dart'; import 'package:hacki/services/services.dart'; diff --git a/lib/utils/link_utils.dart b/lib/utils/link_utils.dart index bee17976..45b06dd3 100644 --- a/lib/utils/link_utils.dart +++ b/lib/utils/link_utils.dart @@ -3,9 +3,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:hacki/config/custom_router.dart'; import 'package:hacki/config/locator.dart'; import 'package:hacki/config/paths.dart'; +import 'package:hacki/config/router.dart'; import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/extensions/extensions.dart'; import 'package:hacki/models/models.dart'; diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 4618f58b..727037ef 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -11,3 +11,4 @@ export 'linkifier_utils.dart'; export 'log_utils.dart'; export 'theme_utils.dart'; export 'throttle.dart'; +export 'widget_utils.dart'; diff --git a/lib/utils/widget_utils.dart b/lib/utils/widget_utils.dart new file mode 100644 index 00000000..37c869b1 --- /dev/null +++ b/lib/utils/widget_utils.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:hacki/config/locator.dart'; +import 'package:logger/logger.dart'; + +abstract final class WidgetUtils { + static double? preferredCacheExtent; + + static Logger get _logger => locator.get(); + + static double calculateCacheExtent(BuildContext context) { + if (preferredCacheExtent != null) return preferredCacheExtent!; + final MediaQueryData mediaQuery = MediaQuery.of(context); + final double screenHeight = mediaQuery.size.height; + final double devicePixelRatio = mediaQuery.devicePixelRatio; + final double physicalHeight = screenHeight * devicePixelRatio; + + double cacheExtent = screenHeight * 2; + + if (Platform.isAndroid) { + cacheExtent *= _ramMultiplier(); + } + + if (devicePixelRatio >= 3.0) { + cacheExtent *= 0.75; + } else if (devicePixelRatio >= 2.0) { + cacheExtent *= 0.9; + } + + late final double result; + if (physicalHeight > 2800) { + result = cacheExtent.clamp(0, screenHeight * 1.5); + preferredCacheExtent = result; + } else { + result = cacheExtent.clamp(400, 2000); + preferredCacheExtent = result; + } + + _logger.i('[WidgetUtils]: preferred cache extent: $result'); + return result; + } + + static double _ramMultiplier() { + try { + final File memFile = File('/proc/meminfo'); + if (!memFile.existsSync()) return 1; + + final List lines = memFile.readAsLinesSync(); + final String totalLine = lines.firstWhere( + (String l) => l.startsWith('MemTotal'), + orElse: () => '', + ); + if (totalLine.isEmpty) return 1; + + final int kb = int.tryParse( + totalLine.replaceAll(RegExp('[^0-9]'), ''), + ) ?? + 0; + final double gb = kb / (1024 * 1024); + + if (gb >= 8) return 1.3; + if (gb >= 6) return 1.15; + if (gb >= 4) return 1; + return 0.75; + } catch (_) { + return 1; + } + } +} From 19f4d7895510aa5222f0aa3263e1e1918e0cb9f8 Mon Sep 17 00:00:00 2001 From: livinglist Date: Wed, 1 Apr 2026 03:13:18 -0700 Subject: [PATCH 2/8] update --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 73c87f9e..bfe85f8b 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ app.*.map.json /android/app/profile /android/app/release /android/build/reports +/android/gradle/local.properties From f1f167103cb643bc080cdb91bfb457f27f3ea818 Mon Sep 17 00:00:00 2001 From: livinglist Date: Wed, 1 Apr 2026 03:16:06 -0700 Subject: [PATCH 3/8] update --- lib/screens/item/widgets/main_view.dart | 2 +- lib/utils/widget_utils.dart | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/screens/item/widgets/main_view.dart b/lib/screens/item/widgets/main_view.dart index 38d7be66..a3af50aa 100644 --- a/lib/screens/item/widgets/main_view.dart +++ b/lib/screens/item/widgets/main_view.dart @@ -89,7 +89,7 @@ class MainView extends StatelessWidget { context.read().itemPositionsListener, itemCount: state.comments.length + 2, scrollOffsetListener: scrollOffsetListener, - minCacheExtent: WidgetUtils.preferredCacheExtent, + minCacheExtent: WidgetUtils.calculateCacheExtent(context), itemBuilder: (BuildContext context, int index) { if (index == 0) { return Material( diff --git a/lib/utils/widget_utils.dart b/lib/utils/widget_utils.dart index 37c869b1..df97234a 100644 --- a/lib/utils/widget_utils.dart +++ b/lib/utils/widget_utils.dart @@ -5,12 +5,14 @@ import 'package:hacki/config/locator.dart'; import 'package:logger/logger.dart'; abstract final class WidgetUtils { - static double? preferredCacheExtent; + static double? _cachedPreferredCacheExtent; static Logger get _logger => locator.get(); static double calculateCacheExtent(BuildContext context) { - if (preferredCacheExtent != null) return preferredCacheExtent!; + if (_cachedPreferredCacheExtent != null) { + return _cachedPreferredCacheExtent!; + } final MediaQueryData mediaQuery = MediaQuery.of(context); final double screenHeight = mediaQuery.size.height; final double devicePixelRatio = mediaQuery.devicePixelRatio; @@ -31,10 +33,10 @@ abstract final class WidgetUtils { late final double result; if (physicalHeight > 2800) { result = cacheExtent.clamp(0, screenHeight * 1.5); - preferredCacheExtent = result; + _cachedPreferredCacheExtent = result; } else { result = cacheExtent.clamp(400, 2000); - preferredCacheExtent = result; + _cachedPreferredCacheExtent = result; } _logger.i('[WidgetUtils]: preferred cache extent: $result'); From 969d989ee45dcc0f2959e2644e70fd4014e785db Mon Sep 17 00:00:00 2001 From: livinglist Date: Wed, 1 Apr 2026 03:31:03 -0700 Subject: [PATCH 4/8] update --- android/app/build.gradle | 2 +- android/app/proguard-rules.pro | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 android/app/proguard-rules.pro diff --git a/android/app/build.gradle b/android/app/build.gradle index 73e4f417..c2898032 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -70,9 +70,9 @@ android { buildTypes { release { signingConfig signingConfigs.release - minifyEnabled true shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..13629e86 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,13 @@ +-dontwarn androidx.window.extensions.WindowExtensions +-dontwarn androidx.window.extensions.WindowExtensionsProvider +-dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation +-dontwarn androidx.window.extensions.layout.DisplayFeature +-dontwarn androidx.window.extensions.layout.FoldingFeature +-dontwarn androidx.window.extensions.layout.WindowLayoutComponent +-dontwarn androidx.window.extensions.layout.WindowLayoutInfo +-dontwarn androidx.window.sidecar.SidecarDeviceState +-dontwarn androidx.window.sidecar.SidecarDisplayFeature +-dontwarn androidx.window.sidecar.SidecarInterface$SidecarCallback +-dontwarn androidx.window.sidecar.SidecarInterface +-dontwarn androidx.window.sidecar.SidecarProvider +-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo \ No newline at end of file From 36d9f12576da70a35f52177a4bb00b5249a3a46f Mon Sep 17 00:00:00 2001 From: livinglist Date: Wed, 1 Apr 2026 03:32:49 -0700 Subject: [PATCH 5/8] update --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index bfe85f8b..a8464de5 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ app.*.map.json /android/app/release /android/build/reports /android/gradle/local.properties +/android/.kotlin/ +/android/app/.cxx/ +/android/app/build/ From dbec82d8b704a7e1a2606318884e1a938cbb6b6e Mon Sep 17 00:00:00 2001 From: livinglist Date: Wed, 1 Apr 2026 04:09:48 -0700 Subject: [PATCH 6/8] update --- lib/cubits/comments/comments_cubit.dart | 2 +- lib/screens/profile/profile_screen.dart | 10 +- lib/screens/settings/settings_screen.dart | 868 +++++++++++----------- lib/screens/widgets/items_list_view.dart | 7 +- 4 files changed, 444 insertions(+), 443 deletions(-) diff --git a/lib/cubits/comments/comments_cubit.dart b/lib/cubits/comments/comments_cubit.dart index b7a791a2..6a7e4451 100644 --- a/lib/cubits/comments/comments_cubit.dart +++ b/lib/cubits/comments/comments_cubit.dart @@ -879,7 +879,7 @@ comments length is ${state.comments.length} } } - await Future.delayed(AppDurations.ms400, () { + await Future.delayed(AppDurations.ms300, () { final BuildContext? newTargetCommentContext = targetCommentGlobalKey?.currentContext; if (targetCommentGlobalKey != null && diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index 0caefef1..60509e33 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -192,7 +192,15 @@ class _ProfileScreenState extends State ), Visibility( visible: pageType == PageType.settings, - child: const SettingsView(), + child: Positioned.fill( + top: context + .read() + .state + .isHackerNewsThemeEnabled + ? Dimens.pt64 + : Dimens.pt50, + child: const SettingsView(), + ), ), Align( alignment: Alignment.topLeft, diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 4940177f..d0f836d5 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -49,6 +49,7 @@ class _SettingsScreenState extends State with ItemActionMixin { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + backgroundColor: Theme.of(context).canvasColor, title: const Text('Settings'), ), body: const SettingsView(), @@ -73,491 +74,482 @@ class _SettingsViewState extends State builder: (BuildContext context, PreferenceState preferenceState) { final AuthState authState = context.read().state; final bool isLoggedIn = authState.isLoggedIn; - return Positioned.fill( - top: preferenceState.isHackerNewsThemeEnabled - ? Dimens.pt64 - : Dimens.pt50, - child: SingleChildScrollView( - child: Column( - children: [ - ListTile( - leading: Icon( - Icons.person, - color: isLoggedIn - ? Theme.of(context).colorScheme.primary - : null, - ), - title: Text( - isLoggedIn ? 'Log Out' : 'Log In', - ), - subtitle: isLoggedIn - ? Text( - context.read().state.username, - ) - : null, - onTap: () { - if (isLoggedIn) { - onLogoutTapped(); - } else { - onLoginTapped(); - } - }, + return SingleChildScrollView( + child: Column( + children: [ + ListTile( + leading: Icon( + Icons.person, + color: + isLoggedIn ? Theme.of(context).colorScheme.primary : null, ), - const EnterOfflineModeListTile(), - const OfflineListTile(), - const SizedBox( - height: Dimens.pt8, + title: Text( + isLoggedIn ? 'Log Out' : 'Log In', ), - OverflowBar( - alignment: MainAxisAlignment.spaceBetween, - overflowSpacing: Dimens.pt12, - children: [ - Padding( - padding: const EdgeInsets.only( - left: Dimens.pt16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Default fetch mode'), - DropdownMenu( - initialSelection: preferenceState.fetchMode, - dropdownMenuEntries: FetchMode.values - .map( - (FetchMode val) => - DropdownMenuEntry( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (FetchMode? fetchMode) { - if (fetchMode != null) { - HapticFeedbackUtils.selection(); - context.read().update( - FetchModePreference( - val: fetchMode.index, - ), - ); - } - }, - ), - ], - ), + subtitle: isLoggedIn + ? Text( + context.read().state.username, + ) + : null, + onTap: () { + if (isLoggedIn) { + onLogoutTapped(); + } else { + onLoginTapped(); + } + }, + ), + const EnterOfflineModeListTile(), + const OfflineListTile(), + const SizedBox( + height: Dimens.pt8, + ), + OverflowBar( + alignment: MainAxisAlignment.spaceBetween, + overflowSpacing: Dimens.pt12, + children: [ + Padding( + padding: const EdgeInsets.only( + left: Dimens.pt16, ), - Padding( - padding: const EdgeInsets.only( - left: Dimens.pt16, - right: Dimens.pt16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Default comments order'), - DropdownMenu( - initialSelection: preferenceState.order, - dropdownMenuEntries: CommentsOrder.values - .map( - (CommentsOrder val) => - DropdownMenuEntry( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (CommentsOrder? order) { - if (order != null) { - HapticFeedbackUtils.selection(); - context.read().update( - CommentsOrderPreference( - val: order.index, - ), - ); - } - }, - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Default fetch mode'), + DropdownMenu( + initialSelection: preferenceState.fetchMode, + dropdownMenuEntries: FetchMode.values + .map( + (FetchMode val) => DropdownMenuEntry( + value: val, + label: val.description, + ), + ) + .toList(), + onSelected: (FetchMode? fetchMode) { + if (fetchMode != null) { + HapticFeedbackUtils.selection(); + context.read().update( + FetchModePreference( + val: fetchMode.index, + ), + ); + } + }, + ), + ], ), - ], - ), - const SizedBox( - height: Dimens.pt12, - ), - OverflowBar( - alignment: MainAxisAlignment.spaceBetween, - overflowSpacing: Dimens.pt12, - children: [ - Padding( - padding: const EdgeInsets.only( - left: Dimens.pt16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Date time display of comments', - ), - DropdownMenu( - initialSelection: preferenceState.displayDateFormat, - dropdownMenuEntries: DateDisplayFormat.values - .map( - (DateDisplayFormat val) => - DropdownMenuEntry( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (DateDisplayFormat? order) { - if (order != null) { - HapticFeedbackUtils.selection(); - context.read().update( - DateFormatPreference( - val: order.index, - ), - ); - DateDisplayFormat.clearCache(); - } - }, - ), - ], - ), + ), + Padding( + padding: const EdgeInsets.only( + left: Dimens.pt16, + right: Dimens.pt16, ), - Padding( - padding: const EdgeInsets.only( - left: Dimens.pt16, - right: Dimens.pt16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Data source', - ), - BlocSelector( - selector: (StoriesState state) => - state.statusByType.values.any( - (Status status) => status == Status.inProgress, - ), - builder: ( - BuildContext context, - bool isInProgress, - ) { - return DropdownMenu( - initialSelection: preferenceState.dataSource, - dropdownMenuEntries: HackerNewsDataSource.values - .map( - (HackerNewsDataSource val) => - DropdownMenuEntry< - HackerNewsDataSource>( - value: val, - label: val.description, - ), - ) - .toList(), - onSelected: (HackerNewsDataSource? source) { - if (source != null) { - HapticFeedbackUtils.selection(); - context.read().update( - HackerNewsDataSourcePreference( - val: source.index, - ), - ); - } - }, - ); - }, - ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Default comments order'), + DropdownMenu( + initialSelection: preferenceState.order, + dropdownMenuEntries: CommentsOrder.values + .map( + (CommentsOrder val) => + DropdownMenuEntry( + value: val, + label: val.description, + ), + ) + .toList(), + onSelected: (CommentsOrder? order) { + if (order != null) { + HapticFeedbackUtils.selection(); + context.read().update( + CommentsOrderPreference( + val: order.index, + ), + ); + } + }, + ), + ], ), - ], - ), - const SizedBox( - height: Dimens.pt12, - ), - const TabBarSettings(), - const TextScaleFactorSettings(), - const Divider(), - StoryTile( - shouldShowWebPreview: preferenceState.isRichStoryTileEnabled, - shouldShowMetadata: preferenceState.isMetadataEnabled, - shouldShowUrl: preferenceState.isUrlEnabled, - shouldShowFavicon: preferenceState.isFaviconEnabled, - shouldShowPreviewImage: - preferenceState.isStoryTilePreviewImageEnabled, - isExpandedTileEnabled: preferenceState.isExpandedTileEnabled, - isIndexedStoryTileEnabled: - preferenceState.isIndexedStoryTileEnabled, - isImageLeftAligned: preferenceState.isPreviewImageLeftAligned, - index: 0, - story: Story.placeholder(), - onTap: () => LinkUtils.launch( - Constants.guidelineLink, - context, ), - ), - const Divider(), - for (final Preference preference - in preferenceState.settingsPreferences) ...[ - if (preference is DividerPlaceholder) - SizedBox( - height: Dimens.pt36, - child: Flex( - mainAxisAlignment: MainAxisAlignment.center, - direction: Axis.horizontal, - children: [ - SizedBoxes.pt12, - const Flexible( - child: Divider(), - ), - SizedBoxes.pt12, - Text(preference.label), - SizedBoxes.pt12, - const Flexible( - child: Divider(), - ), - SizedBoxes.pt12, - ], - ), - ) - else if (preference is PreviewImageAlignmentPreference) - FadeIn( - child: ListTile( - enabled: preference.dependencies - .satisfy(preferenceState.preferences), - title: Text(preference.title), - trailing: SegmentedButton( - showSelectedIcon: false, - segments: const >[ - ButtonSegment( - value: true, - label: Text('Left'), - ), - ButtonSegment( - value: false, - label: Text('Right'), - ), - ], - selected: { - preferenceState.isOn( - preference as BooleanPreference, - ), + ], + ), + const SizedBox( + height: Dimens.pt12, + ), + OverflowBar( + alignment: MainAxisAlignment.spaceBetween, + overflowSpacing: Dimens.pt12, + children: [ + Padding( + padding: const EdgeInsets.only( + left: Dimens.pt16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Date time display of comments', + ), + DropdownMenu( + initialSelection: preferenceState.displayDateFormat, + dropdownMenuEntries: DateDisplayFormat.values + .map( + (DateDisplayFormat val) => + DropdownMenuEntry( + value: val, + label: val.description, + ), + ) + .toList(), + onSelected: (DateDisplayFormat? order) { + if (order != null) { + HapticFeedbackUtils.selection(); + context.read().update( + DateFormatPreference( + val: order.index, + ), + ); + DateDisplayFormat.clearCache(); + } }, - onSelectionChanged: preference.dependencies - .satisfy(preferenceState.preferences) - ? (Set val) { - HapticFeedbackUtils.light(); + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: Dimens.pt16, + right: Dimens.pt16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Data source', + ), + BlocSelector( + selector: (StoriesState state) => + state.statusByType.values.any( + (Status status) => status == Status.inProgress, + ), + builder: ( + BuildContext context, + bool isInProgress, + ) { + return DropdownMenu( + initialSelection: preferenceState.dataSource, + dropdownMenuEntries: HackerNewsDataSource.values + .map( + (HackerNewsDataSource val) => + DropdownMenuEntry( + value: val, + label: val.description, + ), + ) + .toList(), + onSelected: (HackerNewsDataSource? source) { + if (source != null) { + HapticFeedbackUtils.selection(); context.read().update( - preference.copyWith(val: val.single), + HackerNewsDataSourcePreference( + val: source.index, + ), ); } - : null, + }, + ); + }, ), - ), - ) - else - SwitchListTile( - key: ValueKey(preference.key), + ], + ), + ), + ], + ), + const SizedBox( + height: Dimens.pt12, + ), + const TabBarSettings(), + const TextScaleFactorSettings(), + const Divider(), + StoryTile( + shouldShowWebPreview: preferenceState.isRichStoryTileEnabled, + shouldShowMetadata: preferenceState.isMetadataEnabled, + shouldShowUrl: preferenceState.isUrlEnabled, + shouldShowFavicon: preferenceState.isFaviconEnabled, + shouldShowPreviewImage: + preferenceState.isStoryTilePreviewImageEnabled, + isExpandedTileEnabled: preferenceState.isExpandedTileEnabled, + isIndexedStoryTileEnabled: + preferenceState.isIndexedStoryTileEnabled, + isImageLeftAligned: preferenceState.isPreviewImageLeftAligned, + index: 0, + story: Story.placeholder(), + onTap: () => LinkUtils.launch( + Constants.guidelineLink, + context, + ), + ), + const Divider(), + for (final Preference preference + in preferenceState.settingsPreferences) ...[ + if (preference is DividerPlaceholder) + SizedBox( + height: Dimens.pt36, + child: Flex( + mainAxisAlignment: MainAxisAlignment.center, + direction: Axis.horizontal, + children: [ + SizedBoxes.pt12, + const Flexible( + child: Divider(), + ), + SizedBoxes.pt12, + Text(preference.label), + SizedBoxes.pt12, + const Flexible( + child: Divider(), + ), + SizedBoxes.pt12, + ], + ), + ) + else if (preference is PreviewImageAlignmentPreference) + FadeIn( + child: ListTile( + enabled: preference.dependencies + .satisfy(preferenceState.preferences), title: Text(preference.title), - subtitle: preference.subtitle.isNotEmpty - ? Text(preference.subtitle) - : null, - value: preferenceState.isOn( - preference as BooleanPreference, + trailing: SegmentedButton( + showSelectedIcon: false, + segments: const >[ + ButtonSegment( + value: true, + label: Text('Left'), + ), + ButtonSegment( + value: false, + label: Text('Right'), + ), + ], + selected: { + preferenceState.isOn( + preference as BooleanPreference, + ), + }, + onSelectionChanged: preference.dependencies + .satisfy(preferenceState.preferences) + ? (Set val) { + HapticFeedbackUtils.light(); + context.read().update( + preference.copyWith(val: val.single), + ); + } + : null, ), - onChanged: preference.dependencies - .satisfy(preferenceState.preferences) - ? (bool val) { - HapticFeedbackUtils.light(); + ), + ) + else + SwitchListTile( + key: ValueKey(preference.key), + title: Text(preference.title), + subtitle: preference.subtitle.isNotEmpty + ? Text(preference.subtitle) + : null, + value: preferenceState.isOn( + preference as BooleanPreference, + ), + onChanged: preference.dependencies + .satisfy(preferenceState.preferences) + ? (bool val) { + HapticFeedbackUtils.light(); - context - .read() - .update(preference.copyWith(val: val)); + context + .read() + .update(preference.copyWith(val: val)); - if (preference is MarkReadStoriesModePreference && - val == false) { - context - .read() - .add(ClearAllReadStories()); - } + if (preference is MarkReadStoriesModePreference && + val == false) { + context + .read() + .add(ClearAllReadStories()); } - : null, - activeThumbColor: Theme.of(context).colorScheme.primary, - ), - if (preference is MarkReadStoriesModePreference) ...[ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: Dimens.pt16, - ), - child: DropdownMenu( - enabled: preferenceState.isMarkReadStoriesEnabled, - label: Text(StoryMarkingModePreference().title), - initialSelection: preferenceState.storyMarkingMode, - onSelected: (StoryMarkingMode? storyMarkingMode) { - if (storyMarkingMode != null) { - HapticFeedbackUtils.selection(); - context.read().update( - StoryMarkingModePreference( - val: storyMarkingMode.index, - ), - ); } - }, - dropdownMenuEntries: StoryMarkingMode.values - .map( - (StoryMarkingMode val) => - DropdownMenuEntry( - value: val, - label: val.label, - ), - ) - .toList(), - inputDecorationTheme: const InputDecorationTheme( - disabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Palette.grey, + : null, + activeThumbColor: Theme.of(context).colorScheme.primary, + ), + if (preference is MarkReadStoriesModePreference) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: Dimens.pt16, + ), + child: DropdownMenu( + enabled: preferenceState.isMarkReadStoriesEnabled, + label: Text(StoryMarkingModePreference().title), + initialSelection: preferenceState.storyMarkingMode, + onSelected: (StoryMarkingMode? storyMarkingMode) { + if (storyMarkingMode != null) { + HapticFeedbackUtils.selection(); + context.read().update( + StoryMarkingModePreference( + val: storyMarkingMode.index, + ), + ); + } + }, + dropdownMenuEntries: StoryMarkingMode.values + .map( + (StoryMarkingMode val) => + DropdownMenuEntry( + value: val, + label: val.label, ), + ) + .toList(), + inputDecorationTheme: const InputDecorationTheme( + disabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Palette.grey, ), ), - expandedInsets: EdgeInsets.zero, ), + expandedInsets: EdgeInsets.zero, ), - SizedBoxes.pt12, - const Divider(), - ], - if (preference is DividerPreference) const Divider(), - ], - ListTile( - title: const Text( - 'Accent Color', ), - onTap: showColorPicker, + SizedBoxes.pt12, + const Divider(), + ], + if (preference is DividerPreference) const Divider(), + ], + ListTile( + title: const Text( + 'Accent Color', ), - ListTile( - title: const Text( - 'Font', - ), - onTap: showFontSettingDialog, + onTap: showColorPicker, + ), + ListTile( + title: const Text( + 'Font', ), - ListTile( - title: const Text( - 'Theme', - ), - onTap: showThemeSettingDialog, + onTap: showFontSettingDialog, + ), + ListTile( + title: const Text( + 'Theme', ), - const Divider(), - ListTile( - title: const Text( - 'Filter Keywords', - ), - onTap: onFilterKeywordsTapped, + onTap: showThemeSettingDialog, + ), + const Divider(), + ListTile( + title: const Text( + 'Filter Keywords', ), - ListTile( - title: const Text( - 'Export Favorites', - ), - onTap: onExportFavoritesTapped, + onTap: onFilterKeywordsTapped, + ), + ListTile( + title: const Text( + 'Export Favorites', ), - ListTile( - title: const Text( - 'Import Favorites', - ), - onTap: () => - onImportFavoritesTapped(context.read()), + onTap: onExportFavoritesTapped, + ), + ListTile( + title: const Text( + 'Import Favorites', ), - ListTile( - title: const Text( - 'Clear Favorites', - ), - onTap: showClearFavoritesDialog, + onTap: () => onImportFavoritesTapped(context.read()), + ), + ListTile( + title: const Text( + 'Clear Favorites', + ), + onTap: showClearFavoritesDialog, + ), + ListTile( + title: const Text( + 'Clear Cache', ), + onTap: showClearCacheDialog, + ), + ListTile( + title: const Text('Restore Default Settings'), + onTap: showRestoreDefaultSettingsDialog, + ), + if (preferenceState.isDevModeEnabled) ...[ ListTile( title: const Text( - 'Clear Cache', + 'Logs', ), - onTap: showClearCacheDialog, - ), - ListTile( - title: const Text('Restore Default Settings'), - onTap: showRestoreDefaultSettingsDialog, + onTap: () { + context.go(Paths.logs.landing); + }, ), - if (preferenceState.isDevModeEnabled) ...[ - ListTile( - title: const Text( - 'Logs', - ), - onTap: () { - context.go(Paths.logs.landing); - }, - ), - ListTile( - title: const Text( - 'Reset Feature Discovery', - ), - onTap: () { - HapticFeedbackUtils.light(); - FeatureDiscovery.clearPreferences( - context, - DiscoverableFeature.values - .map((DiscoverableFeature f) => f.featureId), - ); - }, - ), - ListTile( - title: const Text( - 'Reset Tips', - ), - onTap: () { - HapticFeedbackUtils.light(); - context.read().reset(); - }, - ), - ], - const Divider(), ListTile( - title: const Text('Feature Request'), - onTap: () => LinkUtils.launch( - Constants.githubLink, - context, + title: const Text( + 'Reset Feature Discovery', ), - ), - ListTile( - title: const Text('Rate Hacki : )'), onTap: () { - LinkUtils.launch( - Platform.isIOS - ? Constants.appStoreLink - : Constants.googlePlayLink, + HapticFeedbackUtils.light(); + FeatureDiscovery.clearPreferences( context, + DiscoverableFeature.values + .map((DiscoverableFeature f) => f.featureId), ); }, ), ListTile( - title: const Text('About'), - subtitle: Text( - Constants.magicWord, + title: const Text( + 'Reset Tips', ), - onTap: showAboutHackiDialog, - onLongPress: () { - context.go(HomeScreen.routeName); - final DevMode updatedDevMode = - DevMode(val: !preferenceState.isDevModeEnabled); - context.read().update(updatedDevMode); - HapticFeedbackUtils.heavy(); - if (updatedDevMode.val) { - showSnackBar(content: 'You are a dev now.'); - } else { - showSnackBar(content: 'Dev mode disabled.'); - } + onTap: () { + HapticFeedbackUtils.light(); + context.read().reset(); }, ), - const SizedBox( - height: Dimens.pt200, - ), ], - ), + const Divider(), + ListTile( + title: const Text('Feature Request'), + onTap: () => LinkUtils.launch( + Constants.githubLink, + context, + ), + ), + ListTile( + title: const Text('Rate Hacki : )'), + onTap: () { + LinkUtils.launch( + Platform.isIOS + ? Constants.appStoreLink + : Constants.googlePlayLink, + context, + ); + }, + ), + ListTile( + title: const Text('About'), + subtitle: Text( + Constants.magicWord, + ), + onTap: showAboutHackiDialog, + onLongPress: () { + context.go(HomeScreen.routeName); + final DevMode updatedDevMode = + DevMode(val: !preferenceState.isDevModeEnabled); + context.read().update(updatedDevMode); + HapticFeedbackUtils.heavy(); + if (updatedDevMode.val) { + showSnackBar(content: 'You are a dev now.'); + } else { + showSnackBar(content: 'Dev mode disabled.'); + } + }, + ), + const SizedBox( + height: Dimens.pt200, + ), + ], ), ); }, diff --git a/lib/screens/widgets/items_list_view.dart b/lib/screens/widgets/items_list_view.dart index 91abc300..4a5395fc 100644 --- a/lib/screens/widgets/items_list_view.dart +++ b/lib/screens/widgets/items_list_view.dart @@ -39,7 +39,7 @@ class ItemsListView extends StatelessWidget { this.header, this.footer, this.onMoreTapped, - this.scrollController, + //this.scrollController, this.itemBuilder, }); @@ -64,7 +64,7 @@ class ItemsListView extends StatelessWidget { final Widget? header; final Widget? footer; final RefreshController refreshController; - final ScrollController? scrollController; + //final ScrollController? scrollController; final VoidCallback? onRefresh; final VoidCallback? onLoadMore; final ValueChanged? onPinned; @@ -77,7 +77,8 @@ class ItemsListView extends StatelessWidget { @override Widget build(BuildContext context) { final ListView child = ListView( - controller: scrollController, + cacheExtent: WidgetUtils.calculateCacheExtent(context), + primary: true, children: [ if (shouldShowOfflineBanner) const OfflineBanner( From 528cc38965968a3d22e0f22a526b00507260f906 Mon Sep 17 00:00:00 2001 From: livinglist Date: Wed, 1 Apr 2026 04:14:45 -0700 Subject: [PATCH 7/8] update --- lib/screens/widgets/items_list_view.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/screens/widgets/items_list_view.dart b/lib/screens/widgets/items_list_view.dart index 4a5395fc..57a9b352 100644 --- a/lib/screens/widgets/items_list_view.dart +++ b/lib/screens/widgets/items_list_view.dart @@ -64,6 +64,7 @@ class ItemsListView extends StatelessWidget { final Widget? header; final Widget? footer; final RefreshController refreshController; + //final ScrollController? scrollController; final VoidCallback? onRefresh; final VoidCallback? onLoadMore; @@ -77,8 +78,6 @@ class ItemsListView extends StatelessWidget { @override Widget build(BuildContext context) { final ListView child = ListView( - cacheExtent: WidgetUtils.calculateCacheExtent(context), - primary: true, children: [ if (shouldShowOfflineBanner) const OfflineBanner( From 04532585392d59b9511fad175983e808eda6b45b Mon Sep 17 00:00:00 2001 From: livinglist Date: Wed, 1 Apr 2026 04:29:51 -0700 Subject: [PATCH 8/8] update --- lib/extensions/widget_extension.dart | 34 +----------- lib/screens/item/widgets/settings_button.dart | 25 ++++++++- lib/screens/settings/settings_screen.dart | 2 +- lib/screens/widgets/items_list_view.dart | 2 - lib/services/dialog_proxy.dart | 52 +++++++++++++++++++ 5 files changed, 79 insertions(+), 36 deletions(-) diff --git a/lib/extensions/widget_extension.dart b/lib/extensions/widget_extension.dart index f4b4dae6..f88bc177 100644 --- a/lib/extensions/widget_extension.dart +++ b/lib/extensions/widget_extension.dart @@ -1,10 +1,8 @@ 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/search/search_screen.dart'; import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart'; +import 'package:hacki/services/dialog_proxy.dart'; import 'package:hacki/styles/styles.dart'; import 'package:hacki/utils/utils.dart'; @@ -37,7 +35,7 @@ extension ContextMenuBuilder on Widget { ..insert( 0, ContextMenuButtonItem( - onPressed: () => _showHackerNewsSearchBottomSheet( + onPressed: () => DialogProxy.showHackerNewsSearchBottomSheet( context, selectedText, ), @@ -67,34 +65,6 @@ extension ContextMenuBuilder on Widget { buttonItems: items, ); } - - void _showHackerNewsSearchBottomSheet( - BuildContext context, - String text, - ) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (BuildContext context) { - return BlocProvider( - create: (_) => SearchCubit()..search(text), - child: SizedBox( - height: MediaQuery.of(context).size.height - Dimens.pt120, - child: const Column( - children: [ - Expanded( - child: SearchScreen( - isInBottomSheet: true, - ), - ), - ], - ), - ), - ); - }, - ); - } } extension WidgetModifier on Widget { diff --git a/lib/screens/item/widgets/settings_button.dart b/lib/screens/item/widgets/settings_button.dart index e62bfc3e..afe40983 100644 --- a/lib/screens/item/widgets/settings_button.dart +++ b/lib/screens/item/widgets/settings_button.dart @@ -2,14 +2,19 @@ import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hacki/config/paths.dart'; +import 'package:hacki/config/router.dart'; import 'package:hacki/models/discoverable_feature.dart'; import 'package:hacki/screens/widgets/widgets.dart'; +import 'package:hacki/services/services.dart'; +import 'package:responsive_builder/responsive_builder.dart'; class SettingsButton extends StatelessWidget { const SettingsButton({ super.key, }); + static DeviceScreenType? _cachedDeviceType; + @override Widget build(BuildContext context) { return IconButton( @@ -26,7 +31,25 @@ class SettingsButton extends StatelessWidget { color: Theme.of(context).colorScheme.onSurface, ), ), - onPressed: () => context.push(Paths.item.settings), + onPressed: () { + _cachedDeviceType ??= () { + final BuildContext? context = navigatorKey.currentContext; + if (context != null) { + final Size size = MediaQuery.of(context).size; + final DeviceScreenType type = getDeviceType(size); + return type; + } + return DeviceScreenType.mobile; + }(); + final DeviceScreenType deviceType = _cachedDeviceType!; + + if (deviceType == DeviceScreenType.mobile) { + context.push(Paths.item.settings); + return; + } else { + DialogProxy.showSettingsBottomSheet(context); + } + }, ); } } diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index d0f836d5..adb1a25b 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -72,7 +72,7 @@ class _SettingsViewState extends State Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, PreferenceState preferenceState) { - final AuthState authState = context.read().state; + final AuthState authState = context.watch().state; final bool isLoggedIn = authState.isLoggedIn; return SingleChildScrollView( child: Column( diff --git a/lib/screens/widgets/items_list_view.dart b/lib/screens/widgets/items_list_view.dart index 57a9b352..65f8b85d 100644 --- a/lib/screens/widgets/items_list_view.dart +++ b/lib/screens/widgets/items_list_view.dart @@ -39,7 +39,6 @@ class ItemsListView extends StatelessWidget { this.header, this.footer, this.onMoreTapped, - //this.scrollController, this.itemBuilder, }); @@ -65,7 +64,6 @@ class ItemsListView extends StatelessWidget { final Widget? footer; final RefreshController refreshController; - //final ScrollController? scrollController; final VoidCallback? onRefresh; final VoidCallback? onLoadMore; final ValueChanged? onPinned; diff --git a/lib/services/dialog_proxy.dart b/lib/services/dialog_proxy.dart index 4adbfb9c..e2f20015 100644 --- a/lib/services/dialog_proxy.dart +++ b/lib/services/dialog_proxy.dart @@ -4,8 +4,10 @@ import 'package:go_router/go_router.dart'; import 'package:hacki/blocs/stories/stories_bloc.dart'; import 'package:hacki/config/locator.dart'; import 'package:hacki/config/router.dart'; +import 'package:hacki/cubits/search/search_cubit.dart'; import 'package:hacki/models/item/item.dart'; import 'package:hacki/screens/item/widgets/time_machine_dialog.dart'; +import 'package:hacki/screens/screens.dart'; import 'package:hacki/services/services.dart'; import 'package:hacki/styles/styles.dart'; import 'package:hacki/utils/haptic_feedback_utils.dart'; @@ -13,6 +15,56 @@ import 'package:responsive_builder/responsive_builder.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; abstract final class DialogProxy { + static void showSettingsBottomSheet( + BuildContext context, + ) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (BuildContext context) { + return SizedBox( + height: MediaQuery.of(context).size.height - Dimens.pt120, + child: const Column( + children: [ + Expanded( + child: SettingsView(), + ), + ], + ), + ); + }, + ); + } + + static void showHackerNewsSearchBottomSheet( + BuildContext context, + String text, + ) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (BuildContext context) { + return BlocProvider( + create: (_) => SearchCubit()..search(text), + child: SizedBox( + height: MediaQuery.of(context).size.height - Dimens.pt120, + child: const Column( + children: [ + Expanded( + child: SearchScreen( + isInBottomSheet: true, + ), + ), + ], + ), + ), + ); + }, + ); + } + static void showTimeMachineDialog( BuildContext context, { required Item rootItem,