mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-17 04:18:50 +00:00
@ -1,11 +1,7 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:chewie/chewie.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
import 'package:video_player/video_player.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 +10,7 @@ import 'package:zap_stream_flutter/widgets/chat.dart';
|
|||||||
import 'package:zap_stream_flutter/widgets/pill.dart';
|
import 'package:zap_stream_flutter/widgets/pill.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/profile.dart';
|
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/stream_info.dart';
|
import 'package:zap_stream_flutter/widgets/stream_info.dart';
|
||||||
|
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 {
|
||||||
@ -26,49 +23,16 @@ class StreamPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _StreamPage extends State<StreamPage> {
|
class _StreamPage extends State<StreamPage> {
|
||||||
VideoPlayerController? _controller;
|
|
||||||
ChewieController? _chewieController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
final url = widget.stream.info.stream;
|
|
||||||
|
|
||||||
if (url != null) {
|
|
||||||
if (_controller != null) {
|
|
||||||
_controller!.dispose();
|
|
||||||
}
|
|
||||||
_controller = VideoPlayerController.networkUrl(
|
|
||||||
Uri.parse(url),
|
|
||||||
httpHeaders: Map.from({"user-agent": userAgent}),
|
|
||||||
);
|
|
||||||
() async {
|
|
||||||
await _controller!.initialize();
|
|
||||||
setState(() {
|
|
||||||
_chewieController = ChewieController(
|
|
||||||
videoPlayerController: _controller!,
|
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
autoPlay: true,
|
|
||||||
placeholder:
|
|
||||||
(widget.stream.info.image?.isNotEmpty ?? false)
|
|
||||||
? ProxyImg(url: widget.stream.info.image!)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
WakelockPlus.disable();
|
WakelockPlus.disable();
|
||||||
if (_controller != null) {
|
|
||||||
_controller!.dispose();
|
|
||||||
_controller = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -98,18 +62,14 @@ class _StreamPage extends State<StreamPage> {
|
|||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child:
|
child:
|
||||||
_chewieController != null
|
stream.info.stream != null
|
||||||
? Chewie(
|
? VideoPlayerWidget(
|
||||||
key: Key("stream:player:${stream.aTag}"),
|
url: stream.info.stream!,
|
||||||
controller: _chewieController!,
|
placeholder: stream.info.image,
|
||||||
)
|
)
|
||||||
: Container(
|
: (stream.info.image?.isNotEmpty ?? false)
|
||||||
color: LAYER_1,
|
? ProxyImg(url: stream.info.image)
|
||||||
child:
|
: Container(decoration: BoxDecoration(color: LAYER_1)),
|
||||||
(stream.info.image?.isNotEmpty ?? false)
|
|
||||||
? ProxyImg(url: stream.info.image!)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
stream.info.title ?? "",
|
stream.info.title ?? "",
|
||||||
|
102
lib/utils.dart
102
lib/utils.dart
@ -6,6 +6,7 @@ import 'package:convert/convert.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
|
import 'package:ndk/shared/nips/nip19/hrps.dart';
|
||||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||||
|
|
||||||
/// Container class over event and stream info
|
/// Container class over event and stream info
|
||||||
@ -394,6 +395,95 @@ String bech32ToHex(String bech32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/nostr-protocol/nips/blob/master/19.md
|
||||||
|
class TLVTypes {
|
||||||
|
static const int kSpecial = 0;
|
||||||
|
static const int kRelay = 1;
|
||||||
|
static const int kAuthor = 2;
|
||||||
|
static const int kKind = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TLVEntity {
|
||||||
|
final String hrp;
|
||||||
|
final List<TLV> data;
|
||||||
|
|
||||||
|
const TLVEntity(this.hrp, this.data);
|
||||||
|
|
||||||
|
TLV? get special {
|
||||||
|
return data.firstWhereOrNull((e) => e.type == TLVTypes.kSpecial);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return the special entry as hex
|
||||||
|
String? get specialHex {
|
||||||
|
final r = special;
|
||||||
|
return r != null ? hex.encode(r.value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return the special entry as utf8 string
|
||||||
|
String? get specialUtf8 {
|
||||||
|
final r = special;
|
||||||
|
return r != null ? utf8.decode(r.value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? get kind {
|
||||||
|
final k = data.firstWhereOrNull((e) => e.type == TLVTypes.kKind);
|
||||||
|
return k != null
|
||||||
|
? k.value[0] << 24 | k.value[1] << 16 | k.value[2] << 8 | k.value[3]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get author {
|
||||||
|
final a = data.firstWhereOrNull((e) => e.type == TLVTypes.kAuthor);
|
||||||
|
return a != null ? hex.encode(a.value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String>? get relays {
|
||||||
|
final r = data.where((r) => r.type == TLVTypes.kRelay);
|
||||||
|
if (r.isNotEmpty) {
|
||||||
|
return r.map((e) => utf8.decode(e.value)).toList();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Filter toFilter() {
|
||||||
|
var ret = <String, dynamic>{};
|
||||||
|
if (hrp == Hrps.kNaddr) {
|
||||||
|
final dTag = specialUtf8;
|
||||||
|
final kindValue = kind;
|
||||||
|
final authorValue = author;
|
||||||
|
if (dTag == null || kindValue == null || authorValue == null) {
|
||||||
|
throw "Invalid naddr entity, special, kind and author must be set";
|
||||||
|
}
|
||||||
|
ret["#d"] = [dTag];
|
||||||
|
ret["authors"] = [authorValue];
|
||||||
|
ret["kinds"] = [kindValue];
|
||||||
|
} else if (hrp == Hrps.kNevent) {
|
||||||
|
final idValue = specialHex;
|
||||||
|
if (idValue == null) {
|
||||||
|
throw "Invalid nevent, special entry is invalid or missing";
|
||||||
|
}
|
||||||
|
ret["ids"] = [idValue];
|
||||||
|
final kindValue = kind;
|
||||||
|
if (kindValue != null) {
|
||||||
|
ret["kinds"] = [kindValue];
|
||||||
|
}
|
||||||
|
final authorValue = author;
|
||||||
|
if (authorValue != null) {
|
||||||
|
ret["authors"] = [authorValue];
|
||||||
|
}
|
||||||
|
} else if (hrp == Hrps.kNoteId) {
|
||||||
|
final idValue = specialHex;
|
||||||
|
if (idValue == null) {
|
||||||
|
throw "Invalid nevent, special entry is invalid or missing";
|
||||||
|
}
|
||||||
|
ret["ids"] = [idValue];
|
||||||
|
} else {
|
||||||
|
throw "Cant convert $hrp to a filter";
|
||||||
|
}
|
||||||
|
return Filter.fromMap(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TLV {
|
class TLV {
|
||||||
final int type;
|
final int type;
|
||||||
final int length;
|
final int length;
|
||||||
@ -478,3 +568,15 @@ String encodeBech32TLV(String hrp, List<TLV> tlvs) {
|
|||||||
throw FormatException('Failed to encode Bech32 or TLV: $e');
|
throw FormatException('Failed to encode Bech32 or TLV: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TLVEntity decodeBech32ToTLVEntity(String input) {
|
||||||
|
final decoder = Bech32Decoder();
|
||||||
|
final data = decoder.convert(input, 10_000);
|
||||||
|
final data8bit = Nip19.convertBits(data.data, 5, 8, false);
|
||||||
|
if (data.hrp != "npub" || data.hrp != "nsec" || data.hrp != "note") {
|
||||||
|
return TLVEntity(data.hrp, parseTLV(data8bit));
|
||||||
|
} else {
|
||||||
|
// convert to basic type using special entry only
|
||||||
|
return TLVEntity(data.hrp, [TLV(0, data8bit.length, data8bit)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -38,7 +38,7 @@ class _ChatModalWidget extends State<ChatModalWidget> {
|
|||||||
width: double.maxFinite,
|
width: double.maxFinite,
|
||||||
decoration: BoxDecoration(color: LAYER_2, borderRadius: DEFAULT_BR),
|
decoration: BoxDecoration(color: LAYER_2, borderRadius: DEFAULT_BR),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
child: NoteText(event: widget.event),
|
child: NoteText(event: widget.event, showEmbeds: false),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
|
@ -1,24 +1,45 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
import 'package:ndk/shared/nips/nip19/nip19.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:zap_stream_flutter/imgproxy.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';
|
||||||
import 'package:zap_stream_flutter/widgets/custom_emoji.dart';
|
import 'package:zap_stream_flutter/widgets/custom_emoji.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/note_embed.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/profile.dart';
|
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/video_player.dart';
|
||||||
|
|
||||||
class NoteText extends StatelessWidget {
|
class NoteText extends StatelessWidget {
|
||||||
final Nip01Event event;
|
final Nip01Event event;
|
||||||
|
final bool? embedMedia;
|
||||||
|
final bool? showEmbeds;
|
||||||
|
|
||||||
const NoteText({super.key, required this.event});
|
const NoteText({
|
||||||
|
super.key,
|
||||||
|
required this.event,
|
||||||
|
this.embedMedia,
|
||||||
|
this.showEmbeds,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// use markdown rendering for articles
|
||||||
|
if (event.kind == 30_023) {
|
||||||
|
return MarkdownBody(data: event.content);
|
||||||
|
}
|
||||||
return RichText(
|
return RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
children: textToSpans(event.content, event.tags, event.pubKey),
|
children: textToSpans(
|
||||||
|
event.content,
|
||||||
|
event.tags,
|
||||||
|
event.pubKey,
|
||||||
|
showEmbeds: showEmbeds,
|
||||||
|
embedMedia: embedMedia,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -26,21 +47,18 @@ class NoteText extends StatelessWidget {
|
|||||||
|
|
||||||
/// Converts a nostr note text containing links
|
/// Converts a nostr note text containing links
|
||||||
/// and mentions into multiple spans for rendering
|
/// and mentions into multiple spans for rendering
|
||||||
|
/// /// https://github.com/leo-lox/camelus/blob/f58455a0ac07fcc780bdc69b8f4544fd5ea4a46d/lib/presentation_layer/components/note_card/note_card_build_split_content.dart#L262
|
||||||
List<InlineSpan> textToSpans(
|
List<InlineSpan> textToSpans(
|
||||||
String content,
|
String content,
|
||||||
List<List<String>> tags,
|
List<List<String>> tags,
|
||||||
String pubkey,
|
String pubkey, {
|
||||||
) {
|
bool? showEmbeds,
|
||||||
return _buildContentSpans(content.trim(), tags);
|
bool? embedMedia,
|
||||||
}
|
}) {
|
||||||
|
|
||||||
/// Content parser from camelus
|
|
||||||
/// https://github.com/leo-lox/camelus/blob/f58455a0ac07fcc780bdc69b8f4544fd5ea4a46d/lib/presentation_layer/components/note_card/note_card_build_split_content.dart#L262
|
|
||||||
List<InlineSpan> _buildContentSpans(String content, List<List<String>> tags) {
|
|
||||||
List<InlineSpan> spans = [];
|
List<InlineSpan> spans = [];
|
||||||
RegExp exp = RegExp(
|
RegExp exp = RegExp(
|
||||||
r'nostr:(nprofile|npub)[a-zA-Z0-9]+|'
|
r'nostr:(nprofile|npub)[a-zA-Z0-9]+|'
|
||||||
r'nostr:(note|nevent)[a-zA-Z0-9]+|'
|
r'nostr:(note|nevent|naddr)[a-zA-Z0-9]+|'
|
||||||
r'(#\$\$\s*[0-9]+\s*\$\$)|'
|
r'(#\$\$\s*[0-9]+\s*\$\$)|'
|
||||||
r'(#\w+)|' // Hashtags
|
r'(#\w+)|' // Hashtags
|
||||||
r'(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*))', // URLs
|
r'(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*))', // URLs
|
||||||
@ -53,11 +71,11 @@ List<InlineSpan> _buildContentSpans(String content, List<List<String>> tags) {
|
|||||||
String? matched = match.group(0);
|
String? matched = match.group(0);
|
||||||
if (matched != null) {
|
if (matched != null) {
|
||||||
if (matched.startsWith('nostr:')) {
|
if (matched.startsWith('nostr:')) {
|
||||||
spans.add(_buildProfileOrNoteSpan(matched));
|
spans.add(_buildProfileOrNoteSpan(matched, showEmbeds ?? true));
|
||||||
} else if (matched.startsWith('#')) {
|
} else if (matched.startsWith('#')) {
|
||||||
spans.add(_buildHashtagSpan(matched));
|
spans.add(_buildHashtagSpan(matched));
|
||||||
} else if (matched.startsWith('http')) {
|
} else if (matched.startsWith('http')) {
|
||||||
spans.add(_buildUrlSpan(matched));
|
spans.add(_buildUrlSpan(matched, embedMedia ?? false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
@ -87,12 +105,14 @@ List<InlineSpan> _buildContentSpans(String content, List<List<String>> tags) {
|
|||||||
return spans;
|
return spans;
|
||||||
}
|
}
|
||||||
|
|
||||||
InlineSpan _buildProfileOrNoteSpan(String word) {
|
InlineSpan _buildProfileOrNoteSpan(String word, bool showEmbeds) {
|
||||||
final cleanedWord = word.replaceAll('nostr:', '');
|
final cleanedWord = word.replaceAll('nostr:', '');
|
||||||
final isProfile =
|
final isProfile =
|
||||||
cleanedWord.startsWith('nprofile') || cleanedWord.startsWith('npub');
|
cleanedWord.startsWith('nprofile') || cleanedWord.startsWith('npub');
|
||||||
final isNote =
|
final isNote =
|
||||||
cleanedWord.startsWith('note') || cleanedWord.startsWith('nevent');
|
cleanedWord.startsWith('note') ||
|
||||||
|
cleanedWord.startsWith('nevent') ||
|
||||||
|
cleanedWord.startsWith("naddr");
|
||||||
|
|
||||||
if (isProfile) {
|
if (isProfile) {
|
||||||
final hexKey = bech32ToHex(cleanedWord);
|
final hexKey = bech32ToHex(cleanedWord);
|
||||||
@ -102,11 +122,13 @@ InlineSpan _buildProfileOrNoteSpan(String word) {
|
|||||||
return TextSpan(text: "@$cleanedWord");
|
return TextSpan(text: "@$cleanedWord");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isNote) {
|
if (isNote && showEmbeds) {
|
||||||
final eventId = bech32ToHex(cleanedWord);
|
return WidgetSpan(
|
||||||
return TextSpan(text: eventId, style: TextStyle(color: PRIMARY_1));
|
child: NoteEmbedWidget(link: cleanedWord),
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return TextSpan(text: word);
|
return TextSpan(text: word, style: TextStyle(color: PRIMARY_1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +136,29 @@ InlineSpan _buildHashtagSpan(String word) {
|
|||||||
return TextSpan(text: word, style: TextStyle(color: PRIMARY_1));
|
return TextSpan(text: word, style: TextStyle(color: PRIMARY_1));
|
||||||
}
|
}
|
||||||
|
|
||||||
InlineSpan _buildUrlSpan(String url) {
|
InlineSpan _buildUrlSpan(String url, bool embedMedia) {
|
||||||
|
if (embedMedia &&
|
||||||
|
(url.endsWith(".jpg") ||
|
||||||
|
url.endsWith(".gif") ||
|
||||||
|
url.endsWith(".jpeg") ||
|
||||||
|
url.endsWith(".webp") ||
|
||||||
|
url.endsWith(".png") ||
|
||||||
|
url.endsWith(".bmp"))) {
|
||||||
|
return WidgetSpan(child: ProxyImg(url: url));
|
||||||
|
}
|
||||||
|
if (embedMedia &&
|
||||||
|
(url.endsWith(".mp4") ||
|
||||||
|
url.endsWith(".mov") ||
|
||||||
|
url.endsWith(".webm") ||
|
||||||
|
url.endsWith(".mkv") ||
|
||||||
|
url.endsWith(".m3u8"))) {
|
||||||
|
return WidgetSpan(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: Center(child: VideoPlayerWidget(url: url, autoPlay: false)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return TextSpan(
|
return TextSpan(
|
||||||
text: url,
|
text: url,
|
||||||
style: TextStyle(color: PRIMARY_1),
|
style: TextStyle(color: PRIMARY_1),
|
||||||
|
89
lib/widgets/note_embed.dart
Normal file
89
lib/widgets/note_embed.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:ndk/ndk.dart';
|
||||||
|
import 'package:zap_stream_flutter/rx_filter.dart';
|
||||||
|
import 'package:zap_stream_flutter/theme.dart';
|
||||||
|
import 'package:zap_stream_flutter/utils.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/nostr_text.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/pill.dart';
|
||||||
|
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||||
|
|
||||||
|
class NoteEmbedWidget extends StatelessWidget {
|
||||||
|
final String link;
|
||||||
|
|
||||||
|
const NoteEmbedWidget({super.key, required this.link});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final entity = decodeBech32ToTLVEntity(link);
|
||||||
|
|
||||||
|
return RxFilter<Nip01Event>(
|
||||||
|
Key("embeded-note:$link"),
|
||||||
|
filters: [entity.toFilter()],
|
||||||
|
builder: (context, data) {
|
||||||
|
final note = data != null && data.isNotEmpty ? data.first : null;
|
||||||
|
return PillWidget(
|
||||||
|
onTap: () {
|
||||||
|
if (note != null) {
|
||||||
|
// redirect to the stream if its a live stream link
|
||||||
|
if (note.kind == 30_311) {
|
||||||
|
context.push("/e/$link", extra: StreamEvent(note));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return SingleChildScrollView(child: _NotePreview(note: note));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
color: LAYER_3,
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(child: Icon(Icons.link, size: 16)),
|
||||||
|
TextSpan(
|
||||||
|
text: switch (entity.kind) {
|
||||||
|
30_023 => " Article by ",
|
||||||
|
30_311 => " Live Stream by ",
|
||||||
|
_ => " Note by ",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (note?.pubKey != null)
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: ProfileNameWidget.pubkey(switch (note!.kind) {
|
||||||
|
30_311 => StreamEvent(note).info.host,
|
||||||
|
_ => note.pubKey,
|
||||||
|
}, linkToProfile: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotePreview extends StatelessWidget {
|
||||||
|
final Nip01Event note;
|
||||||
|
|
||||||
|
const _NotePreview({required this.note});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(color: LAYER_1, borderRadius: DEFAULT_BR),
|
||||||
|
child: Column(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
ProfileWidget.pubkey(note.pubKey),
|
||||||
|
NoteText(event: note, embedMedia: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -24,15 +24,27 @@ class ProfileLoaderWidget extends StatelessWidget {
|
|||||||
class ProfileNameWidget extends StatelessWidget {
|
class ProfileNameWidget extends StatelessWidget {
|
||||||
final Metadata profile;
|
final Metadata profile;
|
||||||
final TextStyle? style;
|
final TextStyle? style;
|
||||||
|
final bool? linkToProfile;
|
||||||
|
|
||||||
const ProfileNameWidget({super.key, required this.profile, this.style});
|
const ProfileNameWidget({
|
||||||
|
super.key,
|
||||||
|
required this.profile,
|
||||||
|
this.style,
|
||||||
|
this.linkToProfile,
|
||||||
|
});
|
||||||
|
|
||||||
static Widget pubkey(String pubkey, {Key? key, TextStyle? style}) {
|
static Widget pubkey(
|
||||||
|
String pubkey, {
|
||||||
|
Key? key,
|
||||||
|
TextStyle? style,
|
||||||
|
bool? linkToProfile,
|
||||||
|
}) {
|
||||||
return ProfileLoaderWidget(
|
return ProfileLoaderWidget(
|
||||||
pubkey,
|
pubkey,
|
||||||
(ctx, data) => ProfileNameWidget(
|
(ctx, data) => ProfileNameWidget(
|
||||||
profile: data.data ?? Metadata(pubKey: pubkey),
|
profile: data.data ?? Metadata(pubKey: pubkey),
|
||||||
style: style,
|
style: style,
|
||||||
|
linkToProfile: linkToProfile,
|
||||||
),
|
),
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
@ -50,14 +62,22 @@ class ProfileNameWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final inner = Text(
|
||||||
|
ProfileNameWidget.nameFromProfile(profile),
|
||||||
|
style: style,
|
||||||
|
);
|
||||||
|
if (linkToProfile ?? true) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap:
|
onTap:
|
||||||
() => context.push(
|
() => context.push(
|
||||||
"/p/${Nip19.encodePubKey(profile.pubKey)}",
|
"/p/${Nip19.encodePubKey(profile.pubKey)}",
|
||||||
extra: profile,
|
extra: profile,
|
||||||
),
|
),
|
||||||
child: Text(ProfileNameWidget.nameFromProfile(profile), style: style),
|
child: inner,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
57
lib/widgets/video_player.dart
Normal file
57
lib/widgets/video_player.dart
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import 'package:chewie/chewie.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
import 'package:zap_stream_flutter/imgproxy.dart';
|
||||||
|
import 'package:zap_stream_flutter/main.dart';
|
||||||
|
|
||||||
|
class VideoPlayerWidget extends StatefulWidget {
|
||||||
|
final String url;
|
||||||
|
final String? placeholder;
|
||||||
|
final double? aspectRatio;
|
||||||
|
final bool? autoPlay;
|
||||||
|
|
||||||
|
const VideoPlayerWidget({
|
||||||
|
super.key,
|
||||||
|
required this.url,
|
||||||
|
this.placeholder,
|
||||||
|
this.aspectRatio,
|
||||||
|
this.autoPlay,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _VideoPlayerWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoPlayerWidget extends State<VideoPlayerWidget> {
|
||||||
|
late final ChewieController _chewieController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
final controller = VideoPlayerController.networkUrl(
|
||||||
|
Uri.parse(widget.url),
|
||||||
|
httpHeaders: Map.from({"user-agent": userAgent}),
|
||||||
|
);
|
||||||
|
_chewieController = ChewieController(
|
||||||
|
videoPlayerController: controller,
|
||||||
|
autoPlay: widget.autoPlay ?? true,
|
||||||
|
autoInitialize: true,
|
||||||
|
placeholder:
|
||||||
|
(widget.placeholder?.isNotEmpty ?? false)
|
||||||
|
? ProxyImg(url: widget.placeholder!)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_chewieController.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Chewie(controller: _chewieController);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user