Skip to content

Commit 0f4d746

Browse files
content: Implement embed video preview
Partially implements #356, provides video thumbnail previews for embedded external videos (Youtube & Vimeo).
1 parent 5bf37c2 commit 0f4d746

File tree

2 files changed

+78
-7
lines changed

2 files changed

+78
-7
lines changed

lib/widgets/content.dart

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,7 @@ class BlockContentList extends StatelessWidget {
107107
]),
108108
style: errorStyle);
109109
} else if (node is EmbedVideoNode) {
110-
return Text.rich(
111-
TextSpan(children: [
112-
const TextSpan(text: "(unimplemented:", style: errorStyle),
113-
TextSpan(text: node.debugHtmlText, style: errorCodeStyle),
114-
const TextSpan(text: ")", style: errorStyle),
115-
]),
116-
style: errorStyle);
110+
return MessageEmbedVideo(node: node);
117111
} else if (node is UnimplementedBlockContentNode) {
118112
return Text.rich(_errorUnimplemented(node));
119113
} else {
@@ -404,6 +398,35 @@ class MessageImage extends StatelessWidget {
404398
}
405399
}
406400

401+
class MessageEmbedVideo extends StatelessWidget {
402+
const MessageEmbedVideo({super.key, required this.node});
403+
404+
final EmbedVideoNode node;
405+
406+
@override
407+
Widget build(BuildContext context) {
408+
final store = PerAccountStoreWidget.of(context);
409+
final previewImageSrcUrl = store.tryResolveUrl(node.previewImageSrcUrl);
410+
411+
return MessageMediaContainer(
412+
onTap: () => _launchUrl(context, node.hrefUrl),
413+
child: Stack(
414+
alignment: Alignment.center,
415+
children: [
416+
if (previewImageSrcUrl != null) // TODO(log)
417+
RealmContentNetworkImage(
418+
previewImageSrcUrl,
419+
filterQuality: FilterQuality.medium),
420+
// Show the "play" icon even when previewImageSrcUrl didn't resolve;
421+
// the action uses hrefUrl, which might still work.
422+
const Icon(
423+
Icons.play_arrow_rounded,
424+
color: Colors.white,
425+
size: 32),
426+
]));
427+
}
428+
}
429+
407430
class MessageMediaContainer extends StatelessWidget {
408431
const MessageMediaContainer({
409432
super.key,

test/widgets/content_test.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,54 @@ void main() {
328328
});
329329
});
330330

331+
group("MessageEmbedVideo", () {
332+
Future<void> prepareContent(WidgetTester tester, String html) async {
333+
addTearDown(testBinding.reset);
334+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
335+
prepareBoringImageHttpClient();
336+
337+
await tester.pumpWidget(GlobalStoreWidget(child: MaterialApp(
338+
home: PerAccountStoreWidget(accountId: eg.selfAccount.id,
339+
child: MessageContent(
340+
message: eg.streamMessage(content: html),
341+
content: parseContent(html))))));
342+
await tester.pump(); // global store
343+
await tester.pump(); // per-account store
344+
debugNetworkImageHttpClientProvider = null;
345+
}
346+
347+
Future<void> checkEmbedVideo(WidgetTester tester, ContentExample example) async {
348+
await prepareContent(tester, example.html);
349+
350+
final expectedTitle = (((example.expectedNodes[0] as ParagraphNode)
351+
.nodes.single as LinkNode).nodes.single as TextNode).text;
352+
await tester.ensureVisible(find.text(expectedTitle));
353+
354+
final expectedVideo = example.expectedNodes[1] as EmbedVideoNode;
355+
final expectedResolvedUrl = eg.store()
356+
.tryResolveUrl(expectedVideo.previewImageSrcUrl)!;
357+
final image = tester.widget<RealmContentNetworkImage>(
358+
find.byType(RealmContentNetworkImage));
359+
check(image.src).equals(expectedResolvedUrl);
360+
361+
final expectedLaunchUrl = expectedVideo.hrefUrl;
362+
await tester.tap(find.byIcon(Icons.play_arrow_rounded));
363+
check(testBinding.takeLaunchUrlCalls())
364+
.single.equals((url: Uri.parse(expectedLaunchUrl), mode: LaunchMode.platformDefault));
365+
}
366+
367+
testWidgets('video preview for youtube embed', (tester) async {
368+
const example = ContentExample.videoEmbedYoutube;
369+
await checkEmbedVideo(tester, example);
370+
});
371+
372+
testWidgets('video preview for vimeo embed', (tester) async {
373+
const example = ContentExample.videoEmbedVimeo;
374+
await checkEmbedVideo(tester, example);
375+
});
376+
});
377+
378+
331379
group("CodeBlock", () {
332380
testContentSmoke(ContentExample.codeBlockPlain);
333381
testContentSmoke(ContentExample.codeBlockHighlightedShort);

0 commit comments

Comments
 (0)