diff --git a/app/lib/widgets/item/details/item_details_rss.dart b/app/lib/widgets/item/details/item_details_rss.dart index 4aa2268..bede5ab 100644 --- a/app/lib/widgets/item/details/item_details_rss.dart +++ b/app/lib/widgets/item/details/item_details_rss.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:html/parser.dart' show parse; + import 'package:feeddeck/models/item.dart'; import 'package:feeddeck/models/source.dart'; import 'package:feeddeck/widgets/item/details/utils/item_description.dart'; @@ -17,8 +19,26 @@ class ItemDetailsRSS extends StatelessWidget { final FDItem item; final FDSource source; + /// [_buildImage] renders the [item.media] when the [shouldBeRendered] is + /// `true`. If it is `false` an empty container is returned. + Widget _buildImage(bool shouldBeRendered) { + if (!shouldBeRendered) { + return Container(); + } + + return ItemMedia( + itemMedia: item.media, + ); + } + @override Widget build(BuildContext context) { + /// Check if the description of the RSS feed contains an image. If this is + /// the case we do not render the image from the [item.media] because the + /// image is already rendered in the [ItemDescription] widget. + final descriptionContainImage = + parse(item.description).querySelectorAll('img').isNotEmpty; + return Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.start, @@ -30,13 +50,11 @@ class ItemDetailsRSS extends StatelessWidget { item: item, source: source, ), - ItemMedia( - itemMedia: item.media, - ), + _buildImage(!descriptionContainImage), ItemDescription( itemDescription: item.description, - sourceFormat: DescriptionFormat.plain, - tagetFormat: DescriptionFormat.plain, + sourceFormat: DescriptionFormat.html, + tagetFormat: DescriptionFormat.markdown, ), ], ); diff --git a/app/lib/widgets/item/details/utils/item_description.dart b/app/lib/widgets/item/details/utils/item_description.dart index 461eb9a..8f24caa 100644 --- a/app/lib/widgets/item/details/utils/item_description.dart +++ b/app/lib/widgets/item/details/utils/item_description.dart @@ -47,7 +47,7 @@ class ItemDescription extends StatelessWidget { Widget _buildMarkdown(BuildContext context, String content) { return MarkdownBody( selectable: true, - data: content, + data: content.trim(), styleSheet: MarkdownStyleSheet( code: TextStyle( fontFamily: getMonospaceFontFamily(), @@ -135,7 +135,7 @@ class ItemDescription extends StatelessWidget { /// [_buildPlain] renders the provided [content] as plain text. Widget _buildPlain(String content) { return SelectableText( - content, + content.trim(), textAlign: TextAlign.left, style: const TextStyle( fontWeight: FontWeight.normal, diff --git a/app/lib/widgets/item/details/utils/item_subtitle.dart b/app/lib/widgets/item/details/utils/item_subtitle.dart index 517300f..6c33183 100644 --- a/app/lib/widgets/item/details/utils/item_subtitle.dart +++ b/app/lib/widgets/item/details/utils/item_subtitle.dart @@ -23,9 +23,10 @@ class ItemSubtitle extends StatelessWidget { @override Widget build(BuildContext context) { final sourceType = source.type.toLocalizedString(); - final sourceTitle = source.title != '' ? ' / ${source.title}' : ''; - final author = - item.author != null && item.author != '' ? ' / ${item.author}' : ''; + final sourceTitle = source.title != '' ? ' / ${source.title.trim()}' : ''; + final author = item.author != null && item.author != '' + ? ' / ${item.author!.trim()}' + : ''; final publishedAt = ' / ${DateFormat.yMMMMd().add_Hm().format(DateTime.fromMillisecondsSinceEpoch(item.publishedAt * 1000))}'; diff --git a/app/lib/widgets/item/preview/item_preview_rss.dart b/app/lib/widgets/item/preview/item_preview_rss.dart index 89ceade..3f15a50 100644 --- a/app/lib/widgets/item/preview/item_preview_rss.dart +++ b/app/lib/widgets/item/preview/item_preview_rss.dart @@ -41,7 +41,7 @@ class ItemPreviewRSS extends StatelessWidget { ), ItemDescription( itemDescription: item.description, - sourceFormat: DescriptionFormat.plain, + sourceFormat: DescriptionFormat.html, tagetFormat: DescriptionFormat.plain, ), ], diff --git a/app/lib/widgets/item/preview/utils/item_description.dart b/app/lib/widgets/item/preview/utils/item_description.dart index 62f17f1..cf5f82b 100644 --- a/app/lib/widgets/item/preview/utils/item_description.dart +++ b/app/lib/widgets/item/preview/utils/item_description.dart @@ -46,7 +46,7 @@ class ItemDescription extends StatelessWidget { ), child: MarkdownBody( selectable: false, - data: content, + data: content.trim(), styleSheet: MarkdownStyleSheet( code: TextStyle( fontFamily: getMonospaceFontFamily(), @@ -87,7 +87,7 @@ class ItemDescription extends StatelessWidget { bottom: Constants.spacingExtraSmall, ), child: Text( - content, + content.trim(), maxLines: 5, style: const TextStyle( overflow: TextOverflow.ellipsis, diff --git a/app/pubspec.lock b/app/pubspec.lock index d5a5822..be39747 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -321,7 +321,7 @@ packages: source: hosted version: "1.1.0" html: - dependency: transitive + dependency: "direct main" description: name: html sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index f231eaa..5e13605 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: collection: ^1.17.0 flutter_markdown: ^0.6.14 flutter_native_splash: ^2.2.19 + html: ^0.15.4 html2md: ^1.2.6 intl: ^0.18.1 just_audio: ^0.9.32 diff --git a/supabase/functions/_shared/feed/rss.ts b/supabase/functions/_shared/feed/rss.ts index 07dfd08..04350c6 100644 --- a/supabase/functions/_shared/feed/rss.ts +++ b/supabase/functions/_shared/feed/rss.ts @@ -232,7 +232,7 @@ const getMedia = (entry: FeedEntry): string | undefined => { for (const media of entry["media:content"]) { if ( media.medium && media.medium === "image" && media.url && - media.url.startsWith("https://") + media.url.startsWith("https://") && !media.url.endsWith(".svg") ) { return media.url; } @@ -253,7 +253,8 @@ const getMedia = (entry: FeedEntry): string | undefined => { if ( mediaContent.medium && mediaContent.medium === "image" && mediaContent.url && - mediaContent.url.startsWith("https://") + mediaContent.url.startsWith("https://") && + !mediaContent.url.endsWith(".svg") ) { return mediaContent.url; } @@ -267,7 +268,8 @@ const getMedia = (entry: FeedEntry): string | undefined => { if ( attachment.mimeType && attachment.mimeType.startsWith("image/") && attachment.url && - attachment.url.startsWith("https://") + attachment.url.startsWith("https://") && + !attachment.url.endsWith(".svg") ) { return attachment.url; } @@ -278,7 +280,10 @@ const getMedia = (entry: FeedEntry): string | undefined => { const matches = /]+\bsrc=["']([^"']+)["']/.exec( entry.description?.value, ); - if (matches && matches.length == 2 && matches[1].startsWith("https://")) { + if ( + matches && matches.length == 2 && matches[1].startsWith("https://") && + !matches[1].endsWith(".svg") + ) { return matches[1]; } } @@ -287,7 +292,10 @@ const getMedia = (entry: FeedEntry): string | undefined => { const matches = /]+\bsrc=["']([^"']+)["']/.exec( entry.content?.value, ); - if (matches && matches.length == 2 && matches[1].startsWith("https://")) { + if ( + matches && matches.length == 2 && matches[1].startsWith("https://") && + !matches[1].endsWith(".svg") + ) { return matches[1]; } }