mirror of
https://github.com/feeddeck/feeddeck.git
synced 2026-05-03 17:48:00 -05:00
This commit improves / simplifies the media handling within the app. Until now we always had to provide the type of the media file (item media / source icon) when we wanted to display it. This is now not necessary anymore. Instead we always display the image from it's original path when the url starts with "http://" or "https://". Additionally we also check the platform to proxy the image request on the web. If the image doesn't start with "http://" or "https://" we always try to display the image from the Supabase storage. For this we also adjusted the corresponding Deno function, so that we check if the image starts with "http://" or "https://" before we upload it to the Supabase storage. If this is the case we upload the source icon to the Supabase storage. If the upload fails, we will use the original path of the icon. Last but not least this commit also introduces our own "CachedNetworkImage" widget, which wraps the original "CachedNetworkImage" widget. Our own widget will ensure that we use the correct url for the image request, so that we do not have to use this function all over the app anymore. Later this widget can also be used to introduce our own cache manager.
168 lines
5.2 KiB
Dart
168 lines
5.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
|
import 'package:html2md/html2md.dart' as html2md;
|
|
|
|
import 'package:feeddeck/utils/constants.dart';
|
|
import 'package:feeddeck/utils/font.dart';
|
|
import 'package:feeddeck/utils/openurl.dart';
|
|
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
|
|
|
|
/// The [DescriptionFormat] enum defines the source and target format of a
|
|
/// description.
|
|
enum DescriptionFormat {
|
|
html,
|
|
markdown,
|
|
plain,
|
|
}
|
|
|
|
/// The [ItemDescription] widget displays the description of an item. The
|
|
/// provided [itemDescription] is converted from the [sourceFormat] to the
|
|
/// [tagetFormat] before displayed.
|
|
class ItemDescription extends StatelessWidget {
|
|
const ItemDescription({
|
|
super.key,
|
|
required this.itemDescription,
|
|
required this.sourceFormat,
|
|
required this.tagetFormat,
|
|
this.disableImages,
|
|
});
|
|
|
|
final String? itemDescription;
|
|
final DescriptionFormat sourceFormat;
|
|
final DescriptionFormat tagetFormat;
|
|
final bool? disableImages;
|
|
|
|
/// [_openUrl] opens the item url in the default browser of the current
|
|
/// device.
|
|
Future<void> _openUrl(String link) async {
|
|
try {
|
|
await openUrl(link);
|
|
} catch (_) {}
|
|
}
|
|
|
|
/// [_buildMarkdown] renders the provided [content] as markdown.
|
|
Widget _buildMarkdown(BuildContext context, String content) {
|
|
return MarkdownBody(
|
|
selectable: true,
|
|
data: content.trim(),
|
|
styleSheet: MarkdownStyleSheet(
|
|
code: TextStyle(
|
|
fontFamily: getMonospaceFontFamily(),
|
|
backgroundColor: Constants.secondary,
|
|
),
|
|
codeblockDecoration: const BoxDecoration(
|
|
color: Constants.secondary,
|
|
),
|
|
),
|
|
onTapLink: (text, href, title) {
|
|
if (href != null) {
|
|
_openUrl(href);
|
|
}
|
|
},
|
|
imageBuilder: (uri, title, alt) {
|
|
if (disableImages == true) {
|
|
return Container();
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.only(
|
|
bottom: Constants.spacingMiddle,
|
|
),
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
isDismissible: true,
|
|
useSafeArea: true,
|
|
backgroundColor: Colors.black,
|
|
constraints: const BoxConstraints(
|
|
maxWidth: double.infinity,
|
|
),
|
|
builder: (BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
body: Stack(
|
|
children: [
|
|
Center(
|
|
child: CachedNetworkImage(
|
|
fit: BoxFit.contain,
|
|
imageUrl: uri.toString(),
|
|
placeholder: (context, url) => Container(),
|
|
errorWidget: (context, url, error) => Container(),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: Constants.spacingExtraSmall,
|
|
right: Constants.spacingExtraSmall,
|
|
child: IconButton(
|
|
icon: const Icon(
|
|
Icons.close,
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
child: CachedNetworkImage(
|
|
width: double.infinity,
|
|
fit: BoxFit.contain,
|
|
imageUrl: uri.toString(),
|
|
placeholder: (context, url) => Container(),
|
|
errorWidget: (context, url, error) => Container(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// [_buildPlain] renders the provided [content] as plain text.
|
|
Widget _buildPlain(String content) {
|
|
return SelectableText(
|
|
content.trim(),
|
|
textAlign: TextAlign.left,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.normal,
|
|
fontSize: 14,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (itemDescription == null || itemDescription == '') {
|
|
return Container();
|
|
}
|
|
|
|
if (sourceFormat == DescriptionFormat.html &&
|
|
tagetFormat == DescriptionFormat.markdown) {
|
|
return _buildMarkdown(context, html2md.convert(itemDescription!));
|
|
}
|
|
|
|
if (sourceFormat == DescriptionFormat.markdown &&
|
|
tagetFormat == DescriptionFormat.markdown) {
|
|
return _buildMarkdown(context, itemDescription!);
|
|
}
|
|
|
|
if (sourceFormat == DescriptionFormat.html &&
|
|
tagetFormat == DescriptionFormat.plain) {
|
|
return _buildPlain(
|
|
itemDescription!.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ''),
|
|
);
|
|
}
|
|
|
|
return _buildPlain(itemDescription!);
|
|
}
|
|
}
|