diff --git a/lib/src/utils/focus_detector.dart b/lib/src/utils/focus_detector.dart new file mode 100644 index 0000000000..40d5800eed --- /dev/null +++ b/lib/src/utils/focus_detector.dart @@ -0,0 +1,176 @@ +import 'package:flutter/widgets.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +// code taken and adapted from +// https://github.com/EdsonBueno/focus_detector + +/// Fires callbacks every time the widget appears or disappears from the screen. +class FocusDetector extends StatefulWidget { + const FocusDetector({ + required this.child, + this.onFocusGained, + this.onFocusRegained, + this.onFocusLost, + this.onVisibilityGained, + this.onVisibilityRegained, + this.onVisibilityLost, + this.onForegroundGained, + this.onForegroundLost, + super.key, + }); + + /// Called when the widget becomes visible or enters foreground while visible. + final VoidCallback? onFocusGained; + + /// Called when the widget gains focus again (same as onFocusGained but does + /// not fires the first time). + final VoidCallback? onFocusRegained; + + /// Called when the widget becomes invisible or enters background while visible. + final VoidCallback? onFocusLost; + + /// Called when the widget becomes visible. + final VoidCallback? onVisibilityGained; + + /// Called when the widget become visible again (same as onVisibilityGained but + /// does not fires the first time). + final VoidCallback? onVisibilityRegained; + + /// Called when the widget becomes invisible. + final VoidCallback? onVisibilityLost; + + /// Called when the app entered the foreground while the widget is visible. + final VoidCallback? onForegroundGained; + + /// Called when the app is sent to background while the widget was visible. + final VoidCallback? onForegroundLost; + + /// The widget below this widget in the tree. + final Widget child; + + @override + _FocusDetectorState createState() => _FocusDetectorState(); +} + +class _FocusDetectorState extends State + with WidgetsBindingObserver { + final _visibilityDetectorKey = UniqueKey(); + + /// Counter to keep track of the visibility changes. + int _visibilityCounter = 0; + + /// Whether this widget is currently visible within the app. + bool _isWidgetVisible = false; + + /// Whether the app is in the foreground. + bool _isAppInForeground = true; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _notifyPlaneTransition(state); + } + + /// Notifies app's transitions to/from the foreground. + void _notifyPlaneTransition(AppLifecycleState state) { + if (!_isWidgetVisible) { + return; + } + + final isAppResumed = state == AppLifecycleState.resumed; + final wasResumed = _isAppInForeground; + if (isAppResumed && !wasResumed) { + _isAppInForeground = true; + _notifyFocusGain(); + _notifyForegroundGain(); + return; + } + + final isAppPaused = state == AppLifecycleState.paused; + if (isAppPaused && wasResumed) { + _isAppInForeground = false; + _notifyFocusLoss(); + _notifyForegroundLoss(); + } + } + + @override + Widget build(BuildContext context) => VisibilityDetector( + key: _visibilityDetectorKey, + onVisibilityChanged: (visibilityInfo) { + final visibleFraction = visibilityInfo.visibleFraction; + _notifyVisibilityStatusChange(visibleFraction); + }, + child: widget.child, + ); + + /// Notifies changes in the widget's visibility. + void _notifyVisibilityStatusChange(double newVisibleFraction) { + if (!_isAppInForeground) { + return; + } + + final wasFullyVisible = _isWidgetVisible; + final isFullyVisible = newVisibleFraction == 1; + if (!wasFullyVisible && isFullyVisible) { + _isWidgetVisible = true; + _notifyFocusGain(); + _notifyVisibilityGain(); + _visibilityCounter++; + } + + final isFullyInvisible = newVisibleFraction == 0; + if (wasFullyVisible && isFullyInvisible) { + _isWidgetVisible = false; + _notifyFocusLoss(); + _notifyVisibilityLoss(); + } + } + + void _notifyFocusGain() { + widget.onFocusGained?.call(); + if (_visibilityCounter > 0) { + widget.onFocusRegained?.call(); + } + } + + void _notifyFocusLoss() { + widget.onFocusLost?.call(); + } + + void _notifyVisibilityGain() { + widget.onVisibilityGained?.call(); + if (_visibilityCounter > 0) { + widget.onVisibilityRegained?.call(); + } + } + + void _notifyVisibilityLoss() { + widget.onVisibilityLost?.call(); + } + + void _notifyForegroundGain() { + final onForegroundGained = widget.onForegroundGained; + if (onForegroundGained != null) { + onForegroundGained(); + } + } + + void _notifyForegroundLoss() { + final onForegroundLost = widget.onForegroundLost; + if (onForegroundLost != null) { + onForegroundLost(); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } +} diff --git a/lib/src/view/relation/relation_screen.dart b/lib/src/view/relation/relation_screen.dart index 0ac8959afe..ebbbada3c7 100644 --- a/lib/src/view/relation/relation_screen.dart +++ b/lib/src/view/relation/relation_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/relation/relation_ctrl.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/relation/following_screen.dart'; @@ -22,13 +22,22 @@ class RelationScreen extends ConsumerStatefulWidget { ConsumerState createState() => _RelationScreenState(); } -class _RelationScreenState extends ConsumerState - with RouteAware { +class _RelationScreenState extends ConsumerState { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + return FocusDetector( + onFocusRegained: () { + ref.read(relationCtrlProvider.notifier).startWatchingFriends(); + }, + onFocusLost: () { + if (context.mounted) { + ref.read(relationCtrlProvider.notifier).stopWatchingFriends(); + } + }, + child: PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ), ); } @@ -47,33 +56,6 @@ class _RelationScreenState extends ConsumerState child: _Body(), ); } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final route = ModalRoute.of(context); - if (route != null && route is PageRoute) { - homeRouteObserver.subscribe(this, route); - } - } - - @override - void dispose() { - homeRouteObserver.unsubscribe(this); - super.dispose(); - } - - @override - void didPushNext() { - ref.read(relationCtrlProvider.notifier).stopWatchingFriends(); - super.didPushNext(); - } - - @override - void didPopNext() { - ref.read(relationCtrlProvider.notifier).startWatchingFriends(); - super.didPopNext(); - } } class _Body extends StatelessWidget { diff --git a/lib/src/view/watch/live_tv_channels_screen.dart b/lib/src/view/watch/live_tv_channels_screen.dart index 85760df19f..049a5209d6 100644 --- a/lib/src/view/watch/live_tv_channels_screen.dart +++ b/lib/src/view/watch/live_tv_channels_screen.dart @@ -4,30 +4,34 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/tv/live_tv_channels.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; -import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/chessground_compat.dart'; +import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/watch/tv_screen.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -class LiveTvChannelsScreen extends ConsumerStatefulWidget { +class LiveTvChannelsScreen extends ConsumerWidget { const LiveTvChannelsScreen({super.key}); @override - ConsumerState createState() => _TvChannelsScreenState(); -} - -class _TvChannelsScreenState extends ConsumerState - with RouteAware, WidgetsBindingObserver { - @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + Widget build(BuildContext context, WidgetRef ref) { + return FocusDetector( + onFocusRegained: () { + ref.read(liveTvChannelsProvider.notifier).startWatching(); + }, + onFocusLost: () { + if (context.mounted) { + ref.read(liveTvChannelsProvider.notifier).stopWatching(); + } + }, + child: PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ), ); } @@ -52,55 +56,6 @@ class _TvChannelsScreenState extends ConsumerState child: _Body(), ); } - - @override - void initState() { - WidgetsBinding.instance.addObserver(this); - super.initState(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - ref.read(liveTvChannelsProvider.notifier).startWatching(); - } else { - ref.read(liveTvChannelsProvider.notifier).stopWatching(); - } - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final route = ModalRoute.of(context); - if (route != null && route is PageRoute) { - rootNavPageRouteObserver.subscribe(this, route); - } - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - rootNavPageRouteObserver.unsubscribe(this); - super.dispose(); - } - - @override - void didPushNext() { - ref.read(liveTvChannelsProvider.notifier).stopWatching(); - super.didPushNext(); - } - - @override - void didPopNext() { - ref.read(liveTvChannelsProvider.notifier).startWatching(); - super.didPopNext(); - } - - @override - void didPop() { - ref.read(liveTvChannelsProvider.notifier).stopWatching(); - super.didPop(); - } } class _Body extends ConsumerWidget { diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 0b91510a4a..63162b3267 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -7,9 +7,9 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_controller.dart'; -import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/chessground_compat.dart'; +import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; @@ -29,8 +29,7 @@ class TvScreen extends ConsumerStatefulWidget { ConsumerState createState() => _TvScreenState(); } -class _TvScreenState extends ConsumerState - with RouteAware, WidgetsBindingObserver { +class _TvScreenState extends ConsumerState { TvControllerProvider get _tvGameCtrl => tvControllerProvider(widget.channel, widget.initialGame); @@ -39,9 +38,19 @@ class _TvScreenState extends ConsumerState @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + return FocusDetector( + onFocusRegained: () { + ref.read(_tvGameCtrl.notifier).startWatching(); + }, + onFocusLost: () { + if (context.mounted) { + ref.read(_tvGameCtrl.notifier).stopWatching(); + } + }, + child: PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ), ); } @@ -81,55 +90,6 @@ class _TvScreenState extends ConsumerState ), ); } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - ref.read(_tvGameCtrl.notifier).startWatching(); - } else { - ref.read(_tvGameCtrl.notifier).stopWatching(); - } - } - - @override - void initState() { - WidgetsBinding.instance.addObserver(this); - super.initState(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final route = ModalRoute.of(context); - if (route != null && route is PageRoute) { - rootNavPageRouteObserver.subscribe(this, route); - } - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - rootNavPageRouteObserver.unsubscribe(this); - super.dispose(); - } - - @override - void didPushNext() { - ref.read(_tvGameCtrl.notifier).stopWatching(); - super.didPushNext(); - } - - @override - void didPopNext() { - ref.read(_tvGameCtrl.notifier).startWatching(); - super.didPopNext(); - } - - @override - void didPop() { - ref.read(_tvGameCtrl.notifier).stopWatching(); - super.didPop(); - } } class _Body extends ConsumerWidget { diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 293b26b082..644e7c52cf 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -190,7 +190,6 @@ class _WatchTvWidget extends ConsumerWidget { headerTrailing: NoPaddingTextButton( onPressed: () => pushPlatformRoute( context, - rootNavigator: true, builder: (context) => const LiveTvChannelsScreen(), ).then((_) => _refreshData(ref)), child: Text( diff --git a/pubspec.lock b/pubspec.lock index 78072a1f46..3ffd8b4aac 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1453,7 +1453,7 @@ packages: source: hosted version: "2.1.4" visibility_detector: - dependency: transitive + dependency: "direct main" description: name: visibility_detector sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" diff --git a/pubspec.yaml b/pubspec.yaml index df82c5f2cf..33c4a1b191 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: system_info_plus: ^0.0.5 timeago: ^3.6.0 url_launcher: ^6.1.9 + visibility_detector: ^0.3.3 wakelock_plus: ^1.1.1 web_socket_channel: ^2.4.0