mirror of
https://github.com/venera-app/venera.git
synced 2025-12-15 14:41:15 +00:00
589 lines
17 KiB
Dart
589 lines
17 KiB
Dart
part of 'reader.dart';
|
|
|
|
bool _shouldBlockComment(Comment comment) {
|
|
var blockedWords = appdata.settings["blockedCommentWords"] as List;
|
|
if (blockedWords.isEmpty) return false;
|
|
|
|
var content = comment.content.toLowerCase();
|
|
for (var word in blockedWords) {
|
|
if (content.contains(word.toString().toLowerCase())) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
class ChapterCommentsPage extends StatefulWidget {
|
|
const ChapterCommentsPage({
|
|
super.key,
|
|
required this.comicId,
|
|
required this.epId,
|
|
required this.source,
|
|
required this.comicTitle,
|
|
required this.chapterTitle,
|
|
this.replyComment,
|
|
});
|
|
|
|
final String comicId;
|
|
final String epId;
|
|
final ComicSource source;
|
|
final String comicTitle;
|
|
final String chapterTitle;
|
|
final Comment? replyComment;
|
|
|
|
@override
|
|
State<ChapterCommentsPage> createState() => _ChapterCommentsPageState();
|
|
}
|
|
|
|
class _ChapterCommentsPageState extends State<ChapterCommentsPage> {
|
|
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.chapterCommentsLoader!(
|
|
widget.comicId,
|
|
widget.epId,
|
|
1,
|
|
widget.replyComment?.id,
|
|
);
|
|
if (res.error) {
|
|
setState(() {
|
|
_error = res.errorMessage;
|
|
_loading = false;
|
|
});
|
|
} else if (mounted) {
|
|
var filteredComments = res.data.where((c) => !_shouldBlockComment(c)).toList();
|
|
setState(() {
|
|
_comments = filteredComments;
|
|
_loading = false;
|
|
maxPage = res.subData;
|
|
});
|
|
}
|
|
}
|
|
|
|
void loadMore() async {
|
|
var res = await widget.source.chapterCommentsLoader!(
|
|
widget.comicId,
|
|
widget.epId,
|
|
_page + 1,
|
|
widget.replyComment?.id,
|
|
);
|
|
if (res.error) {
|
|
context.showMessage(message: res.errorMessage ?? "Unknown Error");
|
|
} else {
|
|
var filteredComments = res.data.where((c) => !_shouldBlockComment(c)).toList();
|
|
setState(() {
|
|
_comments!.addAll(filteredComments);
|
|
_page++;
|
|
if (maxPage == null && res.data.isEmpty) {
|
|
maxPage = _page;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
resizeToAvoidBottomInset: false,
|
|
appBar: Appbar(
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text("Chapter Comments".tl, style: ts.s18),
|
|
Text(widget.chapterTitle, style: ts.s12),
|
|
],
|
|
),
|
|
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: SmoothScrollProvider(
|
|
builder: (context, controller, physics) {
|
|
return ListView.builder(
|
|
controller: controller,
|
|
physics: physics,
|
|
primary: false,
|
|
padding: EdgeInsets.zero,
|
|
itemCount: _comments!.length + 2,
|
|
itemBuilder: (context, index) {
|
|
if (index == 0) {
|
|
if (widget.replyComment != null) {
|
|
return Column(
|
|
children: [
|
|
_ChapterCommentTile(
|
|
comment: widget.replyComment!,
|
|
source: widget.source,
|
|
comicId: widget.comicId,
|
|
epId: widget.epId,
|
|
showAvatar: showAvatar,
|
|
showActions: false,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
alignment: Alignment.centerLeft,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
top: BorderSide(
|
|
color: context.colorScheme.outlineVariant,
|
|
width: 0.6,
|
|
),
|
|
),
|
|
),
|
|
child: Text("Replies".tl, style: ts.s18),
|
|
),
|
|
],
|
|
);
|
|
} else {
|
|
return const SizedBox();
|
|
}
|
|
}
|
|
index--;
|
|
|
|
if (index == _comments!.length) {
|
|
if (_page < (maxPage ?? _page + 1)) {
|
|
loadMore();
|
|
return const ListLoadingIndicator();
|
|
} else {
|
|
return const SizedBox();
|
|
}
|
|
}
|
|
|
|
return _ChapterCommentTile(
|
|
comment: _comments![index],
|
|
source: widget.source,
|
|
comicId: widget.comicId,
|
|
epId: widget.epId,
|
|
showAvatar: showAvatar,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
buildBottom(context),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget buildBottom(BuildContext context) {
|
|
if (widget.source.sendChapterCommentFunc == null) {
|
|
return const SizedBox(height: 0);
|
|
}
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
border: Border(
|
|
top: BorderSide(
|
|
color: context.colorScheme.outlineVariant,
|
|
width: 0.6,
|
|
),
|
|
),
|
|
),
|
|
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),
|
|
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.sendChapterCommentFunc!(
|
|
widget.comicId,
|
|
widget.epId,
|
|
controller.text,
|
|
widget.replyComment?.id,
|
|
);
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
).paddingLeft(16).paddingRight(4),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ChapterCommentTile extends StatefulWidget {
|
|
const _ChapterCommentTile({
|
|
required this.comment,
|
|
required this.source,
|
|
required this.comicId,
|
|
required this.epId,
|
|
required this.showAvatar,
|
|
this.showActions = true,
|
|
});
|
|
|
|
final Comment comment;
|
|
final ComicSource source;
|
|
final String comicId;
|
|
final String epId;
|
|
final bool showAvatar;
|
|
final bool showActions;
|
|
|
|
@override
|
|
State<_ChapterCommentTile> createState() => _ChapterCommentTileState();
|
|
}
|
|
|
|
class _ChapterCommentTileState extends State<_ChapterCommentTile> {
|
|
@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(
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (widget.showAvatar)
|
|
Container(
|
|
width: 36,
|
|
height: 36,
|
|
clipBehavior: Clip.antiAlias,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(18),
|
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
|
),
|
|
child: widget.comment.avatar == null
|
|
? null
|
|
: AnimatedImage(
|
|
image: CachedImageProvider(
|
|
widget.comment.avatar!,
|
|
sourceKey: widget.source.key,
|
|
),
|
|
),
|
|
).paddingRight(8),
|
|
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(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildActions() {
|
|
if (!widget.showActions) {
|
|
return const SizedBox();
|
|
}
|
|
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(),
|
|
// Only show reply button if comment has both id and replyCount
|
|
if (widget.comment.replyCount != null && widget.comment.id != 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: () {
|
|
// Get the parent page's widget to access comicTitle and chapterTitle
|
|
var parentState = context.findAncestorStateOfType<_ChapterCommentsPageState>();
|
|
showSideBar(
|
|
context,
|
|
ChapterCommentsPage(
|
|
comicId: widget.comicId,
|
|
epId: widget.epId,
|
|
source: widget.source,
|
|
comicTitle: parentState?.widget.comicTitle ?? '',
|
|
chapterTitle: parentState?.widget.chapterTitle ?? '',
|
|
replyComment: widget.comment,
|
|
),
|
|
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.comicId,
|
|
widget.epId,
|
|
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.comicId,
|
|
widget.epId,
|
|
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 {
|
|
// Use the RichCommentContent from comments_page.dart
|
|
// For simplicity, we'll just show plain text here
|
|
// In a real implementation, you'd need to import or duplicate the RichCommentContent class
|
|
return SelectableText(text);
|
|
}
|
|
}
|
|
}
|