Skip to content

Commit e2882a7

Browse files
draft
1 parent 9044a9a commit e2882a7

File tree

10 files changed

+749
-7
lines changed

10 files changed

+749
-7
lines changed

ios/Podfile.lock

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ PODS:
131131
- SwiftyGif (5.4.4)
132132
- url_launcher_ios (0.0.1):
133133
- Flutter
134+
- video_player_avfoundation (0.0.1):
135+
- Flutter
136+
- FlutterMacOS
134137

135138
DEPENDENCIES:
136139
- app_settings (from `.symlinks/plugins/app_settings/ios`)
@@ -147,6 +150,7 @@ DEPENDENCIES:
147150
- share_plus (from `.symlinks/plugins/share_plus/ios`)
148151
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
149152
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
153+
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
150154

151155
SPEC REPOS:
152156
trunk:
@@ -194,6 +198,8 @@ EXTERNAL SOURCES:
194198
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
195199
url_launcher_ios:
196200
:path: ".symlinks/plugins/url_launcher_ios/ios"
201+
video_player_avfoundation:
202+
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
197203

198204
SPEC CHECKSUMS:
199205
app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc
@@ -224,7 +230,8 @@ SPEC CHECKSUMS:
224230
sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80
225231
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
226232
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
233+
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
227234

228235
PODFILE CHECKSUM: 6998435987a000fdec9b2e1b5b1eef6d54bdba77
229236

230-
COCOAPODS: 1.13.0
237+
COCOAPODS: 1.15.0

lib/model/content.dart

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,34 @@ class ImageNode extends BlockContentNode {
361361
}
362362
}
363363

364+
class VideoNode extends BlockContentNode {
365+
const VideoNode({
366+
super.debugHtmlNode,
367+
required this.srcUrl,
368+
this.previewImageUrl,
369+
});
370+
371+
final String srcUrl;
372+
final String? previewImageUrl;
373+
374+
@override
375+
bool operator ==(Object other) {
376+
return other is VideoNode
377+
&& other.srcUrl == srcUrl
378+
&& other.previewImageUrl == previewImageUrl;
379+
}
380+
381+
@override
382+
int get hashCode => Object.hash('VideoNode', srcUrl, previewImageUrl);
383+
384+
@override
385+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
386+
super.debugFillProperties(properties);
387+
properties.add(StringProperty('srcUrl', srcUrl));
388+
properties.add(StringProperty('previewImageUrl', previewImageUrl));
389+
}
390+
}
391+
364392
/// A content node that expects an inline layout context from its parent.
365393
///
366394
/// When rendered into a Flutter widget tree, an inline content node
@@ -694,6 +722,29 @@ class _ZulipContentParser {
694722
}();
695723
static final _emojiCodeFromClassNameRegexp = RegExp(r"emoji-([^ ]+)");
696724

