Basic fediverse posts displaying

This commit is contained in:
2025-03-13 00:09:28 +08:00
parent f2d913ffec
commit e44320e0fe
13 changed files with 1512 additions and 271 deletions

View File

@ -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
View 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;
}

View 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),
),
),
);
}
}