mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-15 19:48:23 +00:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
b5b5ef0daf
|
|||
3b07f7d073
|
|||
64655ba21c
|
|||
5ffab5f7bf
|
|||
0d8ce78009
|
|||
ef1c03d8d5
|
49
lib/app.dart
49
lib/app.dart
@ -1,6 +1,9 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||||
import 'package:zap_stream_flutter/const.dart';
|
import 'package:zap_stream_flutter/const.dart';
|
||||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||||
import 'package:zap_stream_flutter/pages/category.dart';
|
import 'package:zap_stream_flutter/pages/category.dart';
|
||||||
@ -19,6 +22,26 @@ import 'package:zap_stream_flutter/theme.dart';
|
|||||||
import 'package:zap_stream_flutter/utils.dart';
|
import 'package:zap_stream_flutter/utils.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/header.dart';
|
import 'package:zap_stream_flutter/widgets/header.dart';
|
||||||
|
|
||||||
|
/// Resolves a NIP-05 identifier to a pubkey
|
||||||
|
Future<String?> resolveNip05(String handle, String domain) async {
|
||||||
|
try {
|
||||||
|
final url = "https://$domain/.well-known/nostr.json?name=$handle";
|
||||||
|
final response = await http.get(Uri.parse(url));
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
final names = json["names"] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
if (names != null) {
|
||||||
|
return names[handle] as String?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// NIP-05 resolution failed
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
void runZapStream() {
|
void runZapStream() {
|
||||||
runApp(
|
runApp(
|
||||||
MaterialApp.router(
|
MaterialApp.router(
|
||||||
@ -142,7 +165,7 @@ void runZapStream() {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/:id",
|
path: "/:id",
|
||||||
redirect: (context, state) {
|
redirect: (context, state) async {
|
||||||
final id = state.pathParameters["id"]!;
|
final id = state.pathParameters["id"]!;
|
||||||
if (id.startsWith("naddr1") ||
|
if (id.startsWith("naddr1") ||
|
||||||
id.startsWith("nevent1") ||
|
id.startsWith("nevent1") ||
|
||||||
@ -151,6 +174,30 @@ void runZapStream() {
|
|||||||
} else if (id.startsWith("npub1") ||
|
} else if (id.startsWith("npub1") ||
|
||||||
id.startsWith("nprofile1")) {
|
id.startsWith("nprofile1")) {
|
||||||
return "/p/$id";
|
return "/p/$id";
|
||||||
|
} else {
|
||||||
|
// Handle short URL format (handle@domain or just handle)
|
||||||
|
try {
|
||||||
|
final handleParts = id.contains("@")
|
||||||
|
? id.split("@")
|
||||||
|
: [id, "zap.stream"];
|
||||||
|
|
||||||
|
if (handleParts.length == 2) {
|
||||||
|
final handle = handleParts[0];
|
||||||
|
final domain = handleParts[1];
|
||||||
|
|
||||||
|
// Try to resolve NIP-05
|
||||||
|
final hexPubkey = await resolveNip05(handle, domain);
|
||||||
|
if (hexPubkey != null) {
|
||||||
|
// Check if they have a current live stream
|
||||||
|
// For now, redirect to profile - we could enhance this later
|
||||||
|
// to check for active streams and redirect to /e/{stream_id} instead
|
||||||
|
final npub = Nip19.encodePubKey(hexPubkey);
|
||||||
|
return "/p/$npub";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If NIP-05 resolution fails, continue to show 404 or fallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
@ -139,17 +139,13 @@ class _LivePage extends State<LivePage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mq = MediaQuery.of(context);
|
final mq = MediaQuery.of(context);
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: false,
|
canPop: !_streaming,
|
||||||
onPopInvokedWithResult: (didPop, result) async {
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
if (_streaming) {
|
if (_streaming && !didPop) {
|
||||||
final go = await showExitStreamDialog(context);
|
final go = await showExitStreamDialog(context);
|
||||||
if (context.mounted) {
|
if (context.mounted && go == true) {
|
||||||
if (go == true) {
|
context.go("/");
|
||||||
context.go("/");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
context.go("/");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: ValueListenableBuilder(
|
child: ValueListenableBuilder(
|
||||||
|
@ -57,7 +57,7 @@ class StreamPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _StreamPage extends State<StreamPage> with RouteAware {
|
class _StreamPage extends State<StreamPage> with RouteAware {
|
||||||
bool _offScreen = false;
|
bool _offScreen = false;
|
||||||
StreamEvent? _stream;
|
final GlobalKey _playerKey = GlobalKey();
|
||||||
|
|
||||||
bool isWidgetVisible(BuildContext context) {
|
bool isWidgetVisible(BuildContext context) {
|
||||||
final router = GoRouter.of(context);
|
final router = GoRouter.of(context);
|
||||||
@ -132,11 +132,11 @@ class _StreamPage extends State<StreamPage> with RouteAware {
|
|||||||
final streamWidget = _buildPlayer(ctx, stream);
|
final streamWidget = _buildPlayer(ctx, stream);
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable: mainPlayer.state,
|
valueListenable: mainPlayer.state,
|
||||||
builder: (context, state, player) {
|
builder: (context, playerState, child) {
|
||||||
if (state?.isPortrait == true) {
|
if (playerState?.isPortrait == true) {
|
||||||
return _buildPortraitStream(context, stream, player!);
|
return _buildPortraitStream(context, stream, child!);
|
||||||
}
|
}
|
||||||
return _buildLandscapeStream(context, stream, player!);
|
return _buildLandscapeStream(context, stream, child!);
|
||||||
},
|
},
|
||||||
child: streamWidget,
|
child: streamWidget,
|
||||||
);
|
);
|
||||||
@ -147,6 +147,7 @@ class _StreamPage extends State<StreamPage> with RouteAware {
|
|||||||
Widget _buildPlayer(BuildContext context, StreamEvent stream) {
|
Widget _buildPlayer(BuildContext context, StreamEvent stream) {
|
||||||
return (stream.info.stream != null && !_offScreen)
|
return (stream.info.stream != null && !_offScreen)
|
||||||
? MainVideoPlayerWidget(
|
? MainVideoPlayerWidget(
|
||||||
|
key: _playerKey,
|
||||||
url: stream.info.stream!,
|
url: stream.info.stream!,
|
||||||
placeholder: stream.info.image,
|
placeholder: stream.info.image,
|
||||||
isLive: true,
|
isLive: true,
|
||||||
@ -170,21 +171,20 @@ class _StreamPage extends State<StreamPage> with RouteAware {
|
|||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
child,
|
|
||||||
Positioned(child: child),
|
Positioned(child: child),
|
||||||
Positioned(
|
Positioned(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 5),
|
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.bottomCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.topCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
LAYER_0.withAlpha(50),
|
|
||||||
LAYER_0.withAlpha(200),
|
|
||||||
LAYER_0.withAlpha(255),
|
LAYER_0.withAlpha(255),
|
||||||
|
LAYER_0.withAlpha(200),
|
||||||
|
LAYER_0.withAlpha(10),
|
||||||
],
|
],
|
||||||
stops: [0.0, 0.2, 1.0],
|
stops: [0.0, 0.7, 1.0],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -200,20 +200,31 @@ class _StreamPage extends State<StreamPage> with RouteAware {
|
|||||||
width: mq.size.width,
|
width: mq.size.width,
|
||||||
height: mq.size.height * 0.4,
|
height: mq.size.height * 0.4,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
LAYER_0.withAlpha(200),
|
||||||
|
LAYER_0.withAlpha(150),
|
||||||
|
LAYER_0.withAlpha(0),
|
||||||
|
],
|
||||||
|
stops: [0.0, 0.8, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
child: ShaderMask(
|
child: ShaderMask(
|
||||||
shaderCallback: (Rect bounds) {
|
|
||||||
return LinearGradient(
|
|
||||||
begin: Alignment.bottomCenter,
|
|
||||||
end: Alignment.topCenter,
|
|
||||||
colors: [
|
|
||||||
LAYER_0.withAlpha(255),
|
|
||||||
LAYER_0.withAlpha(200),
|
|
||||||
LAYER_0.withAlpha(0),
|
|
||||||
],
|
|
||||||
stops: [0.0, 0.8, 1.0],
|
|
||||||
).createShader(bounds);
|
|
||||||
},
|
|
||||||
blendMode: BlendMode.dstIn,
|
blendMode: BlendMode.dstIn,
|
||||||
|
shaderCallback:
|
||||||
|
(rect) => LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
LAYER_0.withAlpha(255),
|
||||||
|
LAYER_0.withAlpha(200),
|
||||||
|
LAYER_0.withAlpha(0),
|
||||||
|
],
|
||||||
|
stops: [0.0, 0.8, 1.0],
|
||||||
|
).createShader(rect),
|
||||||
child: ChatWidget(
|
child: ChatWidget(
|
||||||
stream: stream,
|
stream: stream,
|
||||||
showGoals: false,
|
showGoals: false,
|
||||||
|
@ -28,10 +28,44 @@ class PlayerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MainPlayer extends BaseAudioHandler {
|
class MainPlayer extends BaseAudioHandler {
|
||||||
|
String? _url;
|
||||||
VideoPlayerController? _controller;
|
VideoPlayerController? _controller;
|
||||||
ChewieController? _chewieController;
|
ChewieController? _chewieController;
|
||||||
ValueNotifier<PlayerState?> state = ValueNotifier(null);
|
ValueNotifier<PlayerState?> state = ValueNotifier(null);
|
||||||
|
|
||||||
|
MainPlayer() {
|
||||||
|
AppLifecycleListener(onStateChange: _onStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStateChanged(AppLifecycleState state) async {
|
||||||
|
developer.log(state.name);
|
||||||
|
switch (state) {
|
||||||
|
case AppLifecycleState.detached:
|
||||||
|
{
|
||||||
|
await dispose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
{
|
||||||
|
if (_controller == null && _url != null) {
|
||||||
|
await loadUrl(_url!);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await super.stop();
|
||||||
|
await _controller?.dispose();
|
||||||
|
_chewieController?.dispose();
|
||||||
|
_controller = null;
|
||||||
|
_chewieController = null;
|
||||||
|
state.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
ChewieController? get chewie {
|
ChewieController? get chewie {
|
||||||
return _chewieController;
|
return _chewieController;
|
||||||
}
|
}
|
||||||
@ -48,7 +82,7 @@ class MainPlayer extends BaseAudioHandler {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
await _chewieController?.pause();
|
await dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadUrl(
|
Future<void> loadUrl(
|
||||||
@ -92,6 +126,7 @@ class MainPlayer extends BaseAudioHandler {
|
|||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// insert media item
|
// insert media item
|
||||||
mediaItem.add(
|
mediaItem.add(
|
||||||
MediaItem(
|
MediaItem(
|
||||||
@ -105,6 +140,9 @@ class MainPlayer extends BaseAudioHandler {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
// Update player state immediately after initialization
|
||||||
|
updatePlayerState();
|
||||||
|
_url = url;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is PlatformException && e.code == "VideoError") {
|
if (e is PlatformException && e.code == "VideoError") {
|
||||||
state.value = PlayerState(
|
state.value = PlayerState(
|
||||||
@ -133,6 +171,7 @@ class MainPlayer extends BaseAudioHandler {
|
|||||||
MediaControl.stop,
|
MediaControl.stop,
|
||||||
],
|
],
|
||||||
playing: isPlaying,
|
playing: isPlaying,
|
||||||
|
androidCompactActionIndices: [1],
|
||||||
processingState: switch (_chewieController
|
processingState: switch (_chewieController
|
||||||
?.videoPlayerController
|
?.videoPlayerController
|
||||||
.value
|
.value
|
||||||
@ -143,10 +182,13 @@ class MainPlayer extends BaseAudioHandler {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
state.value = PlayerState(
|
|
||||||
width: _controller!.value.size.width.floor(),
|
if (_controller?.value.isInitialized == true && _controller!.value.size != Size.zero) {
|
||||||
height: _controller!.value.size.height.floor(),
|
state.value = PlayerState(
|
||||||
isPlaying: isPlaying,
|
width: _controller!.value.size.width.floor(),
|
||||||
);
|
height: _controller!.value.size.height.floor(),
|
||||||
|
isPlaying: isPlaying,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
name: zap_stream_flutter
|
name: zap_stream_flutter
|
||||||
description: "zap.stream"
|
description: "zap.stream"
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.1.1+15
|
version: 1.1.3+17
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
|
Reference in New Issue
Block a user