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 'login.dart';
import 'pages/home.dart'; import 'pages/home.dart';
import 'pages/layout.dart';
class NoVerify extends EventVerifier { class NoVerify extends EventVerifier {
@override @override
@ -51,6 +50,8 @@ const defaultRelays = [
const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today"]; const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today"];
final loginData = LoginData(); final loginData = LoginData();
final RouteObserver<ModalRoute<void>> routeObserver =
RouteObserver<ModalRoute<void>>();
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -94,96 +95,93 @@ Future<void> main() async {
), ),
routerConfig: GoRouter( routerConfig: GoRouter(
routes: [ routes: [
StatefulShellRoute.indexedStack( ShellRoute(
observers: [routeObserver],
builder: builder:
(context, state, navigationShell) => (context, state, child) => SafeArea(
SafeArea(child: LayoutScreen(navigationShell)), child: Scaffold(body: child, backgroundColor: Colors.black),
branches: [ ),
StatefulShellBranch( routes: [
routes: [ GoRoute(path: "/", builder: (ctx, state) => HomePage()),
GoRoute(path: "/", builder: (ctx, state) => HomePage()), ShellRoute(
ShellRoute( observers: [routeObserver],
builder: (context, state, child) { builder: (context, state, child) {
return Container( return Container(
margin: EdgeInsets.only(top: 50), margin: EdgeInsets.only(top: 50),
padding: EdgeInsets.symmetric(horizontal: 5), padding: EdgeInsets.symmetric(horizontal: 5),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20, spacing: 20,
children: [ children: [
Center( Center(
child: Image.asset( child: Image.asset("assets/logo.png", height: 150),
"assets/logo.png",
height: 150,
),
),
child,
],
), ),
); child,
}, ],
),
);
},
routes: [
GoRoute(
path: "/login",
builder: (ctx, state) => LoginPage(),
routes: [ routes: [
GoRoute( GoRoute(
path: "/login", path: "key",
builder: (ctx, state) => LoginPage(), builder: (ctx, state) => LoginInputPage(),
routes: [ ),
GoRoute( GoRoute(
path: "key", path: "new",
builder: (ctx, state) => LoginInputPage(), builder: (context, state) => NewAccountPage(),
),
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( GoRoute(
path: "/e/:id", path: "/settings",
builder: (ctx, state) { builder: (context, state) => SizedBox(),
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]),
routes: [ routes: [
GoRoute( GoRoute(
path: "/settings", path: "profile",
builder: (context, state) => SizedBox(), builder: (context, state) => SettingsProfilePage(),
routes: [
GoRoute(
path: "profile",
builder: (context, state) => SettingsProfilePage(),
),
],
), ),
], ],
), ),

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:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:ndk/ndk.dart'; import 'package:ndk/ndk.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:zap_stream_flutter/imgproxy.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/rx_filter.dart';
import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/utils.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'; import 'package:zap_stream_flutter/widgets/zap.dart';
class StreamPage extends StatefulWidget { class StreamPage extends StatefulWidget {
static const String path = "/e/:id";
final StreamEvent stream; final StreamEvent stream;
const StreamPage({super.key, required this.stream}); const StreamPage({super.key, required this.stream});
@ -22,17 +27,62 @@ class StreamPage extends StatefulWidget {
State<StatefulWidget> createState() => _StreamPage(); 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 @override
void initState() { void initState() {
super.initState();
WakelockPlus.enable(); WakelockPlus.enable();
super.initState();
} }
@override @override
void dispose() { void dispose() {
super.dispose();
WakelockPlus.disable(); 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 @override
@ -62,10 +112,11 @@ class _StreamPage extends State<StreamPage> {
AspectRatio( AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: child:
stream.info.stream != null (stream.info.stream != null && !_offScreen)
? VideoPlayerWidget( ? VideoPlayerWidget(
url: stream.info.stream!, url: stream.info.stream!,
placeholder: stream.info.image, placeholder: stream.info.image,
aspectRatio: 16 / 9,
) )
: (stream.info.image?.isNotEmpty ?? false) : (stream.info.image?.isNotEmpty ?? false)
? ProxyImg(url: stream.info.image) ? ProxyImg(url: stream.info.image)
@ -75,66 +126,58 @@ class _StreamPage extends State<StreamPage> {
stream.info.title ?? "", stream.info.title ?? "",
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18), style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
), ),
Row( ProfileWidget.pubkey(
mainAxisAlignment: MainAxisAlignment.spaceBetween, stream.info.host,
children: [ children: [
ProfileWidget.pubkey(stream.info.host), Expanded(child: SizedBox()), // spacer
Row( BasicButton(
spacing: 8, Row(children: [Icon(Icons.bolt, size: 14), Text("Zap")]),
children: [ padding: EdgeInsets.symmetric(horizontal: 10, vertical: 2),
BasicButton( decoration: BoxDecoration(
Row(children: [Icon(Icons.bolt, size: 14), Text("Zap")]), color: PRIMARY_1,
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 2), borderRadius: DEFAULT_BR,
decoration: BoxDecoration( ),
color: PRIMARY_1, onTap: () {
borderRadius: DEFAULT_BR, showModalBottomSheet(
), context: context,
onTap: () { constraints: BoxConstraints.expand(),
showModalBottomSheet( builder: (ctx) {
context: context, return SingleChildScrollView(
constraints: BoxConstraints.expand(), primary: false,
builder: (ctx) { child: ZapWidget(
return SingleChildScrollView( pubkey: stream.info.host,
primary: false, target: stream.event,
child: ZapWidget( zapTags:
pubkey: stream.info.host, // tag goal onto zap request
target: stream.event, stream.info.goal != null
zapTags: ? [
// tag goal onto zap request ["e", stream.info.goal!],
stream.info.goal != null ]
? [ : 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,
), ),
),
),
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),
), ),
], ],
), ),

View File

@ -88,6 +88,7 @@ class ProfileWidget extends StatelessWidget {
final List<Widget>? children; final List<Widget>? children;
final bool? showName; final bool? showName;
final double? spacing; final double? spacing;
final bool? linkToProfile;
const ProfileWidget({ const ProfileWidget({
super.key, super.key,
@ -97,6 +98,7 @@ class ProfileWidget extends StatelessWidget {
this.children, this.children,
this.showName, this.showName,
this.spacing, this.spacing,
this.linkToProfile,
}); });
static Widget pubkey( static Widget pubkey(
@ -106,6 +108,7 @@ class ProfileWidget extends StatelessWidget {
bool? showName, bool? showName,
double? spacing, double? spacing,
Key? key, Key? key,
bool? linkToProfile,
}) { }) {
return ProfileLoaderWidget(pubkey, (ctx, state) { return ProfileLoaderWidget(pubkey, (ctx, state) {
return ProfileWidget( return ProfileWidget(
@ -114,6 +117,7 @@ class ProfileWidget extends StatelessWidget {
showName: showName, showName: showName,
spacing: spacing, spacing: spacing,
key: key, key: key,
linkToProfile: linkToProfile,
children: children, children: children,
); );
}); });
@ -125,7 +129,12 @@ class ProfileWidget extends StatelessWidget {
spacing: spacing ?? 8, spacing: spacing ?? 8,
children: [ children: [
AvatarWidget(profile: profile, size: size), AvatarWidget(profile: profile, size: size),
if (showName ?? true) ProfileNameWidget(profile: profile, key: key), if (showName ?? true)
ProfileNameWidget(
profile: profile,
key: key,
linkToProfile: linkToProfile,
),
...(children ?? []), ...(children ?? []),
], ],
); );

View File

@ -28,7 +28,7 @@ class StreamInfoWidget extends StatelessWidget {
spacing: 8, spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ProfileWidget.pubkey(stream.info.host), ProfileWidget.pubkey(stream.info.host, linkToProfile: false),
FollowButton( FollowButton(
pubkey: stream.info.host, pubkey: stream.info.host,
onTap: () { onTap: () {

View File

@ -23,19 +23,21 @@ class VideoPlayerWidget extends StatefulWidget {
} }
class _VideoPlayerWidget extends State<VideoPlayerWidget> { class _VideoPlayerWidget extends State<VideoPlayerWidget> {
late final VideoPlayerController _controller;
late final ChewieController _chewieController; late final ChewieController _chewieController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final controller = VideoPlayerController.networkUrl( _controller = VideoPlayerController.networkUrl(
Uri.parse(widget.url), Uri.parse(widget.url),
httpHeaders: Map.from({"user-agent": userAgent}), httpHeaders: Map.from({"user-agent": userAgent}),
); );
_chewieController = ChewieController( _chewieController = ChewieController(
videoPlayerController: controller, videoPlayerController: _controller,
autoPlay: widget.autoPlay ?? true, autoPlay: widget.autoPlay ?? true,
aspectRatio: widget.aspectRatio,
autoInitialize: true, autoInitialize: true,
placeholder: placeholder:
(widget.placeholder?.isNotEmpty ?? false) (widget.placeholder?.isNotEmpty ?? false)
@ -46,8 +48,9 @@ class _VideoPlayerWidget extends State<VideoPlayerWidget> {
@override @override
void dispose() { void dispose() {
super.dispose(); _controller.dispose();
_chewieController.dispose(); _chewieController.dispose();
super.dispose();
} }
@override @override