Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 23 additions & 40 deletions lib/screens/item/item_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ class _ItemScreenState extends State<ItemScreen>
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() {
Expand Down Expand Up @@ -220,6 +220,7 @@ class _ItemScreenState extends State<ItemScreen>
featureDiscoveryDismissThrottle.dispose();
focusNode.dispose();
scrollOffsetSubscription?.cancel();
_webViewController.dispose();
super.dispose();
}

Expand All @@ -229,8 +230,15 @@ class _ItemScreenState extends State<ItemScreen>
.read<CommentsCubit>()
.state
.isOfflineReading;
final bool isWebViewBottomSheetEnabled = context
.select<PreferenceCubit, bool>(
(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<dynamic, dynamic>>[
BlocListener<PostCubit, PostState>(
Expand Down Expand Up @@ -312,11 +320,7 @@ class _ItemScreenState extends State<ItemScreen>
cmt,
context.read<CommentsCubit>().state.item,
),
onStoryUrlTapped: () {
setState(() {
_isWebViewBottomSheetVisible = true;
});
},
onStoryUrlTapped: _webViewController.show,
shouldMarkNewComment: widget.shouldMarkNewComment,
),
),
Expand Down Expand Up @@ -354,7 +358,11 @@ class _ItemScreenState extends State<ItemScreen>
bottom: Dimens.pt36,
child: FloatingSkipButtons(),
),
if (shouldShowWebViewBottomSheet) webViewBottomSheet,
if (shouldShowWebViewBottomSheet)
ItemScreenWebView(
url: widget.item.url,
controller: _webViewController,
),
Positioned(
bottom: Dimens.zero,
left: Dimens.zero,
Expand Down Expand Up @@ -426,11 +434,7 @@ class _ItemScreenState extends State<ItemScreen>
cmt,
context.read<CommentsCubit>().state.item,
),
onStoryUrlTapped: () {
setState(() {
_isWebViewBottomSheetVisible = true;
});
},
onStoryUrlTapped: _webViewController.show,
shouldMarkNewComment: widget.shouldMarkNewComment,
),
),
Expand Down Expand Up @@ -465,37 +469,16 @@ class _ItemScreenState extends State<ItemScreen>
context.read<CommentsCubit>().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) {
Expand Down
64 changes: 64 additions & 0 deletions lib/screens/item/widgets/item_screen_web_view.dart
Original file line number Diff line number Diff line change
@@ -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: WebViewBottomSheet(
initialUrl: url,
isVisible: controller.isVisible,
onDragHandleTapped: controller.show,
onCloseTapped: controller.toggle,
),
),
);
},
);
}
}
1 change: 1 addition & 0 deletions lib/screens/item/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
39 changes: 29 additions & 10 deletions lib/screens/widgets/web_view_bottom_sheet.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,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;

Expand All @@ -34,31 +34,35 @@ class _WebViewBottomSheetState extends State<WebViewBottomSheet>
late final Animation<double> _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;
double _loadingProgress = 0;

@override
void initState() {
super.initState();
_urlController.text = widget.initialUrl;
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) {
if (!mounted) return;
setState(() {
_isLoading = true;
_loadingProgress = 0;
_urlController.text = url;
});
},
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;
Expand All @@ -68,6 +72,7 @@ class _WebViewBottomSheetState extends State<WebViewBottomSheet>
});
},
onWebResourceError: (WebResourceError error) {
if (!mounted) return;
setState(() => _isLoading = false);
},
),
Expand All @@ -81,6 +86,7 @@ class _WebViewBottomSheetState extends State<WebViewBottomSheet>
_rotationAnim = Tween<double>(begin: 0, end: 0.5).animate(_animController);
_sheetController.addListener(() {
final double newSize = _sheetController.size;

final double scrollPosition =
((newSize - _minChildSize) / (_maxChildSize - _minChildSize)).clamp(
0.0,
Expand All @@ -91,8 +97,25 @@ class _WebViewBottomSheetState extends State<WebViewBottomSheet>
});
}

@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();
Expand All @@ -106,13 +129,6 @@ class _WebViewBottomSheetState extends State<WebViewBottomSheet>

@override
Widget build(BuildContext context) {
final bool isWebViewBottomSheetEnabled = context
.select<PreferenceCubit, bool>(
(PreferenceCubit cubit) => cubit.state.isWebViewBottomSheetEnabled,
);
if (!isWebViewBottomSheetEnabled) {
return const SizedBox.shrink();
}
return DraggableScrollableSheet(
controller: _sheetController,
snapAnimationDuration: AppDurations.ms200,
Expand Down Expand Up @@ -184,6 +200,9 @@ class _WebViewBottomSheetState extends State<WebViewBottomSheet>
onClose: () {
if (_sheetController.isAttached) {
if (_sheetController.size == _minChildSize) {
_controller.loadRequest(
Uri.parse('about:blank'),
);
widget.onCloseTapped();
} else {
_sheetController.animateTo(
Expand Down