mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Support chapter groups.
This commit is contained in:
790
lib/pages/comic_details_page/comments_page.dart
Normal file
790
lib/pages/comic_details_page/comments_page.dart
Normal file
@@ -0,0 +1,790 @@
|
||||
part of 'comic_page.dart';
|
||||
|
||||
class CommentsPage extends StatefulWidget {
|
||||
const CommentsPage(
|
||||
{super.key, required this.data, required this.source, this.replyId});
|
||||
|
||||
final ComicDetails data;
|
||||
|
||||
final ComicSource source;
|
||||
|
||||
final String? replyId;
|
||||
|
||||
@override
|
||||
State<CommentsPage> createState() => _CommentsPageState();
|
||||
}
|
||||
|
||||
class _CommentsPageState extends State<CommentsPage> {
|
||||
bool _loading = true;
|
||||
List<Comment>? _comments;
|
||||
String? _error;
|
||||
int _page = 1;
|
||||
int? maxPage;
|
||||
var controller = TextEditingController();
|
||||
bool sending = false;
|
||||
|
||||
void firstLoad() async {
|
||||
var res = await widget.source.commentsLoader!(
|
||||
widget.data.comicId, widget.data.subId, 1, widget.replyId);
|
||||
if (res.error) {
|
||||
setState(() {
|
||||
_error = res.errorMessage;
|
||||
_loading = false;
|
||||
});
|
||||
} else if (mounted) {
|
||||
setState(() {
|
||||
_comments = res.data;
|
||||
_loading = false;
|
||||
maxPage = res.subData;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void loadMore() async {
|
||||
var res = await widget.source.commentsLoader!(
|
||||
widget.data.comicId, widget.data.subId, _page + 1, widget.replyId);
|
||||
if (res.error) {
|
||||
context.showMessage(message: res.errorMessage ?? "Unknown Error");
|
||||
} else {
|
||||
setState(() {
|
||||
_comments!.addAll(res.data);
|
||||
_page++;
|
||||
if (maxPage == null && res.data.isEmpty) {
|
||||
maxPage = _page;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: Appbar(
|
||||
title: Text("Comments".tl),
|
||||
style: AppbarStyle.shadow,
|
||||
),
|
||||
body: buildBody(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
if (_loading) {
|
||||
firstLoad();
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else if (_error != null) {
|
||||
return NetworkError(
|
||||
message: _error!,
|
||||
retry: () {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
},
|
||||
withAppbar: false,
|
||||
);
|
||||
} else {
|
||||
var showAvatar = _comments!.any((e) {
|
||||
return e.avatar != null;
|
||||
});
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
primary: false,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _comments!.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _comments!.length) {
|
||||
if (_page < (maxPage ?? _page + 1)) {
|
||||
loadMore();
|
||||
return const ListLoadingIndicator();
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
return _CommentTile(
|
||||
comment: _comments![index],
|
||||
source: widget.source,
|
||||
comic: widget.data,
|
||||
showAvatar: showAvatar,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
buildBottom(context)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildBottom(BuildContext context) {
|
||||
if (widget.source.sendCommentFunc == null) {
|
||||
return const SizedBox(
|
||||
height: 0,
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Material(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
hintText: "Comment".tl),
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
),
|
||||
),
|
||||
if (sending)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8.5),
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
if (controller.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
sending = true;
|
||||
});
|
||||
var b = await widget.source.sendCommentFunc!(
|
||||
widget.data.comicId,
|
||||
widget.data.subId,
|
||||
controller.text,
|
||||
widget.replyId);
|
||||
if (!b.error) {
|
||||
controller.text = "";
|
||||
setState(() {
|
||||
sending = false;
|
||||
_loading = true;
|
||||
_comments?.clear();
|
||||
_page = 1;
|
||||
maxPage = null;
|
||||
});
|
||||
} else {
|
||||
context.showMessage(message: b.errorMessage ?? "Error");
|
||||
setState(() {
|
||||
sending = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.send,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
)
|
||||
],
|
||||
).paddingVertical(2).paddingLeft(16).paddingRight(4),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CommentTile extends StatefulWidget {
|
||||
const _CommentTile({
|
||||
required this.comment,
|
||||
required this.source,
|
||||
required this.comic,
|
||||
required this.showAvatar,
|
||||
});
|
||||
|
||||
final Comment comment;
|
||||
|
||||
final ComicSource source;
|
||||
|
||||
final ComicDetails comic;
|
||||
|
||||
final bool showAvatar;
|
||||
|
||||
@override
|
||||
State<_CommentTile> createState() => _CommentTileState();
|
||||
}
|
||||
|
||||
class _CommentTileState extends State<_CommentTile> {
|
||||
@override
|
||||
void initState() {
|
||||
likes = widget.comment.score ?? 0;
|
||||
isLiked = widget.comment.isLiked ?? false;
|
||||
voteStatus = widget.comment.voteStatus;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.showAvatar)
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer),
|
||||
child: widget.comment.avatar == null
|
||||
? null
|
||||
: AnimatedImage(
|
||||
image: CachedImageProvider(
|
||||
widget.comment.avatar!,
|
||||
sourceKey: widget.source.key,
|
||||
),
|
||||
),
|
||||
).paddingRight(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.comment.userName,
|
||||
style: ts.bold,
|
||||
),
|
||||
if (widget.comment.time != null)
|
||||
Text(widget.comment.time!, style: ts.s12),
|
||||
const SizedBox(height: 4),
|
||||
_CommentContent(text: widget.comment.content),
|
||||
buildActions(),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
).paddingAll(16),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions() {
|
||||
if (widget.comment.score == null && widget.comment.replyCount == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (widget.comment.score != null &&
|
||||
widget.source.voteCommentFunc != null)
|
||||
buildVote(),
|
||||
if (widget.comment.score != null &&
|
||||
widget.source.likeCommentFunc != null)
|
||||
buildLike(),
|
||||
if (widget.comment.replyCount != null) buildReply(),
|
||||
],
|
||||
),
|
||||
).paddingTop(8);
|
||||
}
|
||||
|
||||
Widget buildReply() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 0.6,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
showSideBar(
|
||||
context,
|
||||
CommentsPage(
|
||||
data: widget.comic,
|
||||
source: widget.source,
|
||||
replyId: widget.comment.id,
|
||||
),
|
||||
showBarrier: false,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.insert_comment_outlined, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.comment.replyCount.toString()),
|
||||
],
|
||||
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool isLiking = false;
|
||||
|
||||
bool isLiked = false;
|
||||
|
||||
var likes = 0;
|
||||
|
||||
Widget buildLike() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 0.6,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () async {
|
||||
if (isLiking) return;
|
||||
setState(() {
|
||||
isLiking = true;
|
||||
});
|
||||
var res = await widget.source.likeCommentFunc!(
|
||||
widget.comic.comicId,
|
||||
widget.comic.subId,
|
||||
widget.comment.id!,
|
||||
!isLiked,
|
||||
);
|
||||
if (res.success) {
|
||||
isLiked = !isLiked;
|
||||
likes += isLiked ? 1 : -1;
|
||||
} else {
|
||||
context.showMessage(message: res.errorMessage ?? "Error");
|
||||
}
|
||||
setState(() {
|
||||
isLiking = false;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isLiking)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else if (isLiked)
|
||||
Icon(
|
||||
Icons.favorite,
|
||||
size: 16,
|
||||
color: context.useTextColor(Colors.red),
|
||||
)
|
||||
else
|
||||
const Icon(Icons.favorite_border, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(likes.toString()),
|
||||
],
|
||||
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int? voteStatus;
|
||||
|
||||
bool isVotingUp = false;
|
||||
|
||||
bool isVotingDown = false;
|
||||
|
||||
void vote(bool isUp) async {
|
||||
if (isVotingUp || isVotingDown) return;
|
||||
setState(() {
|
||||
if (isUp) {
|
||||
isVotingUp = true;
|
||||
} else {
|
||||
isVotingDown = true;
|
||||
}
|
||||
});
|
||||
var isCancel = (isUp && voteStatus == 1) || (!isUp && voteStatus == -1);
|
||||
var res = await widget.source.voteCommentFunc!(
|
||||
widget.comic.comicId,
|
||||
widget.comic.subId,
|
||||
widget.comment.id!,
|
||||
isUp,
|
||||
isCancel,
|
||||
);
|
||||
if (res.success) {
|
||||
if (isCancel) {
|
||||
voteStatus = 0;
|
||||
} else {
|
||||
if (isUp) {
|
||||
voteStatus = 1;
|
||||
} else {
|
||||
voteStatus = -1;
|
||||
}
|
||||
}
|
||||
widget.comment.voteStatus = voteStatus;
|
||||
widget.comment.score = res.data ?? widget.comment.score;
|
||||
} else {
|
||||
context.showMessage(message: res.errorMessage ?? "Error");
|
||||
}
|
||||
setState(() {
|
||||
isVotingUp = false;
|
||||
isVotingDown = false;
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildVote() {
|
||||
var upColor = context.colorScheme.outline;
|
||||
if (voteStatus == 1) {
|
||||
upColor = context.useTextColor(Colors.red);
|
||||
}
|
||||
var downColor = context.colorScheme.outline;
|
||||
if (voteStatus == -1) {
|
||||
downColor = context.useTextColor(Colors.blue);
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 0.6,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Button.icon(
|
||||
isLoading: isVotingUp,
|
||||
icon: const Icon(Icons.arrow_upward),
|
||||
size: 18,
|
||||
color: upColor,
|
||||
onPressed: () => vote(true),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(widget.comment.score.toString()),
|
||||
const SizedBox(width: 4),
|
||||
Button.icon(
|
||||
isLoading: isVotingDown,
|
||||
icon: const Icon(Icons.arrow_downward),
|
||||
size: 18,
|
||||
color: downColor,
|
||||
onPressed: () => vote(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CommentContent extends StatelessWidget {
|
||||
const _CommentContent({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!text.contains('<') && !text.contains('http')) {
|
||||
return SelectableText(text);
|
||||
} else {
|
||||
return RichCommentContent(text: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Tag {
|
||||
final String name;
|
||||
final Map<String, String> attributes;
|
||||
|
||||
const _Tag(this.name, this.attributes);
|
||||
|
||||
TextSpan merge(TextSpan s, BuildContext context) {
|
||||
var style = s.style ?? ts;
|
||||
style = switch (name) {
|
||||
'b' => style.bold,
|
||||
'i' => style.italic,
|
||||
'u' => style.underline,
|
||||
's' => style.lineThrough,
|
||||
'a' => style.withColor(context.colorScheme.primary),
|
||||
'strong' => style.bold,
|
||||
'span' => () {
|
||||
if (attributes.containsKey('style')) {
|
||||
var s = attributes['style']!;
|
||||
var css = s.split(';');
|
||||
for (var c in css) {
|
||||
var kv = c.split(':');
|
||||
if (kv.length == 2) {
|
||||
var key = kv[0].trim();
|
||||
var value = kv[1].trim();
|
||||
switch (key) {
|
||||
case 'color':
|
||||
// Color is not supported, we should make text display well in light and dark mode.
|
||||
break;
|
||||
case 'font-weight':
|
||||
if (value == 'bold') {
|
||||
style = style.bold;
|
||||
} else if (value == 'lighter') {
|
||||
style = style.light;
|
||||
}
|
||||
break;
|
||||
case 'font-style':
|
||||
if (value == 'italic') {
|
||||
style = style.italic;
|
||||
}
|
||||
break;
|
||||
case 'text-decoration':
|
||||
if (value == 'underline') {
|
||||
style = style.underline;
|
||||
} else if (value == 'line-through') {
|
||||
style = style.lineThrough;
|
||||
}
|
||||
break;
|
||||
case 'font-size':
|
||||
// Font size is not supported.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return style;
|
||||
}(),
|
||||
_ => style,
|
||||
};
|
||||
if (style.color != null) {
|
||||
style = style.copyWith(decorationColor: style.color);
|
||||
}
|
||||
var recognizer = s.recognizer;
|
||||
if (name == 'a') {
|
||||
var link = attributes['href'];
|
||||
if (link != null && link.isURL) {
|
||||
recognizer = TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
handleLink(link);
|
||||
};
|
||||
}
|
||||
}
|
||||
return TextSpan(
|
||||
text: s.text,
|
||||
style: style,
|
||||
recognizer: recognizer,
|
||||
);
|
||||
}
|
||||
|
||||
static void handleLink(String link) async {
|
||||
if (link.isURL) {
|
||||
if (await handleAppLink(Uri.parse(link))) {
|
||||
Navigator.of(App.rootContext).maybePop();
|
||||
} else {
|
||||
launchUrlString(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CommentImage {
|
||||
final String url;
|
||||
final String? link;
|
||||
|
||||
const _CommentImage(this.url, this.link);
|
||||
}
|
||||
|
||||
class RichCommentContent extends StatefulWidget {
|
||||
const RichCommentContent({super.key, required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
State<RichCommentContent> createState() => _RichCommentContentState();
|
||||
}
|
||||
|
||||
class _RichCommentContentState extends State<RichCommentContent> {
|
||||
var textSpan = <InlineSpan>[];
|
||||
var images = <_CommentImage>[];
|
||||
bool isRendered = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
if (!isRendered) {
|
||||
render();
|
||||
isRendered = true;
|
||||
}
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
bool isValidUrlChar(String char) {
|
||||
return RegExp(r'[a-zA-Z0-9%:/.@\-_?&=#*!+;]').hasMatch(char);
|
||||
}
|
||||
|
||||
void render() {
|
||||
var s = Queue<_Tag>();
|
||||
|
||||
int i = 0;
|
||||
var buffer = StringBuffer();
|
||||
var text = widget.text;
|
||||
text = text.replaceAll('\r\n', '\n');
|
||||
text = text.replaceAll('&', '&');
|
||||
|
||||
void writeBuffer() {
|
||||
if (buffer.isEmpty) return;
|
||||
var span = TextSpan(text: buffer.toString());
|
||||
for (var tag in s) {
|
||||
span = tag.merge(span, context);
|
||||
}
|
||||
textSpan.add(span);
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
while (i < text.length) {
|
||||
if (text[i] == '<' && i != text.length - 1) {
|
||||
if (text[i + 1] != '/') {
|
||||
// start tag
|
||||
var j = text.indexOf('>', i);
|
||||
if (j != -1) {
|
||||
var tagContent = text.substring(i + 1, j);
|
||||
var splits = tagContent.split(' ');
|
||||
splits.removeWhere((element) => element.isEmpty);
|
||||
var tagName = splits[0];
|
||||
var attributes = <String, String>{};
|
||||
for (var k = 1; k < splits.length; k++) {
|
||||
var attr = splits[k];
|
||||
var attrSplits = attr.split('=');
|
||||
if (attrSplits.length == 2) {
|
||||
attributes[attrSplits[0]] = attrSplits[1].replaceAll('"', '');
|
||||
}
|
||||
}
|
||||
const acceptedTags = ['img', 'a', 'b', 'i', 'u', 's', 'br', 'span', 'strong'];
|
||||
if (acceptedTags.contains(tagName)) {
|
||||
writeBuffer();
|
||||
if (tagName == 'img') {
|
||||
var url = attributes['src'];
|
||||
String? link;
|
||||
for (var tag in s) {
|
||||
if (tag.name == 'a') {
|
||||
link = tag.attributes['href'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (url != null) {
|
||||
images.add(_CommentImage(url, link));
|
||||
}
|
||||
} else if (tagName == 'br') {
|
||||
buffer.write('\n');
|
||||
} else {
|
||||
s.add(_Tag(tagName, attributes));
|
||||
}
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// end tag
|
||||
var j = text.indexOf('>', i);
|
||||
if (j != -1) {
|
||||
var tagContent = text.substring(i + 2, j);
|
||||
var splits = tagContent.split(' ');
|
||||
splits.removeWhere((element) => element.isEmpty);
|
||||
var tagName = splits[0];
|
||||
if (s.isNotEmpty && s.last.name == tagName) {
|
||||
writeBuffer();
|
||||
s.removeLast();
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
if (tagName == 'br') {
|
||||
i = j + 1;
|
||||
buffer.write('\n');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (text.length - i > 8 &&
|
||||
text.substring(i, i + 4) == 'http' &&
|
||||
!s.any((e) => e.name == 'a')) {
|
||||
// auto link
|
||||
int j = i;
|
||||
for (; j < text.length; j++) {
|
||||
if (!isValidUrlChar(text[j])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
var url = text.substring(i, j);
|
||||
if (url.isURL) {
|
||||
writeBuffer();
|
||||
textSpan.add(TextSpan(
|
||||
text: url,
|
||||
style: ts.withColor(context.colorScheme.primary),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
_Tag.handleLink(url);
|
||||
},
|
||||
));
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
buffer.write(text[i]);
|
||||
i++;
|
||||
}
|
||||
writeBuffer();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget content = SelectableText.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: textSpan,
|
||||
),
|
||||
);
|
||||
if (images.isNotEmpty) {
|
||||
content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
content,
|
||||
Wrap(
|
||||
runSpacing: 4,
|
||||
spacing: 4,
|
||||
children: images.map((e) {
|
||||
Widget image = Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: Image(
|
||||
width: 100,
|
||||
height: 100,
|
||||
image: CachedImageProvider(e.url),
|
||||
),
|
||||
);
|
||||
if (e.link != null) {
|
||||
image = InkWell(
|
||||
onTap: () {
|
||||
_Tag.handleLink(e.link!);
|
||||
},
|
||||
child: image,
|
||||
);
|
||||
}
|
||||
return image;
|
||||
}).toList(),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user