725+
static final _videoClassNameRegexp = () {
726+
const sourceType = r"(message_inline_video)?(youtube-video)?(embed-video)?";
727+
return RegExp("^message_inline_image $sourceType|$sourceType message_inline_image\$");
728+
}();
729+
730+
bool _isVideoClassNameInlineVideo(String input) {
731+
if (!_videoClassNameRegexp.hasMatch(input)) return false;
732+
final groups = _videoClassNameRegexp.firstMatch(input)!.groups([1, 4]);
733+
return groups.nonNulls.firstOrNull == 'message_inline_video';
734+
}
735+
736+
bool _isVideoClassNameYoutubeVideo(String input) {
737+
if (!_videoClassNameRegexp.hasMatch(input)) return false;
738+
final groups = _videoClassNameRegexp.firstMatch(input)!.groups([2, 5]);
739+
return groups.nonNulls.firstOrNull == 'youtube-video';
740+
}
741+
742+
bool _isVideoClassNameEmbedVideo(String input) {
743+
if (!_videoClassNameRegexp.hasMatch(input)) return false;
744+
final groups = _videoClassNameRegexp.firstMatch(input)!.groups([3, 6]);
745+
return groups.nonNulls.firstOrNull == 'embed-video';
746+
}
747+
697748
InlineContentNode parseInlineContent(dom.Node node) {
698749
assert(_debugParserContext == _ParserContext.inline);
699750
final debugHtmlNode = kDebugMode ? node : null;
@@ -948,6 +999,85 @@ class _ZulipContentParser {
948999
return ImageNode(srcUrl: src, debugHtmlNode: debugHtmlNode);
9491000
}
9501001

1002+
BlockContentNode parseVideoNode(dom.Element divElement) {
1003+
assert(_debugParserContext == _ParserContext.block);
1004+
assert(divElement.localName == 'div'
1005+
&& _videoClassNameRegexp.hasMatch(divElement.className));
1006+
1007+
if (_isVideoClassNameInlineVideo(divElement.className)) {
1008+
return _parseInlineVideo(divElement);
1009+
}
1010+
if (_isVideoClassNameYoutubeVideo(divElement.className)
1011+
|| _isVideoClassNameEmbedVideo(divElement.className)) {
1012+
return _parseEmbedVideoWithPreviewImage(divElement);
1013+
}
1014+
return UnimplementedBlockContentNode(htmlNode: divElement);
1015+
}
1016+
1017+
BlockContentNode _parseInlineVideo(dom.Element divElement) {
1018+
final videoElement = () {
1019+
if (divElement.nodes.length != 1) return null;
1020+
final child = divElement.nodes[0];
1021+
if (child is! dom.Element) return null;
1022+
if (child.localName != 'a') return null;
1023+
if (child.className.isNotEmpty) return null;
1024+
1025+
if (child.nodes.length != 1) return null;
1026+
final grandchild = child.nodes[0];
1027+
if (grandchild is! dom.Element) return null;
1028+
if (grandchild.localName != 'video') return null;
1029+
if (grandchild.className.isNotEmpty) return null;
1030+
return grandchild;
1031+
}();
1032+
1033+
final debugHtmlNode = kDebugMode ? divElement : null;
1034+
if (videoElement == null) {
1035+
return UnimplementedBlockContentNode(htmlNode: divElement);
1036+
}
1037+
1038+
final src = videoElement.attributes['src'];
1039+
if (src == null) {
1040+
return UnimplementedBlockContentNode(htmlNode: divElement);
1041+
}
1042+
1043+
return VideoNode(srcUrl: src, debugHtmlNode: debugHtmlNode);
1044+
}
1045+
1046+
BlockContentNode _parseEmbedVideoWithPreviewImage(dom.Element divElement) {
1047+
final result = () {
1048+
if (divElement.nodes.length != 1) return null;
1049+
final child = divElement.nodes[0];
1050+
if (child is! dom.Element) return null;
1051+
if (child.localName != 'a') return null;
1052+
if (child.className.isNotEmpty) return null;
1053+
1054+
if (child.nodes.length != 1) return null;
1055+
final grandchild = child.nodes[0];
1056+
if (grandchild is! dom.Element) return null;
1057+
if (grandchild.localName != 'img') return null;
1058+
if (grandchild.className.isNotEmpty) return null;
1059+
return (child, grandchild);
1060+
}();
1061+
1062+
final debugHtmlNode = kDebugMode ? divElement : null;
1063+
if (result == null) {
1064+
return UnimplementedBlockContentNode(htmlNode: divElement);
1065+
}
1066+
final (anchorElement, imgElement) = result;
1067+
1068+
final imgSrc = imgElement.attributes['src'];
1069+
if (imgSrc == null) {
1070+
return UnimplementedBlockContentNode(htmlNode: divElement);
1071+
}
1072+
1073+
final href = anchorElement.attributes['href'];
1074+
if (href == null) {
1075+
return UnimplementedBlockContentNode(htmlNode: divElement);
1076+
}
1077+
1078+
return VideoNode(srcUrl: href, previewImageUrl: imgSrc, debugHtmlNode: debugHtmlNode);
1079+
}
1080+
9511081
BlockContentNode parseBlockContent(dom.Node node) {
9521082
assert(_debugParserContext == _ParserContext.block);
9531083
final debugHtmlNode = kDebugMode ? node : null;
@@ -1024,6 +1154,10 @@ class _ZulipContentParser {
10241154
return parseImageNode(element);
10251155
}
10261156

1157+
if (localName == 'div' && _videoClassNameRegexp.hasMatch(className)) {
1158+
return parseVideoNode(element);
1159+
}
1160+
10271161
// TODO more types of node
10281162
return UnimplementedBlockContentNode(htmlNode: node);
10291163
}

0 commit comments

Comments
 (0)