mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
802 lines
22 KiB
Dart
802 lines
22 KiB
Dart
import 'dart:collection';
|
|
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
|
import 'package:venera/components/components.dart';
|
|
import 'package:venera/foundation/app.dart';
|
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
|
import 'package:venera/foundation/image_provider/cached_image.dart';
|
|
import 'package:venera/utils/app_links.dart';
|
|
import 'package:venera/utils/ext.dart';
|
|
import 'package:venera/utils/translations.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;
|
|
}
|
|
}
|