✨ Basic fediverse posts displaying
This commit is contained in:
@ -95,8 +95,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image)
|
||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) {
|
||||
return;
|
||||
}
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null).cast(),
|
||||
@ -209,7 +210,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
child: AspectRatio(
|
||||
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
|
||||
child: ScrollConfiguration(
|
||||
behavior: _AttachmentListScrollBehavior(),
|
||||
behavior: AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
shrinkWrap: true,
|
||||
@ -283,7 +284,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
}
|
||||
}
|
||||
|
||||
class _AttachmentListScrollBehavior extends MaterialScrollBehavior {
|
||||
class AttachmentListScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices =>
|
||||
{PointerDeviceKind.touch, PointerDeviceKind.mouse};
|
||||
|
108
lib/widgets/html.dart
Normal file
108
lib/widgets/html.dart
Normal file
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
List<Widget> parseHtmlToWidgets(
|
||||
BuildContext context, Iterable<dom.Element>? elements) {
|
||||
if (elements == null) return [];
|
||||
|
||||
final List<Widget> widgets = [];
|
||||
|
||||
for (final node in elements) {
|
||||
switch (node.localName) {
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
widgets.add(Text(node.text.trim(),
|
||||
style: Theme.of(context).textTheme.titleMedium));
|
||||
break;
|
||||
case 'p':
|
||||
if (node.text.trim().isEmpty) continue;
|
||||
widgets.add(
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: node.text.trim(),
|
||||
children: [
|
||||
for (final child in node.children)
|
||||
switch (child.localName) {
|
||||
'a' => TextSpan(
|
||||
text: child.text.trim(),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlString(child.attributes['href']!);
|
||||
},
|
||||
),
|
||||
_ => TextSpan(text: child.text.trim()),
|
||||
},
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'a':
|
||||
// drop single link
|
||||
break;
|
||||
case 'div':
|
||||
// ignore div text, normally it is not meaningful
|
||||
widgets.addAll(parseHtmlToWidgets(context, node.children));
|
||||
break;
|
||||
case 'hr':
|
||||
widgets.add(const Divider());
|
||||
break;
|
||||
case 'img':
|
||||
var src = node.attributes['src'];
|
||||
if (src == null) break;
|
||||
final width = double.tryParse(node.attributes['width'] ?? 'null');
|
||||
final height = double.tryParse(node.attributes['height'] ?? 'null');
|
||||
final ratio = width != null && height != null ? width / height : 1.0;
|
||||
if (src.startsWith('//')) {
|
||||
src = 'https:$src';
|
||||
} else if (!src.startsWith('http')) {
|
||||
// final baseUri = Uri.parse(_article!.url);
|
||||
// final baseUrl = '${baseUri.scheme}://${baseUri.host}';
|
||||
src = src;
|
||||
}
|
||||
widgets.add(
|
||||
AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
height: height ?? double.infinity,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoResizeUniversalImage(
|
||||
src,
|
||||
fit: width != null && height != null
|
||||
? BoxFit.cover
|
||||
: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
widgets.addAll(parseHtmlToWidgets(context, node.children));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
107
lib/widgets/post/fediverse_post_item.dart
Normal file
107
lib/widgets/post/fediverse_post_item.dart
Normal file
@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/html.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class FediversePostWidget extends StatelessWidget {
|
||||
final SnFediversePost data;
|
||||
final double maxWidth;
|
||||
const FediversePostWidget(
|
||||
{super.key, required this.data, required this.maxWidth});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderSide =
|
||||
BorderSide(width: 1, color: Theme.of(context).dividerColor);
|
||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AccountImage(content: data.user.avatar),
|
||||
const Gap(8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.user.nick.isNotEmpty
|
||||
? data.user.nick
|
||||
: '@${data.user.name}',
|
||||
).bold(),
|
||||
Text(data.user.identifier),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
...parseHtmlToWidgets(context, parse(data.content).children),
|
||||
if (data.images.isNotEmpty)
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ScrollConfiguration(
|
||||
behavior: AttachmentListScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: data.images.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AutoResizeUniversalImage(
|
||||
data.images[idx],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label: Text(
|
||||
'${idx + 1}/${data.images.length}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 8),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user