From 8173eab05dbb770ac76535ddb09808e9b35cd454 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 16 May 2025 16:55:44 +0100 Subject: [PATCH] fix: stop player when navigating away closes #24 --- lib/main.dart | 160 +++++++++++++++++---------------- lib/pages/layout.dart | 16 ---- lib/pages/stream.dart | 161 +++++++++++++++++++++------------- lib/widgets/profile.dart | 11 ++- lib/widgets/stream_info.dart | 2 +- lib/widgets/video_player.dart | 9 +- 6 files changed, 198 insertions(+), 161 deletions(-) delete mode 100644 lib/pages/layout.dart diff --git a/lib/main.dart b/lib/main.dart index 90bc2b1..7df06da 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,6 @@ import 'package:zap_stream_flutter/widgets/header.dart'; import 'login.dart'; import 'pages/home.dart'; -import 'pages/layout.dart'; class NoVerify extends EventVerifier { @override @@ -51,6 +50,8 @@ const defaultRelays = [ const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today"]; final loginData = LoginData(); +final RouteObserver> routeObserver = + RouteObserver>(); Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -94,96 +95,93 @@ Future main() async { ), routerConfig: GoRouter( routes: [ - StatefulShellRoute.indexedStack( + ShellRoute( + observers: [routeObserver], builder: - (context, state, navigationShell) => - SafeArea(child: LayoutScreen(navigationShell)), - branches: [ - StatefulShellBranch( - routes: [ - GoRoute(path: "/", builder: (ctx, state) => HomePage()), - ShellRoute( - builder: (context, state, child) { - return Container( - margin: EdgeInsets.only(top: 50), - padding: EdgeInsets.symmetric(horizontal: 5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 20, - children: [ - Center( - child: Image.asset( - "assets/logo.png", - height: 150, - ), - ), - child, - ], + (context, state, child) => SafeArea( + child: Scaffold(body: child, backgroundColor: Colors.black), + ), + routes: [ + GoRoute(path: "/", builder: (ctx, state) => HomePage()), + ShellRoute( + observers: [routeObserver], + builder: (context, state, child) { + return Container( + margin: EdgeInsets.only(top: 50), + padding: EdgeInsets.symmetric(horizontal: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 20, + children: [ + Center( + child: Image.asset("assets/logo.png", height: 150), ), - ); - }, + child, + ], + ), + ); + }, + routes: [ + GoRoute( + path: "/login", + builder: (ctx, state) => LoginPage(), routes: [ GoRoute( - path: "/login", - builder: (ctx, state) => LoginPage(), - routes: [ - GoRoute( - path: "key", - builder: (ctx, state) => LoginInputPage(), - ), - GoRoute( - path: "new", - builder: (context, state) => NewAccountPage(), - ), - ], + path: "key", + builder: (ctx, state) => LoginInputPage(), + ), + GoRoute( + path: "new", + builder: (context, state) => NewAccountPage(), ), ], ), + ], + ), + GoRoute( + path: StreamPage.path, + builder: (ctx, state) { + if (state.extra is StreamEvent) { + return StreamPage(stream: state.extra as StreamEvent); + } else { + throw UnimplementedError(); + } + }, + ), + GoRoute( + path: "/p/:id", + builder: (ctx, state) { + return ProfilePage(pubkey: state.pathParameters["id"]!); + }, + ), + GoRoute( + path: "/t/:id", + builder: (context, state) { + return HashtagPage(tag: state.pathParameters["id"]!); + }, + ), + GoRoute( + path: "/category/:id", + builder: (context, state) { + return CategoryPage( + category: state.pathParameters["id"]!, + info: state.extra as GameInfo?, + ); + }, + ), + ShellRoute( + observers: [routeObserver], + builder: + (context, state, child) => + Column(children: [HeaderWidget(), child]), + routes: [ GoRoute( - path: "/e/:id", - builder: (ctx, state) { - if (state.extra is StreamEvent) { - return StreamPage(stream: state.extra as StreamEvent); - } else { - throw UnimplementedError(); - } - }, - ), - GoRoute( - path: "/p/:id", - builder: (ctx, state) { - return ProfilePage(pubkey: state.pathParameters["id"]!); - }, - ), - GoRoute( - path: "/t/:id", - builder: (context, state) { - return HashtagPage(tag: state.pathParameters["id"]!); - }, - ), - GoRoute( - path: "/category/:id", - builder: (context, state) { - return CategoryPage( - category: state.pathParameters["id"]!, - info: state.extra as GameInfo?, - ); - }, - ), - ShellRoute( - builder: - (context, state, child) => - Column(children: [HeaderWidget(), child]), + path: "/settings", + builder: (context, state) => SizedBox(), routes: [ GoRoute( - path: "/settings", - builder: (context, state) => SizedBox(), - routes: [ - GoRoute( - path: "profile", - builder: (context, state) => SettingsProfilePage(), - ), - ], + path: "profile", + builder: (context, state) => SettingsProfilePage(), ), ], ), diff --git a/lib/pages/layout.dart b/lib/pages/layout.dart deleted file mode 100644 index 830ab89..0000000 --- a/lib/pages/layout.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -class LayoutScreen extends StatelessWidget { - final StatefulNavigationShell navigationShell; - - const LayoutScreen(this.navigationShell, {super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: navigationShell, - backgroundColor: Colors.black, - ); - } -} diff --git a/lib/pages/stream.dart b/lib/pages/stream.dart index f665fd5..2c13e71 100644 --- a/lib/pages/stream.dart +++ b/lib/pages/stream.dart @@ -1,7 +1,11 @@ +import 'dart:developer' as developer; + import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:ndk/ndk.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:zap_stream_flutter/imgproxy.dart'; +import 'package:zap_stream_flutter/main.dart'; import 'package:zap_stream_flutter/rx_filter.dart'; import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/utils.dart'; @@ -14,6 +18,7 @@ import 'package:zap_stream_flutter/widgets/video_player.dart'; import 'package:zap_stream_flutter/widgets/zap.dart'; class StreamPage extends StatefulWidget { + static const String path = "/e/:id"; final StreamEvent stream; const StreamPage({super.key, required this.stream}); @@ -22,17 +27,62 @@ class StreamPage extends StatefulWidget { State createState() => _StreamPage(); } -class _StreamPage extends State { +class _StreamPage extends State with RouteAware { + bool _offScreen = false; + + bool isWidgetVisible(BuildContext context) { + final router = GoRouter.of(context); + final currentConfiguration = router.routerDelegate.currentConfiguration; + final match = currentConfiguration.matches.lastOrNull; + final lastMatch = match is ShellRouteMatch ? match.matches.lastOrNull : match; + return lastMatch != null && + (lastMatch.route is GoRoute && + (lastMatch.route as GoRoute).path == StreamPage.path); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + routeObserver.subscribe(this, ModalRoute.of(context)!); + } + @override void initState() { - super.initState(); WakelockPlus.enable(); + super.initState(); } @override void dispose() { - super.dispose(); WakelockPlus.disable(); + routeObserver.unsubscribe(this); + super.dispose(); + } + + @override + void didPush() { + setState(() { + developer.log("STREAM: ON SCREEN"); + _offScreen = false; + }); + } + + @override + void didPopNext() { + setState(() { + developer.log("STREAM: ON SCREEN"); + _offScreen = false; + }); + } + + @override + void didPushNext() { + if (!isWidgetVisible(context)) { + setState(() { + developer.log("STREAM: OFF SCREEN"); + _offScreen = true; + }); + } } @override @@ -62,10 +112,11 @@ class _StreamPage extends State { AspectRatio( aspectRatio: 16 / 9, child: - stream.info.stream != null + (stream.info.stream != null && !_offScreen) ? VideoPlayerWidget( url: stream.info.stream!, placeholder: stream.info.image, + aspectRatio: 16 / 9, ) : (stream.info.image?.isNotEmpty ?? false) ? ProxyImg(url: stream.info.image) @@ -75,66 +126,58 @@ class _StreamPage extends State { stream.info.title ?? "", style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ProfileWidget.pubkey( + stream.info.host, children: [ - ProfileWidget.pubkey(stream.info.host), - Row( - spacing: 8, - children: [ - BasicButton( - Row(children: [Icon(Icons.bolt, size: 14), Text("Zap")]), - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 2), - decoration: BoxDecoration( - color: PRIMARY_1, - borderRadius: DEFAULT_BR, - ), - onTap: () { - showModalBottomSheet( - context: context, - constraints: BoxConstraints.expand(), - builder: (ctx) { - return SingleChildScrollView( - primary: false, - child: ZapWidget( - pubkey: stream.info.host, - target: stream.event, - zapTags: - // tag goal onto zap request - stream.info.goal != null - ? [ - ["e", stream.info.goal!], - ] - : null, - ), - ); - }, - ); - }, - ), - if (stream.info.participants != null) - PillWidget( - color: LAYER_1, - child: Text( - "${stream.info.participants} viewers", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, + Expanded(child: SizedBox()), // spacer + BasicButton( + Row(children: [Icon(Icons.bolt, size: 14), Text("Zap")]), + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 2), + decoration: BoxDecoration( + color: PRIMARY_1, + borderRadius: DEFAULT_BR, + ), + onTap: () { + showModalBottomSheet( + context: context, + constraints: BoxConstraints.expand(), + builder: (ctx) { + return SingleChildScrollView( + primary: false, + child: ZapWidget( + pubkey: stream.info.host, + target: stream.event, + zapTags: + // tag goal onto zap request + stream.info.goal != null + ? [ + ["e", stream.info.goal!], + ] + : null, ), - ), - ), - GestureDetector( - onTap: () { - showModalBottomSheet( - context: context, - constraints: BoxConstraints.expand(), - isScrollControlled: true, - builder: (context) => StreamInfoWidget(stream: stream), ); }, - child: Icon(Icons.info), + ); + }, + ), + if (stream.info.participants != null) + PillWidget( + color: LAYER_1, + child: Text( + "${stream.info.participants} viewers", + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ), - ], + ), + GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + constraints: BoxConstraints.expand(), + isScrollControlled: true, + builder: (context) => StreamInfoWidget(stream: stream), + ); + }, + child: Icon(Icons.info), ), ], ), diff --git a/lib/widgets/profile.dart b/lib/widgets/profile.dart index 23a1f31..612381b 100644 --- a/lib/widgets/profile.dart +++ b/lib/widgets/profile.dart @@ -88,6 +88,7 @@ class ProfileWidget extends StatelessWidget { final List? children; final bool? showName; final double? spacing; + final bool? linkToProfile; const ProfileWidget({ super.key, @@ -97,6 +98,7 @@ class ProfileWidget extends StatelessWidget { this.children, this.showName, this.spacing, + this.linkToProfile, }); static Widget pubkey( @@ -106,6 +108,7 @@ class ProfileWidget extends StatelessWidget { bool? showName, double? spacing, Key? key, + bool? linkToProfile, }) { return ProfileLoaderWidget(pubkey, (ctx, state) { return ProfileWidget( @@ -114,6 +117,7 @@ class ProfileWidget extends StatelessWidget { showName: showName, spacing: spacing, key: key, + linkToProfile: linkToProfile, children: children, ); }); @@ -125,7 +129,12 @@ class ProfileWidget extends StatelessWidget { spacing: spacing ?? 8, children: [ AvatarWidget(profile: profile, size: size), - if (showName ?? true) ProfileNameWidget(profile: profile, key: key), + if (showName ?? true) + ProfileNameWidget( + profile: profile, + key: key, + linkToProfile: linkToProfile, + ), ...(children ?? []), ], ); diff --git a/lib/widgets/stream_info.dart b/lib/widgets/stream_info.dart index 49d15de..74cb6a2 100644 --- a/lib/widgets/stream_info.dart +++ b/lib/widgets/stream_info.dart @@ -28,7 +28,7 @@ class StreamInfoWidget extends StatelessWidget { spacing: 8, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfileWidget.pubkey(stream.info.host), + ProfileWidget.pubkey(stream.info.host, linkToProfile: false), FollowButton( pubkey: stream.info.host, onTap: () { diff --git a/lib/widgets/video_player.dart b/lib/widgets/video_player.dart index e45d847..20dd8d7 100644 --- a/lib/widgets/video_player.dart +++ b/lib/widgets/video_player.dart @@ -23,19 +23,21 @@ class VideoPlayerWidget extends StatefulWidget { } class _VideoPlayerWidget extends State { + late final VideoPlayerController _controller; late final ChewieController _chewieController; @override void initState() { super.initState(); - final controller = VideoPlayerController.networkUrl( + _controller = VideoPlayerController.networkUrl( Uri.parse(widget.url), httpHeaders: Map.from({"user-agent": userAgent}), ); _chewieController = ChewieController( - videoPlayerController: controller, + videoPlayerController: _controller, autoPlay: widget.autoPlay ?? true, + aspectRatio: widget.aspectRatio, autoInitialize: true, placeholder: (widget.placeholder?.isNotEmpty ?? false) @@ -46,8 +48,9 @@ class _VideoPlayerWidget extends State { @override void dispose() { - super.dispose(); + _controller.dispose(); _chewieController.dispose(); + super.dispose(); } @override