diff --git a/assets/init.js b/assets/init.js index 755df9b..8ea0e8b 100644 --- a/assets/init.js +++ b/assets/init.js @@ -880,8 +880,8 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage * @param cover {string} * @param description {string?} * @param tags {Map | {} | null | undefined} - * @param chapters {Map | {} | null | undefined}} - key: chapter id, value: chapter title - * @param isFavorite {boolean | null | undefined}} - favorite status. If the comic source supports multiple folders, this field should be null + * @param chapters {Map | {} | null | undefined} - key: chapter id, value: chapter title + * @param isFavorite {boolean | null | undefined} - favorite status. If the comic source supports multiple folders, this field should be null * @param subId {string?} - a param which is passed to comments api * @param thumbnails {string[]?} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails * @param recommend {Comic[]?} - related comics @@ -894,9 +894,10 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage * @param url {string?} * @param stars {number?} - 0-5, double * @param maxPage {number?} + * @param comments {Comment[]?}- `since 1.0.7` App will display comments in the details page. * @constructor */ -function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url, stars, maxPage}) { +function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url, stars, maxPage, comments}) { this.title = title; this.cover = cover; this.description = description; @@ -915,6 +916,7 @@ function ComicDetails({title, cover, description, tags, chapters, isFavorite, su this.url = url; this.stars = stars; this.maxPage = maxPage; + this.comments = comments; } /** diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 042682b..80843d0 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -160,6 +160,8 @@ class ComicDetails with HistoryMixin { @override final int? maxPage; + final List? comments; + static Map> _generateMap(Map map) { var res = >{}; map.forEach((key, value) { @@ -193,7 +195,10 @@ class ComicDetails with HistoryMixin { updateTime = json["updateTime"], url = json["url"], stars = (json["stars"] as num?)?.toDouble(), - maxPage = json["maxPage"]; + maxPage = json["maxPage"], + comments = (json["comments"] as List?) + ?.map((e) => Comment.fromJson(e)) + .toList(); Map toJson() { return { diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 081ad0f..6e8f68a 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -115,6 +115,7 @@ class _ComicPageState extends LoadingState buildDescription(), buildInfo(), buildChapters(), + buildComments(), buildThumbnails(), buildRecommend(), SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)), @@ -287,7 +288,7 @@ class _ComicPageState extends LoadingState onLongPressed: quickFavorite, iconColor: context.useTextColor(Colors.purple), ), - if (comicSource.commentsLoader != null) + if (comicSource.commentsLoader != null && comic.comments == null) _ActionButton( icon: const Icon(Icons.comment), text: (comic.commentsCount ?? 'Comments'.tl).toString(), @@ -549,6 +550,16 @@ class _ComicPageState extends LoadingState SliverGridComics(comics: comic.recommend!), ]); } + + Widget buildComments() { + if (comic.comments == null || comic.comments!.isEmpty) { + return const SliverPadding(padding: EdgeInsets.zero); + } + return _CommentsPart( + comments: comic.comments!, + showMore: showComments, + ); + } } abstract mixin class _ComicPageActions { @@ -1670,3 +1681,148 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> { ); } } + +class _CommentsPart extends StatefulWidget { + const _CommentsPart({ + required this.comments, + required this.showMore, + }); + + final List comments; + + final void Function() showMore; + + @override + State<_CommentsPart> createState() => _CommentsPartState(); +} + +class _CommentsPartState extends State<_CommentsPart> { + final scrollController = ScrollController(); + + late List comments; + + @override + void initState() { + comments = widget.comments; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MultiSliver( + children: [ + SliverToBoxAdapter( + child: ListTile( + title: Text("Comments".tl), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: () { + scrollController.animateTo( + scrollController.position.pixels - 340, + duration: const Duration(milliseconds: 200), + curve: Curves.ease, + ); + }, + ), + IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: () { + scrollController.animateTo( + scrollController.position.pixels + 340, + duration: const Duration(milliseconds: 200), + curve: Curves.ease, + ); + }, + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 184, + child: ListView.builder( + controller: scrollController, + scrollDirection: Axis.horizontal, + itemCount: comments.length, + itemBuilder: (context, index) { + return _CommentWidget(comment: comments[index]); + }, + ), + ), + const SizedBox(height: 8), + _ActionButton( + icon: const Icon(Icons.comment), + text: "View more".tl, + onPressed: widget.showMore, + iconColor: context.useTextColor(Colors.green), + ).fixHeight(48).paddingRight(8).toAlign(Alignment.centerRight), + const SizedBox(height: 8), + ], + ), + ), + const SliverToBoxAdapter( + child: Divider(), + ), + ], + ); + } +} + +class _CommentWidget extends StatelessWidget { + const _CommentWidget({required this.comment}); + + final Comment comment; + + @override + Widget build(BuildContext context) { + return Container( + height: double.infinity, + margin: const EdgeInsets.fromLTRB(16, 8, 0, 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + width: 324, + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Row( + children: [ + if (comment.avatar != null) + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18), + color: context.colorScheme.surfaceContainer, + ), + clipBehavior: Clip.antiAlias, + child: Image( + image: CachedImageProvider(comment.avatar!), + width: 36, + height: 36, + fit: BoxFit.cover, + ), + ).paddingRight(8), + Text(comment.userName, style: ts.bold), + ], + ), + const SizedBox(height: 4), + Expanded( + child: RichCommentContent(text: comment.content).fixWidth(324), + ), + const SizedBox(height: 4), + if (comment.time != null) + Text(comment.time!, style: ts.s12).toAlign(Alignment.centerLeft), + ], + ), + ); + } +} diff --git a/lib/pages/comments_page.dart b/lib/pages/comments_page.dart index aa60868..74ad4f8 100644 --- a/lib/pages/comments_page.dart +++ b/lib/pages/comments_page.dart @@ -510,7 +510,7 @@ class _CommentContent extends StatelessWidget { if (!text.contains('<') && !text.contains('http')) { return SelectableText(text); } else { - return _RichCommentContent(text: text); + return RichCommentContent(text: text); } } } @@ -610,16 +610,16 @@ class _CommentImage { const _CommentImage(this.url, this.link); } -class _RichCommentContent extends StatefulWidget { - const _RichCommentContent({required this.text}); +class RichCommentContent extends StatefulWidget { + const RichCommentContent({super.key, required this.text}); final String text; @override - State<_RichCommentContent> createState() => _RichCommentContentState(); + State createState() => _RichCommentContentState(); } -class _RichCommentContentState extends State<_RichCommentContent> { +class _RichCommentContentState extends State { var textSpan = []; var images = <_CommentImage>[]; @@ -639,6 +639,8 @@ class _RichCommentContentState extends State<_RichCommentContent> { 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;