fix: stop player when navigating away

closes #24
This commit is contained in:
2025-05-16 16:55:44 +01:00
parent e91807e80e
commit 8173eab05d
6 changed files with 198 additions and 161 deletions

View File

@ -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<ModalRoute<void>> routeObserver =
RouteObserver<ModalRoute<void>>();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -94,15 +95,16 @@ Future<void> main() async {
),
routerConfig: GoRouter(
routes: [
StatefulShellRoute.indexedStack(
ShellRoute(
observers: [routeObserver],
builder:
(context, state, navigationShell) =>
SafeArea(child: LayoutScreen(navigationShell)),
branches: [
StatefulShellBranch(
(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),
@ -112,10 +114,7 @@ Future<void> main() async {
spacing: 20,
children: [
Center(
child: Image.asset(
"assets/logo.png",
height: 150,
),
child: Image.asset("assets/logo.png", height: 150),
),
child,
],
@ -140,7 +139,7 @@ Future<void> main() async {
],
),
GoRoute(
path: "/e/:id",
path: StreamPage.path,
builder: (ctx, state) {
if (state.extra is StreamEvent) {
return StreamPage(stream: state.extra as StreamEvent);
@ -171,6 +170,7 @@ Future<void> main() async {
},
),
ShellRoute(
observers: [routeObserver],
builder:
(context, state, child) =>
Column(children: [HeaderWidget(), child]),
@ -191,8 +191,6 @@ Future<void> main() async {
),
],
),
],
),
),
);
}

View File

@ -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,
);
}
}

View File

@ -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<StatefulWidget> createState() => _StreamPage();
}
class _StreamPage extends State<StreamPage> {
class _StreamPage extends State<StreamPage> 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<StreamPage> {
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,13 +126,10 @@ class _StreamPage extends State<StreamPage> {
stream.info.title ?? "",
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ProfileWidget.pubkey(stream.info.host),
Row(
spacing: 8,
ProfileWidget.pubkey(
stream.info.host,
children: [
Expanded(child: SizedBox()), // spacer
BasicButton(
Row(children: [Icon(Icons.bolt, size: 14), Text("Zap")]),
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 2),
@ -117,10 +165,7 @@ class _StreamPage extends State<StreamPage> {
color: LAYER_1,
child: Text(
"${stream.info.participants} viewers",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
),
GestureDetector(
@ -136,8 +181,6 @@ class _StreamPage extends State<StreamPage> {
),
],
),
],
),
Expanded(child: ChatWidget(stream: stream)),
],
);

View File

@ -88,6 +88,7 @@ class ProfileWidget extends StatelessWidget {
final List<Widget>? 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 ?? []),
],
);

View File

@ -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: () {

View File

@ -23,19 +23,21 @@ class VideoPlayerWidget extends StatefulWidget {
}
class _VideoPlayerWidget extends State<VideoPlayerWidget> {
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<VideoPlayerWidget> {
@override
void dispose() {
super.dispose();
_controller.dispose();
_chewieController.dispose();
super.dispose();
}
@override