feat: preview media in modal

closes #38
This commit is contained in:
2025-05-28 12:17:44 +01:00
parent 2ec17c6c41
commit c6168ec094
7 changed files with 77 additions and 19 deletions

View File

@ -58,6 +58,7 @@ class ProfilePage extends StatelessWidget {
TextSpan(
style: TextStyle(color: LAYER_5),
children: textToSpans(
context,
profile.about ?? "",
[],
profile.pubKey,

View File

@ -145,6 +145,7 @@ class _StreamPage extends State<StreamPage> with RouteAware {
url: stream.info.stream!,
placeholder: stream.info.image,
aspectRatio: 16 / 9,
isLive: true,
)
: (stream.info.image?.isNotEmpty ?? false)
? ProxyImg(url: stream.info.image)

View File

@ -46,7 +46,7 @@ class ChatMessageWidget extends StatelessWidget {
spacing: 2,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_chatText(profile),
_chatText(context, profile),
/*RxFilter<Nip01Event>(
Key(msg.id),
filters: [
@ -63,7 +63,7 @@ class ChatMessageWidget extends StatelessWidget {
});
}
Widget _chatText(Metadata profile) {
Widget _chatText(BuildContext context, Metadata profile) {
return RichText(
text: TextSpan(
children: [
@ -92,7 +92,13 @@ class ChatMessageWidget extends StatelessWidget {
),
),
TextSpan(text: " "),
...textToSpans(msg.content, msg.tags, msg.pubKey),
...textToSpans(
context,
msg.content,
msg.tags,
msg.pubKey,
embedMedia: false,
),
],
),
);

View File

@ -17,12 +17,14 @@ class NoteText extends StatelessWidget {
final Nip01Event event;
final bool? embedMedia;
final bool? showEmbeds;
final bool? previewMedia;
const NoteText({
super.key,
required this.event,
this.embedMedia,
this.showEmbeds,
this.previewMedia,
});
@override
@ -34,11 +36,13 @@ class NoteText extends StatelessWidget {
return RichText(
text: TextSpan(
children: textToSpans(
context,
event.content,
event.tags,
event.pubKey,
showEmbeds: showEmbeds,
embedMedia: embedMedia,
previewMedia: previewMedia,
),
),
);
@ -49,11 +53,13 @@ class NoteText extends StatelessWidget {
/// 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(
BuildContext context,
String content,
List<List<String>> tags,
String pubkey, {
bool? showEmbeds,
bool? embedMedia,
bool? previewMedia,
}) {
List<InlineSpan> spans = [];
RegExp exp = RegExp(
@ -76,7 +82,14 @@ List<InlineSpan> textToSpans(
} else if (matched.startsWith('#')) {
spans.add(_buildHashtagSpan(matched));
} else if (matched.startsWith('http')) {
spans.add(_buildUrlSpan(matched, embedMedia ?? false));
spans.add(
_buildUrlSpan(
context,
matched,
embedMedia ?? false,
previewMedia ?? true,
),
);
} else if (matched.startsWith(":") &&
matched.endsWith(":") &&
tags.any(
@ -134,22 +147,29 @@ InlineSpan _buildHashtagSpan(String word) {
return TextSpan(text: word, style: TextStyle(color: PRIMARY_1));
}
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"))) {
InlineSpan _buildUrlSpan(
BuildContext context,
String url,
bool embedMedia,
bool previewMedia,
) {
final isImage =
url.endsWith(".jpg") ||
url.endsWith(".gif") ||
url.endsWith(".jpeg") ||
url.endsWith(".webp") ||
url.endsWith(".png") ||
url.endsWith(".bmp");
final isVideo =
url.endsWith(".mp4") ||
url.endsWith(".mov") ||
url.endsWith(".webm") ||
url.endsWith(".mkv") ||
url.endsWith(".m3u8");
if (embedMedia && isImage) {
return WidgetSpan(child: ProxyImg(url: url));
}
if (embedMedia &&
(url.endsWith(".mp4") ||
url.endsWith(".mov") ||
url.endsWith(".webm") ||
url.endsWith(".mkv") ||
url.endsWith(".m3u8"))) {
if (embedMedia && isVideo) {
return WidgetSpan(
child: AspectRatio(
aspectRatio: 16 / 9,
@ -163,7 +183,32 @@ InlineSpan _buildUrlSpan(String url, bool embedMedia) {
recognizer:
TapGestureRecognizer()
..onTap = () {
launchUrl(Uri.parse(url));
if (previewMedia) {
showModalBottomSheet(
context: context,
builder: (context) {
return Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: LAYER_1,
borderRadius: DEFAULT_BR,
),
child:
isImage
? ProxyImg(url: url)
: AspectRatio(
aspectRatio: 16 / 9,
child: VideoPlayerWidget(
url: url,
autoPlay: false,
),
),
);
},
);
} else {
launchUrl(Uri.parse(url));
}
},
);
}

View File

@ -45,6 +45,7 @@ class NoteEmbedWidget extends StatelessWidget {
},
color: LAYER_3,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.link, size: 16),
ProfileLoaderWidget(author, (context, state) {

View File

@ -79,6 +79,7 @@ class StreamInfoWidget extends StatelessWidget {
Text.rich(
TextSpan(
children: textToSpans(
context,
stream.info.summary!,
[],
stream.info.host,

View File

@ -9,6 +9,7 @@ class VideoPlayerWidget extends StatefulWidget {
final String? placeholder;
final double? aspectRatio;
final bool? autoPlay;
final bool? isLive;
const VideoPlayerWidget({
super.key,
@ -16,6 +17,7 @@ class VideoPlayerWidget extends StatefulWidget {
this.placeholder,
this.aspectRatio,
this.autoPlay,
this.isLive,
});
@override
@ -38,6 +40,7 @@ class _VideoPlayerWidget extends State<VideoPlayerWidget> {
videoPlayerController: _controller,
autoPlay: widget.autoPlay ?? true,
aspectRatio: widget.aspectRatio,
isLive: widget.isLive ?? false,
autoInitialize: true,
placeholder:
(widget.placeholder?.isNotEmpty ?? false)