mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-14 19:36:33 +00:00
feat: protrait video style
This commit is contained in:
@ -4,9 +4,9 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 22
|
/// Locales: 22
|
||||||
/// Strings: 2010 (91 per locale)
|
/// Strings: 2011 (91 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2025-05-30 at 13:17 UTC
|
/// Built on 2025-06-03 at 10:11 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
@ -86,6 +86,7 @@ class TranslationsStreamEn {
|
|||||||
String started({required Object timestamp}) => 'Started ${timestamp}';
|
String started({required Object timestamp}) => 'Started ${timestamp}';
|
||||||
String notification({required Object name}) => '${name} went live!';
|
String notification({required Object name}) => '${name} went live!';
|
||||||
late final TranslationsStreamChatEn chat = TranslationsStreamChatEn.internal(_root);
|
late final TranslationsStreamChatEn chat = TranslationsStreamChatEn.internal(_root);
|
||||||
|
late final TranslationsStreamErrorEn error = TranslationsStreamErrorEn.internal(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: goal
|
// Path: goal
|
||||||
@ -280,6 +281,16 @@ class TranslationsStreamChatEn {
|
|||||||
late final TranslationsStreamChatRaidEn raid = TranslationsStreamChatRaidEn.internal(_root);
|
late final TranslationsStreamChatRaidEn raid = TranslationsStreamChatRaidEn.internal(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: stream.error
|
||||||
|
class TranslationsStreamErrorEn {
|
||||||
|
TranslationsStreamErrorEn.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
String load_failed({required Object url}) => 'Failed to load stream from ${url}';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: zap.error
|
// Path: zap.error
|
||||||
class TranslationsZapErrorEn {
|
class TranslationsZapErrorEn {
|
||||||
TranslationsZapErrorEn.internal(this._root);
|
TranslationsZapErrorEn.internal(this._root);
|
||||||
@ -455,6 +466,7 @@ extension on Translations {
|
|||||||
case 'stream.chat.raid.to': return ({required Object name}) => 'RAIDING ${name}';
|
case 'stream.chat.raid.to': return ({required Object name}) => 'RAIDING ${name}';
|
||||||
case 'stream.chat.raid.from': return ({required Object name}) => 'RAID FROM ${name}';
|
case 'stream.chat.raid.from': return ({required Object name}) => 'RAID FROM ${name}';
|
||||||
case 'stream.chat.raid.countdown': return ({required Object time}) => 'Raiding in ${time}';
|
case 'stream.chat.raid.countdown': return ({required Object time}) => 'Raiding in ${time}';
|
||||||
|
case 'stream.error.load_failed': return ({required Object url}) => 'Failed to load stream from ${url}';
|
||||||
case 'goal.title': return ({required Object amount}) => 'Goal: ${amount}';
|
case 'goal.title': return ({required Object amount}) => 'Goal: ${amount}';
|
||||||
case 'goal.remaining': return ({required Object amount}) => 'Remaining: ${amount}';
|
case 'goal.remaining': return ({required Object amount}) => 'Remaining: ${amount}';
|
||||||
case 'goal.complete': return 'COMPLETE';
|
case 'goal.complete': return 'COMPLETE';
|
||||||
|
@ -59,6 +59,8 @@ stream:
|
|||||||
countdown: Raiding in ${time}
|
countdown: Raiding in ${time}
|
||||||
"@countdown":
|
"@countdown":
|
||||||
description: Countdown timer for auto-raiding
|
description: Countdown timer for auto-raiding
|
||||||
|
error:
|
||||||
|
load_failed: Failed to load stream from ${url}
|
||||||
goal:
|
goal:
|
||||||
title: "Goal: $amount"
|
title: "Goal: $amount"
|
||||||
remaining: "Remaining: $amount"
|
remaining: "Remaining: $amount"
|
||||||
|
@ -8,6 +8,7 @@ import 'package:wakelock_plus/wakelock_plus.dart';
|
|||||||
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
import 'package:zap_stream_flutter/i18n/strings.g.dart';
|
||||||
import 'package:zap_stream_flutter/imgproxy.dart';
|
import 'package:zap_stream_flutter/imgproxy.dart';
|
||||||
import 'package:zap_stream_flutter/const.dart';
|
import 'package:zap_stream_flutter/const.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';
|
||||||
@ -56,6 +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;
|
||||||
|
|
||||||
bool isWidgetVisible(BuildContext context) {
|
bool isWidgetVisible(BuildContext context) {
|
||||||
final router = GoRouter.of(context);
|
final router = GoRouter.of(context);
|
||||||
@ -127,97 +129,184 @@ class _StreamPage extends State<StreamPage> with RouteAware {
|
|||||||
],
|
],
|
||||||
builder: (ctx, state) {
|
builder: (ctx, state) {
|
||||||
final stream = StreamEvent(state?.firstOrNull ?? widget.stream.event);
|
final stream = StreamEvent(state?.firstOrNull ?? widget.stream.event);
|
||||||
return _buildStream(context, stream);
|
final streamWidget = _buildPlayer(ctx, stream);
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: mainPlayer.state,
|
||||||
|
builder: (context, state, player) {
|
||||||
|
if (state?.isPortrait == true) {
|
||||||
|
return _buildPortraitStream(context, stream, player!);
|
||||||
|
}
|
||||||
|
return _buildLandscapeStream(context, stream, player!);
|
||||||
|
},
|
||||||
|
child: streamWidget,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStream(BuildContext context, StreamEvent stream) {
|
Widget _buildPlayer(BuildContext context, StreamEvent stream) {
|
||||||
|
return (stream.info.stream != null && !_offScreen)
|
||||||
|
? MainVideoPlayerWidget(
|
||||||
|
url: stream.info.stream!,
|
||||||
|
placeholder: stream.info.image,
|
||||||
|
isLive: true,
|
||||||
|
title: stream.info.title,
|
||||||
|
)
|
||||||
|
: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child:
|
||||||
|
(stream.info.image?.isNotEmpty ?? false)
|
||||||
|
? ProxyImg(url: stream.info.image)
|
||||||
|
: Container(decoration: BoxDecoration(color: LAYER_1)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPortraitStream(
|
||||||
|
BuildContext context,
|
||||||
|
StreamEvent stream,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
|
final mq = MediaQuery.of(context);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
Positioned(child: child),
|
||||||
|
Positioned(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
LAYER_0.withAlpha(50),
|
||||||
|
LAYER_0.withAlpha(200),
|
||||||
|
LAYER_0.withAlpha(255),
|
||||||
|
],
|
||||||
|
stops: [0.0, 0.2, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: _streamInfo(context, stream),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
left: 0,
|
||||||
|
child: Container(
|
||||||
|
width: mq.size.width,
|
||||||
|
height: mq.size.height * 0.4,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
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,
|
||||||
|
child: ChatWidget(
|
||||||
|
stream: stream,
|
||||||
|
showGoals: false,
|
||||||
|
showTopZappers: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLandscapeStream(
|
||||||
|
BuildContext context,
|
||||||
|
StreamEvent stream,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
return Column(
|
return Column(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AspectRatio(
|
child,
|
||||||
aspectRatio: 16 / 9,
|
..._streamInfo(context, stream),
|
||||||
child:
|
|
||||||
(stream.info.stream != null && !_offScreen)
|
|
||||||
? MainVideoPlayerWidget(
|
|
||||||
url: stream.info.stream!,
|
|
||||||
placeholder: stream.info.image,
|
|
||||||
isLive: true,
|
|
||||||
title: stream.info.title,
|
|
||||||
)
|
|
||||||
: (stream.info.image?.isNotEmpty ?? false)
|
|
||||||
? ProxyImg(url: stream.info.image)
|
|
||||||
: Container(decoration: BoxDecoration(color: LAYER_1)),
|
|
||||||
),
|
|
||||||
if (stream.info.title?.isNotEmpty ?? false)
|
|
||||||
Text(
|
|
||||||
stream.info.title!,
|
|
||||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
|
||||||
),
|
|
||||||
ProfileWidget.pubkey(
|
|
||||||
stream.info.host,
|
|
||||||
children: [
|
|
||||||
NotificationsButtonWidget(pubkey: widget.stream.info.host),
|
|
||||||
BasicButton(
|
|
||||||
Row(
|
|
||||||
children: [Icon(Icons.bolt, size: 14), Text(t.zap.button_zap)],
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: PRIMARY_1,
|
|
||||||
borderRadius: DEFAULT_BR,
|
|
||||||
),
|
|
||||||
onTap: (context) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
constraints: BoxConstraints.expand(),
|
|
||||||
builder: (context) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
primary: false,
|
|
||||||
child: ZapWidget(
|
|
||||||
pubkey: stream.info.host,
|
|
||||||
target: stream.event,
|
|
||||||
onPaid: (_) {
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
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(
|
|
||||||
t.viewers(n: stream.info.participants!),
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(child: ChatWidget(stream: stream)),
|
Expanded(child: ChatWidget(stream: stream)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Widget> _streamInfo(BuildContext context, StreamEvent stream) {
|
||||||
|
return [
|
||||||
|
if (stream.info.title?.isNotEmpty ?? false)
|
||||||
|
Text(
|
||||||
|
stream.info.title!,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
||||||
|
),
|
||||||
|
ProfileWidget.pubkey(
|
||||||
|
stream.info.host,
|
||||||
|
children: [
|
||||||
|
NotificationsButtonWidget(pubkey: widget.stream.info.host),
|
||||||
|
BasicButton(
|
||||||
|
Row(children: [Icon(Icons.bolt, size: 14), Text(t.zap.button_zap)]),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: PRIMARY_1,
|
||||||
|
borderRadius: DEFAULT_BR,
|
||||||
|
),
|
||||||
|
onTap: (context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
constraints: BoxConstraints.expand(),
|
||||||
|
builder: (context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
primary: false,
|
||||||
|
child: ZapWidget(
|
||||||
|
pubkey: stream.info.host,
|
||||||
|
target: stream.event,
|
||||||
|
onPaid: (_) {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
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(
|
||||||
|
t.viewers(n: stream.info.participants!),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
121
lib/player.dart
121
lib/player.dart
@ -1,12 +1,36 @@
|
|||||||
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.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/imgproxy.dart';
|
import 'package:zap_stream_flutter/imgproxy.dart';
|
||||||
|
|
||||||
|
class PlayerState {
|
||||||
|
final int? width;
|
||||||
|
final int? height;
|
||||||
|
final bool isPlaying;
|
||||||
|
final Exception? error;
|
||||||
|
|
||||||
|
bool get isPortrait {
|
||||||
|
return width != null && height != null ? width! / height! < 1.0 : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlayerState({
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.isPlaying = false,
|
||||||
|
this.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class MainPlayer extends BaseAudioHandler {
|
class MainPlayer extends BaseAudioHandler {
|
||||||
VideoPlayerController? _controller;
|
VideoPlayerController? _controller;
|
||||||
ChewieController? _chewieController;
|
ChewieController? _chewieController;
|
||||||
|
ValueNotifier<PlayerState?> state = ValueNotifier(null);
|
||||||
|
|
||||||
ChewieController? get chewie {
|
ChewieController? get chewie {
|
||||||
return _chewieController;
|
return _chewieController;
|
||||||
@ -27,7 +51,7 @@ class MainPlayer extends BaseAudioHandler {
|
|||||||
await _chewieController?.pause();
|
await _chewieController?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadUrl(
|
Future<void> loadUrl(
|
||||||
String url, {
|
String url, {
|
||||||
String? title,
|
String? title,
|
||||||
bool? autoPlay,
|
bool? autoPlay,
|
||||||
@ -35,42 +59,64 @@ class MainPlayer extends BaseAudioHandler {
|
|||||||
bool? isLive,
|
bool? isLive,
|
||||||
String? placeholder,
|
String? placeholder,
|
||||||
String? artist,
|
String? artist,
|
||||||
}) {
|
}) async {
|
||||||
if (_chewieController != null) {
|
if (_controller?.dataSource == url) {
|
||||||
_chewieController!.dispose();
|
return;
|
||||||
_controller!.dispose();
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
developer.log("PLAYER loading $url");
|
||||||
|
if (_chewieController != null) {
|
||||||
|
_controller!.removeListener(updatePlayerState);
|
||||||
|
await _controller!.dispose();
|
||||||
|
_controller = null;
|
||||||
|
_chewieController!.dispose();
|
||||||
|
_chewieController = null;
|
||||||
|
}
|
||||||
|
state.value = null;
|
||||||
|
_controller = VideoPlayerController.networkUrl(
|
||||||
|
Uri.parse(url),
|
||||||
|
httpHeaders: Map.from({"user-agent": userAgent}),
|
||||||
|
videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true),
|
||||||
|
);
|
||||||
|
await _controller!.initialize();
|
||||||
|
_controller!.addListener(updatePlayerState);
|
||||||
|
_chewieController = ChewieController(
|
||||||
|
videoPlayerController: _controller!,
|
||||||
|
autoPlay: autoPlay ?? true,
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
|
isLive: isLive ?? false,
|
||||||
|
allowedScreenSleep: false,
|
||||||
|
placeholder:
|
||||||
|
(placeholder?.isNotEmpty ?? false)
|
||||||
|
? ProxyImg(url: placeholder!)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
_controller = VideoPlayerController.networkUrl(
|
// insert media item
|
||||||
Uri.parse(url),
|
mediaItem.add(
|
||||||
httpHeaders: Map.from({"user-agent": userAgent}),
|
MediaItem(
|
||||||
videoPlayerOptions: VideoPlayerOptions(allowBackgroundPlayback: true),
|
id: url.hashCode.toString(),
|
||||||
);
|
title: title ?? url,
|
||||||
_chewieController = ChewieController(
|
artist: artist,
|
||||||
videoPlayerController: _controller!,
|
isLive: _chewieController!.isLive,
|
||||||
autoPlay: autoPlay ?? true,
|
artUri:
|
||||||
aspectRatio: aspectRatio,
|
(placeholder?.isNotEmpty ?? false)
|
||||||
isLive: isLive ?? false,
|
? Uri.parse(placeholder!)
|
||||||
autoInitialize: true,
|
: null,
|
||||||
allowedScreenSleep: false,
|
),
|
||||||
placeholder:
|
);
|
||||||
(placeholder?.isNotEmpty ?? false)
|
} catch (e) {
|
||||||
? ProxyImg(url: placeholder!)
|
if (e is PlatformException && e.code == "VideoError") {
|
||||||
: null,
|
state.value = PlayerState(
|
||||||
);
|
error: Exception(t.stream.error.load_failed(url: url)),
|
||||||
|
);
|
||||||
// insert media item
|
} else {
|
||||||
mediaItem.add(
|
state.value = PlayerState(
|
||||||
MediaItem(
|
error: e is Exception ? e : Exception(e.toString()),
|
||||||
id: url.hashCode.toString(),
|
);
|
||||||
title: title ?? url,
|
}
|
||||||
artist: artist,
|
developer.log("Failed to start player: ${e.toString()}");
|
||||||
isLive: _chewieController!.isLive,
|
}
|
||||||
artUri:
|
|
||||||
(placeholder?.isNotEmpty ?? false) ? Uri.parse(placeholder!) : null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_chewieController!.videoPlayerController.addListener(updatePlayerState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updatePlayerState() {
|
void updatePlayerState() {
|
||||||
@ -97,5 +143,10 @@ class MainPlayer extends BaseAudioHandler {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
state.value = PlayerState(
|
||||||
|
width: _controller!.value.size.width.floor(),
|
||||||
|
height: _controller!.value.size.height.floor(),
|
||||||
|
isPlaying: isPlaying,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,16 +18,16 @@ import 'package:zap_stream_flutter/widgets/profile.dart';
|
|||||||
|
|
||||||
class ChatWidget extends StatelessWidget {
|
class ChatWidget extends StatelessWidget {
|
||||||
final StreamEvent stream;
|
final StreamEvent stream;
|
||||||
final bool? showGoals;
|
final bool showGoals;
|
||||||
final bool? showTopZappers;
|
final bool showTopZappers;
|
||||||
final bool? showRaids;
|
final bool showRaids;
|
||||||
|
|
||||||
const ChatWidget({
|
const ChatWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.stream,
|
required this.stream,
|
||||||
this.showGoals,
|
this.showGoals = true,
|
||||||
this.showTopZappers,
|
this.showTopZappers = true,
|
||||||
this.showRaids,
|
this.showRaids = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -40,7 +40,7 @@ class ChatWidget extends StatelessWidget {
|
|||||||
|
|
||||||
var filters = [
|
var filters = [
|
||||||
Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]),
|
Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]),
|
||||||
if (showRaids ?? true)
|
if (showRaids)
|
||||||
Filter(kinds: [1312, 1313], limit: 200, aTags: [stream.aTag]),
|
Filter(kinds: [1312, 1313], limit: 200, aTags: [stream.aTag]),
|
||||||
Filter(kinds: [Nip51List.kMute], authors: moderators),
|
Filter(kinds: [Nip51List.kMute], authors: moderators),
|
||||||
Filter(kinds: [1314], authors: moderators),
|
Filter(kinds: [1314], authors: moderators),
|
||||||
@ -118,9 +118,9 @@ class ChatWidget extends StatelessWidget {
|
|||||||
spacing: 8,
|
spacing: 8,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (zaps.isNotEmpty && (showTopZappers ?? true))
|
if (zaps.isNotEmpty && showTopZappers)
|
||||||
_TopZappersWidget(events: zaps),
|
_TopZappersWidget(events: zaps),
|
||||||
if (stream.info.goal != null && (showGoals ?? true))
|
if (stream.info.goal != null && showGoals)
|
||||||
GoalWidget.id(stream.info.goal!),
|
GoalWidget.id(stream.info.goal!),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:zap_stream_flutter/main.dart';
|
import 'package:zap_stream_flutter/main.dart';
|
||||||
|
import 'package:zap_stream_flutter/theme.dart';
|
||||||
|
|
||||||
class MainVideoPlayerWidget extends StatefulWidget {
|
class MainVideoPlayerWidget extends StatefulWidget {
|
||||||
final String url;
|
final String url;
|
||||||
@ -34,7 +35,7 @@ class _MainVideoPlayerWidget extends State<MainVideoPlayerWidget> {
|
|||||||
aspectRatio: widget.aspectRatio,
|
aspectRatio: widget.aspectRatio,
|
||||||
autoPlay: widget.autoPlay,
|
autoPlay: widget.autoPlay,
|
||||||
isLive: widget.isLive,
|
isLive: widget.isLive,
|
||||||
artist: "zap.stream"
|
artist: "zap.stream",
|
||||||
);
|
);
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -48,6 +49,26 @@ class _MainVideoPlayerWidget extends State<MainVideoPlayerWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Chewie(controller: mainPlayer.chewie!);
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: mainPlayer.state,
|
||||||
|
builder: (context, state, _) {
|
||||||
|
final innerWidget =
|
||||||
|
mainPlayer.chewie != null
|
||||||
|
? Chewie(controller: mainPlayer.chewie!)
|
||||||
|
: Center(
|
||||||
|
child:
|
||||||
|
state?.error != null
|
||||||
|
? Text(
|
||||||
|
state!.error.toString(),
|
||||||
|
style: TextStyle(color: WARNING),
|
||||||
|
)
|
||||||
|
: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
if (state?.isPortrait == true) {
|
||||||
|
return innerWidget;
|
||||||
|
}
|
||||||
|
return AspectRatio(aspectRatio: 16 / 9, child: innerWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user