From a373e2c04b13e6f5db05d02f7957018e4b188cf0 Mon Sep 17 00:00:00 2001 From: Livinglist Date: Wed, 29 Jul 2026 20:25:54 -0700 Subject: [PATCH 1/3] update --- lib/screens/item/item_screen.dart | 63 +++++++----------- .../item/widgets/item_screen_web_view.dart | 64 +++++++++++++++++++ lib/screens/item/widgets/widgets.dart | 1 + .../widgets/web_view_bottom_sheet.dart | 32 ++++++---- 4 files changed, 108 insertions(+), 52 deletions(-) create mode 100644 lib/screens/item/widgets/item_screen_web_view.dart diff --git a/lib/screens/item/item_screen.dart b/lib/screens/item/item_screen.dart index ef3e697b..eca8495e 100644 --- a/lib/screens/item/item_screen.dart +++ b/lib/screens/item/item_screen.dart @@ -177,8 +177,8 @@ class _ItemScreenState extends State AppDurations.oneSecond; static const double _indentPadding = 8; static const double _indentLineWidth = 2; - static const double _webViewOffsetInvisible = 0.1; - bool _isWebViewBottomSheetVisible = true; + final ItemScreenWebViewController _webViewController = + ItemScreenWebViewController(); @override void didPop() { @@ -220,6 +220,7 @@ class _ItemScreenState extends State featureDiscoveryDismissThrottle.dispose(); focusNode.dispose(); scrollOffsetSubscription?.cancel(); + _webViewController.dispose(); super.dispose(); } @@ -229,8 +230,15 @@ class _ItemScreenState extends State .read() .state .isOfflineReading; + final bool isWebViewBottomSheetEnabled = context.select( + (PreferenceCubit cubit) => cubit.state.isWebViewBottomSheetEnabled, + ); final bool shouldShowWebViewBottomSheet = - !isOfflineReading && widget.item is Story && widget.item.url.isNotEmpty; + !isOfflineReading && + isWebViewBottomSheetEnabled && + widget.item is Story && + widget.item.url.isNotEmpty; return MultiBlocListener( listeners: >[ BlocListener( @@ -312,11 +320,7 @@ class _ItemScreenState extends State cmt, context.read().state.item, ), - onStoryUrlTapped: () { - setState(() { - _isWebViewBottomSheetVisible = true; - }); - }, + onStoryUrlTapped: _webViewController.show, shouldMarkNewComment: widget.shouldMarkNewComment, ), ), @@ -354,7 +358,11 @@ class _ItemScreenState extends State bottom: Dimens.pt36, child: FloatingSkipButtons(), ), - if (shouldShowWebViewBottomSheet) webViewBottomSheet, + if (shouldShowWebViewBottomSheet) + ItemScreenWebView( + url: widget.item.url, + controller: _webViewController, + ), Positioned( bottom: Dimens.zero, left: Dimens.zero, @@ -426,11 +434,7 @@ class _ItemScreenState extends State cmt, context.read().state.item, ), - onStoryUrlTapped: () { - setState(() { - _isWebViewBottomSheetVisible = true; - }); - }, + onStoryUrlTapped: _webViewController.show, shouldMarkNewComment: widget.shouldMarkNewComment, ), ), @@ -465,37 +469,16 @@ class _ItemScreenState extends State context.read().scrollTo(index: 0), ), ), - if (shouldShowWebViewBottomSheet) webViewBottomSheet, + if (shouldShowWebViewBottomSheet) + ItemScreenWebView( + url: widget.item.url, + controller: _webViewController, + ), ], ), ); } - Widget get webViewBottomSheet => Positioned.fill( - child: AnimatedSlide( - offset: Offset( - 0, - _isWebViewBottomSheetVisible ? 0 : _webViewOffsetInvisible, - ), - duration: AppDurations.ms200, - child: WebViewBottomSheet( - initialUrl: widget.item.url, - onDragHandleTapped: () { - if (!_isWebViewBottomSheetVisible) { - setState(() { - _isWebViewBottomSheetVisible = true; - }); - } - }, - onCloseTapped: () { - setState(() { - _isWebViewBottomSheetVisible = !_isWebViewBottomSheetVisible; - }); - }, - ), - ), - ); - void removeReplyBoxFocusOnScroll(double _) { focusNode.unfocus(); if (commentEditingController.text.isEmpty) { diff --git a/lib/screens/item/widgets/item_screen_web_view.dart b/lib/screens/item/widgets/item_screen_web_view.dart new file mode 100644 index 00000000..6f6b7a1f --- /dev/null +++ b/lib/screens/item/widgets/item_screen_web_view.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:hacki/config/constants.dart'; +import 'package:hacki/screens/widgets/widgets.dart'; + +/// Controls the visibility of an [ItemScreenWebView]. +/// +/// Held by the owning screen so the sheet can be revealed both from within +/// (drag handle / close button) and from the outside (e.g. tapping a story's +/// url in the header). +class ItemScreenWebViewController extends ChangeNotifier { + bool _isVisible = true; + + bool get isVisible => _isVisible; + + /// Reveals the sheet if it is currently hidden. + void show() { + if (_isVisible) return; + _isVisible = true; + notifyListeners(); + } + + /// Flips the sheet between visible and hidden. + void toggle() { + _isVisible = !_isVisible; + notifyListeners(); + } +} + +/// The draggable web view bottom sheet shown on the item screen for a story's +/// linked url. It slides just out of view when hidden instead of being removed +/// from the tree, so its state is preserved. +class ItemScreenWebView extends StatelessWidget { + const ItemScreenWebView({ + required this.url, + required this.controller, + super.key, + }); + + final String url; + final ItemScreenWebViewController controller; + + static const double _offsetInvisible = 0.1; + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: controller, + builder: (BuildContext context, Widget? child) { + return Positioned.fill( + child: AnimatedSlide( + offset: Offset(0, controller.isVisible ? 0 : _offsetInvisible), + duration: AppDurations.ms200, + child: child, + ), + ); + }, + child: WebViewBottomSheet( + initialUrl: url, + onDragHandleTapped: controller.show, + onCloseTapped: controller.toggle, + ), + ); + } +} diff --git a/lib/screens/item/widgets/widgets.dart b/lib/screens/item/widgets/widgets.dart index 0ac53f8e..850634e5 100644 --- a/lib/screens/item/widgets/widgets.dart +++ b/lib/screens/item/widgets/widgets.dart @@ -3,6 +3,7 @@ export 'custom_floating_action_button.dart'; export 'fav_icon_button.dart'; export 'in_thread_search_icon_button.dart'; export 'item_screen_background.dart'; +export 'item_screen_web_view.dart'; export 'lazy_fetch_load_button.dart'; export 'login_dialog.dart'; export 'main_view.dart'; diff --git a/lib/screens/widgets/web_view_bottom_sheet.dart b/lib/screens/widgets/web_view_bottom_sheet.dart index 85a59e9e..00122d71 100644 --- a/lib/screens/widgets/web_view_bottom_sheet.dart +++ b/lib/screens/widgets/web_view_bottom_sheet.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hacki/config/constants.dart'; -import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/screens/widgets/spring_curve.dart'; import 'package:hacki/styles/dimens.dart'; import 'package:hacki/styles/palette.dart'; @@ -34,14 +32,26 @@ class _WebViewBottomSheetState extends State late final Animation _rotationAnim; static const double _minChildSize = 0.1; static const double _maxChildSize = 0.94; - bool _isLoading = true; + bool _isLoading = false; bool _canGoBack = false; bool _canGoForward = false; + bool _hasLoaded = false; double _loadingProgress = 0; + /// Loads [WebViewBottomSheet.initialUrl] the first time the sheet is + /// expanded beyond its collapsed peek. Loading lazily (rather than in + /// [initState]) avoids running the linked page — and any audio/video it + /// autoplays — while the sheet is merely peeking and not being viewed. + void _loadIfNeeded() { + if (_hasLoaded) return; + _hasLoaded = true; + _controller.loadRequest(Uri.parse(widget.initialUrl)); + } + @override void initState() { super.initState(); + _urlController.text = widget.initialUrl; _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( @@ -71,8 +81,7 @@ class _WebViewBottomSheetState extends State setState(() => _isLoading = false); }, ), - ) - ..loadRequest(Uri.parse(widget.initialUrl)); + ); _animController = AnimationController( duration: AppDurations.ms300, @@ -81,6 +90,12 @@ class _WebViewBottomSheetState extends State _rotationAnim = Tween(begin: 0, end: 0.5).animate(_animController); _sheetController.addListener(() { final double newSize = _sheetController.size; + + /// Load the page only once the user starts expanding the sheet. + if (newSize > _minChildSize) { + _loadIfNeeded(); + } + final double scrollPosition = ((newSize - _minChildSize) / (_maxChildSize - _minChildSize)).clamp( 0.0, @@ -106,13 +121,6 @@ class _WebViewBottomSheetState extends State @override Widget build(BuildContext context) { - final bool isWebViewBottomSheetEnabled = context - .select( - (PreferenceCubit cubit) => cubit.state.isWebViewBottomSheetEnabled, - ); - if (!isWebViewBottomSheetEnabled) { - return const SizedBox.shrink(); - } return DraggableScrollableSheet( controller: _sheetController, snapAnimationDuration: AppDurations.ms200, From a72e91edb9562f2b98f158af9fdfbe6abc4f732d Mon Sep 17 00:00:00 2001 From: Livinglist Date: Wed, 29 Jul 2026 20:44:17 -0700 Subject: [PATCH 2/3] update --- .../item/widgets/item_screen_web_view.dart | 12 ++--- .../widgets/web_view_bottom_sheet.dart | 45 ++++++++++++------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/lib/screens/item/widgets/item_screen_web_view.dart b/lib/screens/item/widgets/item_screen_web_view.dart index 6f6b7a1f..645ba884 100644 --- a/lib/screens/item/widgets/item_screen_web_view.dart +++ b/lib/screens/item/widgets/item_screen_web_view.dart @@ -50,15 +50,15 @@ class ItemScreenWebView extends StatelessWidget { child: AnimatedSlide( offset: Offset(0, controller.isVisible ? 0 : _offsetInvisible), duration: AppDurations.ms200, - child: child, + child: WebViewBottomSheet( + initialUrl: url, + isVisible: controller.isVisible, + onDragHandleTapped: controller.show, + onCloseTapped: controller.toggle, + ), ), ); }, - child: WebViewBottomSheet( - initialUrl: url, - onDragHandleTapped: controller.show, - onCloseTapped: controller.toggle, - ), ); } } diff --git a/lib/screens/widgets/web_view_bottom_sheet.dart b/lib/screens/widgets/web_view_bottom_sheet.dart index 00122d71..12428aa2 100644 --- a/lib/screens/widgets/web_view_bottom_sheet.dart +++ b/lib/screens/widgets/web_view_bottom_sheet.dart @@ -9,12 +9,14 @@ import 'package:webview_flutter/webview_flutter.dart'; class WebViewBottomSheet extends StatefulWidget { const WebViewBottomSheet({ required this.initialUrl, + required this.isVisible, required this.onCloseTapped, required this.onDragHandleTapped, super.key, }); final String initialUrl; + final bool isVisible; final VoidCallback onCloseTapped; final VoidCallback onDragHandleTapped; @@ -35,19 +37,8 @@ class _WebViewBottomSheetState extends State bool _isLoading = false; bool _canGoBack = false; bool _canGoForward = false; - bool _hasLoaded = false; double _loadingProgress = 0; - /// Loads [WebViewBottomSheet.initialUrl] the first time the sheet is - /// expanded beyond its collapsed peek. Loading lazily (rather than in - /// [initState]) avoids running the linked page — and any audio/video it - /// autoplays — while the sheet is merely peeking and not being viewed. - void _loadIfNeeded() { - if (_hasLoaded) return; - _hasLoaded = true; - _controller.loadRequest(Uri.parse(widget.initialUrl)); - } - @override void initState() { super.initState(); @@ -57,6 +48,7 @@ class _WebViewBottomSheetState extends State ..setNavigationDelegate( NavigationDelegate( onPageStarted: (String url) { + if (!mounted) return; setState(() { _isLoading = true; _loadingProgress = 0; @@ -64,11 +56,13 @@ class _WebViewBottomSheetState extends State }); }, onProgress: (int progress) { + if (!mounted) return; setState(() => _loadingProgress = progress / 100.0); }, onPageFinished: (String url) async { final bool canBack = await _controller.canGoBack(); final bool canFwd = await _controller.canGoForward(); + if (!mounted) return; setState(() { _isLoading = false; _loadingProgress = 1.0; @@ -78,10 +72,12 @@ class _WebViewBottomSheetState extends State }); }, onWebResourceError: (WebResourceError error) { + if (!mounted) return; setState(() => _isLoading = false); }, ), - ); + ) + ..loadRequest(Uri.parse(widget.initialUrl)); _animController = AnimationController( duration: AppDurations.ms300, @@ -91,11 +87,6 @@ class _WebViewBottomSheetState extends State _sheetController.addListener(() { final double newSize = _sheetController.size; - /// Load the page only once the user starts expanding the sheet. - if (newSize > _minChildSize) { - _loadIfNeeded(); - } - final double scrollPosition = ((newSize - _minChildSize) / (_maxChildSize - _minChildSize)).clamp( 0.0, @@ -106,8 +97,25 @@ class _WebViewBottomSheetState extends State }); } + @override + void didUpdateWidget(WebViewBottomSheet oldWidget) { + super.didUpdateWidget(oldWidget); + + /// When the sheet is revealed again after being hidden, reload the + /// original page (it was navigated to about:blank when closed to stop + /// any media playback). + if (!oldWidget.isVisible && widget.isVisible) { + _controller.loadRequest(Uri.parse(widget.initialUrl)); + } + } + @override void dispose() { + /// Navigate the web view away from the loaded page so any audio/video it + /// is playing stops when the sheet is torn down (e.g. leaving the item + /// screen). WKWebView keeps playing media otherwise. + _controller.loadRequest(Uri.parse('about:blank')); + _animController.dispose(); _urlController.dispose(); _sheetController.dispose(); super.dispose(); @@ -192,6 +200,9 @@ class _WebViewBottomSheetState extends State onClose: () { if (_sheetController.isAttached) { if (_sheetController.size == _minChildSize) { + _controller.loadRequest( + Uri.parse('about:blank'), + ); widget.onCloseTapped(); } else { _sheetController.animateTo( From 8d031dee4c8dc12021f55ff9fdbfeeb0c2f73a0d Mon Sep 17 00:00:00 2001 From: Livinglist Date: Wed, 29 Jul 2026 20:45:06 -0700 Subject: [PATCH 3/3] format --- lib/screens/item/item_screen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/screens/item/item_screen.dart b/lib/screens/item/item_screen.dart index eca8495e..117b766f 100644 --- a/lib/screens/item/item_screen.dart +++ b/lib/screens/item/item_screen.dart @@ -230,10 +230,10 @@ class _ItemScreenState extends State .read() .state .isOfflineReading; - final bool isWebViewBottomSheetEnabled = context.select( - (PreferenceCubit cubit) => cubit.state.isWebViewBottomSheetEnabled, - ); + final bool isWebViewBottomSheetEnabled = context + .select( + (PreferenceCubit cubit) => cubit.state.isWebViewBottomSheetEnabled, + ); final bool shouldShowWebViewBottomSheet = !isOfflineReading && isWebViewBottomSheetEnabled &&