mirror of
https://github.com/venera-app/venera.git
synced 2025-12-15 06:41:14 +00:00
@@ -383,6 +383,8 @@
|
||||
"Continuous": "连续",
|
||||
"Display mode of comic list": "漫画列表的显示模式",
|
||||
"Show Page Number": "显示页码",
|
||||
"Show Chapter Comments": "显示章节评论",
|
||||
"Chapter Comments": "章节评论",
|
||||
"Jump to page": "跳转到页面",
|
||||
"Page": "页面",
|
||||
"Jump": "跳转",
|
||||
@@ -804,6 +806,8 @@
|
||||
"Continuous": "連續",
|
||||
"Display mode of comic list": "漫畫列表的顯示模式",
|
||||
"Show Page Number": "顯示頁碼",
|
||||
"Show Chapter Comments": "顯示章節評論",
|
||||
"Chapter Comments": "章節評論",
|
||||
"Jump to page": "跳轉到頁面",
|
||||
"Page": "頁面",
|
||||
"Jump": "跳轉",
|
||||
|
||||
@@ -553,6 +553,51 @@ If `load` function is implemented, `loadNext` function will be ignored.
|
||||
*/
|
||||
sendComment: async (comicId, subId, content, replyTo) => {
|
||||
|
||||
},
|
||||
/**
|
||||
* [Optional] load chapter comments
|
||||
*
|
||||
* Chapter comments are displayed in the reader.
|
||||
* Same rich text support as loadComments.
|
||||
*
|
||||
* Note: To control reply functionality:
|
||||
* - If a comment does not support replies, set its `id` to null/undefined
|
||||
* - Or set its `replyCount` to null/undefined
|
||||
* - The reply button will only show when both `id` and `replyCount` are present
|
||||
*
|
||||
* @param comicId {string}
|
||||
* @param epId {string} - chapter id
|
||||
* @param page {number}
|
||||
* @param replyTo {string?} - commentId to reply, not null when reply to a comment
|
||||
* @returns {Promise<{comments: Comment[], maxPage: number?}>}
|
||||
*
|
||||
* @example
|
||||
* // Example for comments without reply support:
|
||||
* return {
|
||||
* comments: data.list.map(e => ({
|
||||
* userName: e.user_name,
|
||||
* avatar: e.user_avatar,
|
||||
* content: e.comment,
|
||||
* time: e.create_at,
|
||||
* replyCount: null, // or undefined - no reply support
|
||||
* id: null, // or undefined - no reply support
|
||||
* })),
|
||||
* maxPage: Math.ceil(total / 20)
|
||||
* }
|
||||
*/
|
||||
loadChapterComments: async (comicId, epId, page, replyTo) => {
|
||||
|
||||
},
|
||||
/**
|
||||
* [Optional] send a chapter comment, return any value to indicate success
|
||||
* @param comicId {string}
|
||||
* @param epId {string} - chapter id
|
||||
* @param content {string}
|
||||
* @param replyTo {string?} - commentId to reply, not null when reply to a comment
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
sendChapterComment: async (comicId, epId, content, replyTo) => {
|
||||
|
||||
},
|
||||
/**
|
||||
* [Optional] like or unlike a comment
|
||||
|
||||
@@ -194,6 +194,7 @@ class Settings with ChangeNotifier {
|
||||
'readerScrollSpeed': 1.0, // 0.5 - 3.0
|
||||
'localFavoritesFirst': true,
|
||||
'autoCloseFavoritePanel': false,
|
||||
'showChapterComments': true, // show chapter comments in reader
|
||||
};
|
||||
|
||||
operator [](String key) {
|
||||
@@ -207,7 +208,11 @@ class Settings with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void setEnabledComicSpecificSettings(String comicId, String sourceKey, bool enabled) {
|
||||
void setEnabledComicSpecificSettings(
|
||||
String comicId,
|
||||
String sourceKey,
|
||||
bool enabled,
|
||||
) {
|
||||
setReaderSetting(comicId, sourceKey, "enabled", enabled);
|
||||
}
|
||||
|
||||
@@ -215,7 +220,8 @@ class Settings with ChangeNotifier {
|
||||
if (comicId == null || sourceKey == null) {
|
||||
return false;
|
||||
}
|
||||
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] == true;
|
||||
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] ==
|
||||
true;
|
||||
}
|
||||
|
||||
dynamic getReaderSetting(String comicId, String sourceKey, String key) {
|
||||
|
||||
@@ -61,8 +61,10 @@ class ComicSourceManager with ChangeNotifier, Init {
|
||||
await for (var entity in Directory(path).list()) {
|
||||
if (entity is File && entity.path.endsWith(".js")) {
|
||||
try {
|
||||
var source = await ComicSourceParser()
|
||||
.parse(await entity.readAsString(), entity.absolute.path);
|
||||
var source = await ComicSourceParser().parse(
|
||||
await entity.readAsString(),
|
||||
entity.absolute.path,
|
||||
);
|
||||
_sources.add(source);
|
||||
} catch (e, s) {
|
||||
Log.error("ComicSource", "$e\n$s");
|
||||
@@ -154,7 +156,7 @@ class ComicSource {
|
||||
final GetImageLoadingConfigFunc? getImageLoadingConfig;
|
||||
|
||||
final Map<String, dynamic> Function(String imageKey)?
|
||||
getThumbnailLoadingConfig;
|
||||
getThumbnailLoadingConfig;
|
||||
|
||||
var data = <String, dynamic>{};
|
||||
|
||||
@@ -170,6 +172,10 @@ class ComicSource {
|
||||
|
||||
final SendCommentFunc? sendCommentFunc;
|
||||
|
||||
final ChapterCommentsLoader? chapterCommentsLoader;
|
||||
|
||||
final SendChapterCommentFunc? sendChapterCommentFunc;
|
||||
|
||||
final RegExp? idMatcher;
|
||||
|
||||
final LikeOrUnlikeComicFunc? likeOrUnlikeComic;
|
||||
@@ -256,6 +262,8 @@ class ComicSource {
|
||||
this.version,
|
||||
this.commentsLoader,
|
||||
this.sendCommentFunc,
|
||||
this.chapterCommentsLoader,
|
||||
this.sendChapterCommentFunc,
|
||||
this.likeOrUnlikeComic,
|
||||
this.voteCommentFunc,
|
||||
this.likeCommentFunc,
|
||||
@@ -367,11 +375,19 @@ enum ExplorePageType {
|
||||
override,
|
||||
}
|
||||
|
||||
typedef SearchFunction = Future<Res<List<Comic>>> Function(
|
||||
String keyword, int page, List<String> searchOption);
|
||||
typedef SearchFunction =
|
||||
Future<Res<List<Comic>>> Function(
|
||||
String keyword,
|
||||
int page,
|
||||
List<String> searchOption,
|
||||
);
|
||||
|
||||
typedef SearchNextFunction = Future<Res<List<Comic>>> Function(
|
||||
String keyword, String? next, List<String> searchOption);
|
||||
typedef SearchNextFunction =
|
||||
Future<Res<List<Comic>>> Function(
|
||||
String keyword,
|
||||
String? next,
|
||||
List<String> searchOption,
|
||||
);
|
||||
|
||||
class SearchPageData {
|
||||
/// If this is not null, the default value of search options will be first element.
|
||||
@@ -398,11 +414,19 @@ class SearchOptions {
|
||||
String get defaultValue => defaultVal ?? options.keys.firstOrNull ?? "";
|
||||
}
|
||||
|
||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
||||
String category, String? param, List<String> options, int page);
|
||||
typedef CategoryComicsLoader =
|
||||
Future<Res<List<Comic>>> Function(
|
||||
String category,
|
||||
String? param,
|
||||
List<String> options,
|
||||
int page,
|
||||
);
|
||||
|
||||
typedef CategoryOptionsLoader = Future<Res<List<CategoryComicsOptions>>> Function(
|
||||
String category, String? param);
|
||||
typedef CategoryOptionsLoader =
|
||||
Future<Res<List<CategoryComicsOptions>>> Function(
|
||||
String category,
|
||||
String? param,
|
||||
);
|
||||
|
||||
class CategoryComicsData {
|
||||
/// options
|
||||
@@ -419,7 +443,12 @@ class CategoryComicsData {
|
||||
|
||||
final RankingData? rankingData;
|
||||
|
||||
const CategoryComicsData({this.options, this.optionsLoader, required this.load, this.rankingData});
|
||||
const CategoryComicsData({
|
||||
this.options,
|
||||
this.optionsLoader,
|
||||
required this.load,
|
||||
this.rankingData,
|
||||
});
|
||||
}
|
||||
|
||||
class RankingData {
|
||||
@@ -428,7 +457,7 @@ class RankingData {
|
||||
final Future<Res<List<Comic>>> Function(String option, int page)? load;
|
||||
|
||||
final Future<Res<List<Comic>>> Function(String option, String? next)?
|
||||
loadWithNext;
|
||||
loadWithNext;
|
||||
|
||||
const RankingData(this.options, this.load, this.loadWithNext);
|
||||
}
|
||||
@@ -447,7 +476,12 @@ class CategoryComicsOptions {
|
||||
|
||||
final List<String>? showWhen;
|
||||
|
||||
const CategoryComicsOptions(this.label, this.options, this.notShowWhen, this.showWhen);
|
||||
const CategoryComicsOptions(
|
||||
this.label,
|
||||
this.options,
|
||||
this.notShowWhen,
|
||||
this.showWhen,
|
||||
);
|
||||
}
|
||||
|
||||
class LinkHandler {
|
||||
|
||||
@@ -151,6 +151,8 @@ class ComicSourceParser {
|
||||
version ?? "1.0.0",
|
||||
_parseCommentsLoader(),
|
||||
_parseSendCommentFunc(),
|
||||
_parseChapterCommentsLoader(),
|
||||
_parseSendChapterCommentFunc(),
|
||||
_parseLikeFunc(),
|
||||
_parseVoteCommentFunc(),
|
||||
_parseLikeCommentFunc(),
|
||||
@@ -560,12 +562,16 @@ class ComicSourceParser {
|
||||
res = await res;
|
||||
}
|
||||
if (res is! List) {
|
||||
return Res.error("Invalid data:\nExpected: List\nGot: ${res.runtimeType}");
|
||||
return Res.error(
|
||||
"Invalid data:\nExpected: List\nGot: ${res.runtimeType}",
|
||||
);
|
||||
}
|
||||
var options = <CategoryComicsOptions>[];
|
||||
for (var element in res) {
|
||||
if (element is! Map) {
|
||||
return Res.error("Invalid option data:\nExpected: Map\nGot: ${element.runtimeType}");
|
||||
return Res.error(
|
||||
"Invalid option data:\nExpected: Map\nGot: ${element.runtimeType}",
|
||||
);
|
||||
}
|
||||
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
||||
for (var option in element["options"] ?? []) {
|
||||
@@ -582,13 +588,14 @@ class ComicSourceParser {
|
||||
element["label"] ?? "",
|
||||
map,
|
||||
List.from(element["notShowWhen"] ?? []),
|
||||
element["showWhen"] == null ? null : List.from(element["showWhen"]),
|
||||
element["showWhen"] == null
|
||||
? null
|
||||
: List.from(element["showWhen"]),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Res(options);
|
||||
}
|
||||
catch(e) {
|
||||
} catch (e) {
|
||||
Log.error("Data Analysis", "Failed to load category options.\n$e");
|
||||
return Res.error(e.toString());
|
||||
}
|
||||
@@ -1005,6 +1012,54 @@ class ComicSourceParser {
|
||||
};
|
||||
}
|
||||
|
||||
ChapterCommentsLoader? _parseChapterCommentsLoader() {
|
||||
if (!_checkExists("comic.loadChapterComments")) return null;
|
||||
return (comicId, epId, page, replyTo) async {
|
||||
try {
|
||||
var res = await JsEngine().runCode("""
|
||||
ComicSource.sources.$_key.comic.loadChapterComments(
|
||||
${jsonEncode(comicId)}, ${jsonEncode(epId)}, ${jsonEncode(page)}, ${jsonEncode(replyTo)})
|
||||
""");
|
||||
return Res(
|
||||
(res["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
||||
subData: res["maxPage"],
|
||||
);
|
||||
} catch (e, s) {
|
||||
Log.error("Network", "$e\n$s");
|
||||
return Res.error(e.toString());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SendChapterCommentFunc? _parseSendChapterCommentFunc() {
|
||||
if (!_checkExists("comic.sendChapterComment")) return null;
|
||||
return (comicId, epId, content, replyTo) async {
|
||||
Future<Res<bool>> func() async {
|
||||
try {
|
||||
await JsEngine().runCode("""
|
||||
ComicSource.sources.$_key.comic.sendChapterComment(
|
||||
${jsonEncode(comicId)}, ${jsonEncode(epId)}, ${jsonEncode(content)}, ${jsonEncode(replyTo)})
|
||||
""");
|
||||
return const Res(true);
|
||||
} catch (e, s) {
|
||||
Log.error("Network", "$e\n$s");
|
||||
return Res.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
var res = await func();
|
||||
if (res.error && res.errorMessage!.contains("Login expired")) {
|
||||
var reLoginRes = await ComicSource.find(_key!)!.reLogin();
|
||||
if (!reLoginRes) {
|
||||
return const Res.error("Login expired and re-login failed");
|
||||
} else {
|
||||
return func();
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
GetImageLoadingConfigFunc? _parseImageLoadingConfigFunc() {
|
||||
if (!_checkExists("comic.onImageLoad")) {
|
||||
return null;
|
||||
|
||||
@@ -4,50 +4,90 @@ part of 'comic_source.dart';
|
||||
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
||||
|
||||
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
|
||||
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
|
||||
String? next);
|
||||
typedef ComicListBuilderWithNext =
|
||||
Future<Res<List<Comic>>> Function(String? next);
|
||||
|
||||
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
||||
|
||||
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
||||
|
||||
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
|
||||
String id, String? ep);
|
||||
typedef LoadComicPagesFunc =
|
||||
Future<Res<List<String>>> Function(String id, String? ep);
|
||||
|
||||
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
|
||||
String id, String? subId, int page, String? replyTo);
|
||||
typedef CommentsLoader =
|
||||
Future<Res<List<Comment>>> Function(
|
||||
String id,
|
||||
String? subId,
|
||||
int page,
|
||||
String? replyTo,
|
||||
);
|
||||
|
||||
typedef SendCommentFunc = Future<Res<bool>> Function(
|
||||
String id, String? subId, String content, String? replyTo);
|
||||
typedef ChapterCommentsLoader =
|
||||
Future<Res<List<Comment>>> Function(
|
||||
String comicId,
|
||||
String epId,
|
||||
int page,
|
||||
String? replyTo,
|
||||
);
|
||||
|
||||
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
|
||||
String imageKey, String comicId, String epId)?;
|
||||
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
||||
String imageKey)?;
|
||||
typedef SendCommentFunc =
|
||||
Future<Res<bool>> Function(
|
||||
String id,
|
||||
String? subId,
|
||||
String content,
|
||||
String? replyTo,
|
||||
);
|
||||
|
||||
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
|
||||
String comicId, String? next);
|
||||
typedef SendChapterCommentFunc =
|
||||
Future<Res<bool>> Function(
|
||||
String comicId,
|
||||
String epId,
|
||||
String content,
|
||||
String? replyTo,
|
||||
);
|
||||
|
||||
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
|
||||
String comicId, bool isLiking);
|
||||
typedef GetImageLoadingConfigFunc =
|
||||
Future<Map<String, dynamic>> Function(
|
||||
String imageKey,
|
||||
String comicId,
|
||||
String epId,
|
||||
)?;
|
||||
typedef GetThumbnailLoadingConfigFunc =
|
||||
Map<String, dynamic> Function(String imageKey)?;
|
||||
|
||||
typedef ComicThumbnailLoader =
|
||||
Future<Res<List<String>>> Function(String comicId, String? next);
|
||||
|
||||
typedef LikeOrUnlikeComicFunc =
|
||||
Future<Res<bool>> Function(String comicId, bool isLiking);
|
||||
|
||||
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
||||
/// return the new likes count or null.
|
||||
typedef LikeCommentFunc = Future<Res<int?>> Function(
|
||||
String comicId, String? subId, String commentId, bool isLiking);
|
||||
typedef LikeCommentFunc =
|
||||
Future<Res<int?>> Function(
|
||||
String comicId,
|
||||
String? subId,
|
||||
String commentId,
|
||||
bool isLiking,
|
||||
);
|
||||
|
||||
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
||||
/// return the new vote count or null.
|
||||
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
||||
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
||||
typedef VoteCommentFunc =
|
||||
Future<Res<int?>> Function(
|
||||
String comicId,
|
||||
String? subId,
|
||||
String commentId,
|
||||
bool isUp,
|
||||
bool isCancel,
|
||||
);
|
||||
|
||||
typedef HandleClickTagEvent = PageJumpTarget? Function(
|
||||
String namespace, String tag);
|
||||
typedef HandleClickTagEvent =
|
||||
PageJumpTarget? Function(String namespace, String tag);
|
||||
|
||||
/// Handle tag suggestion selection event. Should return the text to insert
|
||||
/// into the search field.
|
||||
typedef TagSuggestionSelectFunc = String Function(
|
||||
String namespace, String tag);
|
||||
typedef TagSuggestionSelectFunc = String Function(String namespace, String tag);
|
||||
|
||||
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
||||
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
|
||||
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
|
||||
|
||||
573
lib/pages/reader/chapter_comments.dart
Normal file
573
lib/pages/reader/chapter_comments.dart
Normal file
@@ -0,0 +1,573 @@
|
||||
part of 'reader.dart';
|
||||
|
||||
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) {
|
||||
setState(() {
|
||||
_comments = res.data;
|
||||
_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 {
|
||||
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: 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import 'package:venera/foundation/consts.dart';
|
||||
import 'package:venera/foundation/favorites.dart';
|
||||
import 'package:venera/foundation/global_state.dart';
|
||||
import 'package:venera/foundation/history.dart';
|
||||
import 'package:venera/foundation/image_provider/cached_image.dart';
|
||||
import 'package:venera/foundation/image_provider/reader_image.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
@@ -54,6 +55,8 @@ part 'loading.dart';
|
||||
|
||||
part 'chapters.dart';
|
||||
|
||||
part 'chapter_comments.dart';
|
||||
|
||||
extension _ReaderContext on BuildContext {
|
||||
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
|
||||
|
||||
@@ -168,12 +171,22 @@ class _ReaderState extends State<Reader>
|
||||
}
|
||||
}
|
||||
// mode = ReaderMode.fromKey(appdata.settings['readerMode']);
|
||||
mode = ReaderMode.fromKey(appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerMode'));
|
||||
mode = ReaderMode.fromKey(
|
||||
appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerMode'),
|
||||
);
|
||||
history = widget.history;
|
||||
if (!appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSystemStatusBar')) {
|
||||
if (!appdata.settings.getReaderSetting(
|
||||
cid,
|
||||
type.sourceKey,
|
||||
'showSystemStatusBar',
|
||||
)) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
}
|
||||
if (appdata.settings.getReaderSetting(cid, type.sourceKey, 'enableTurnPageByVolumeKey')) {
|
||||
if (appdata.settings.getReaderSetting(
|
||||
cid,
|
||||
type.sourceKey,
|
||||
'enableTurnPageByVolumeKey',
|
||||
)) {
|
||||
handleVolumeEvent();
|
||||
}
|
||||
setImageCacheSize();
|
||||
@@ -211,8 +224,10 @@ class _ReaderState extends State<Reader>
|
||||
} else {
|
||||
maxImageCacheSize = 500 << 20;
|
||||
}
|
||||
Log.info("Reader",
|
||||
"Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize");
|
||||
Log.info(
|
||||
"Reader",
|
||||
"Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize",
|
||||
);
|
||||
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
|
||||
}
|
||||
|
||||
@@ -242,13 +257,15 @@ class _ReaderState extends State<Reader>
|
||||
onKeyEvent: onKeyEvent,
|
||||
child: Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
return _ReaderScaffold(
|
||||
child: _ReaderGestureDetector(
|
||||
child: _ReaderImages(key: Key(chapter.toString())),
|
||||
),
|
||||
);
|
||||
})
|
||||
OverlayEntry(
|
||||
builder: (context) {
|
||||
return _ReaderScaffold(
|
||||
child: _ReaderGestureDetector(
|
||||
child: _ReaderImages(key: Key(chapter.toString())),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -385,16 +402,29 @@ abstract mixin class _ImagePerPageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
bool showSingleImageOnFirstPage() =>
|
||||
appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSingleImageOnFirstPage');
|
||||
bool showSingleImageOnFirstPage() => appdata.settings.getReaderSetting(
|
||||
cid,
|
||||
type.sourceKey,
|
||||
'showSingleImageOnFirstPage',
|
||||
);
|
||||
|
||||
/// The number of images displayed on one screen
|
||||
int get imagesPerPage {
|
||||
if (mode.isContinuous) return 1;
|
||||
if (isPortrait) {
|
||||
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForPortrait') ?? 1;
|
||||
return appdata.settings.getReaderSetting(
|
||||
cid,
|
||||
type.sourceKey,
|
||||
'readerScreenPicNumberForPortrait',
|
||||
) ??
|
||||
1;
|
||||
} else {
|
||||
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForLandscape') ?? 1;
|
||||
return appdata.settings.getReaderSetting(
|
||||
cid,
|
||||
type.sourceKey,
|
||||
'readerScreenPicNumberForLandscape',
|
||||
) ??
|
||||
1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,15 +433,22 @@ abstract mixin class _ImagePerPageHandler {
|
||||
int currentImagesPerPage = imagesPerPage;
|
||||
bool currentOrientation = isPortrait;
|
||||
|
||||
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {
|
||||
_adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
|
||||
if (_lastImagesPerPage != currentImagesPerPage ||
|
||||
_lastOrientation != currentOrientation) {
|
||||
_adjustPageForImagesPerPageChange(
|
||||
_lastImagesPerPage,
|
||||
currentImagesPerPage,
|
||||
);
|
||||
_lastImagesPerPage = currentImagesPerPage;
|
||||
_lastOrientation = currentOrientation;
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust the page number when the number of images per page changes
|
||||
void _adjustPageForImagesPerPageChange(int oldImagesPerPage, int newImagesPerPage) {
|
||||
void _adjustPageForImagesPerPageChange(
|
||||
int oldImagesPerPage,
|
||||
int newImagesPerPage,
|
||||
) {
|
||||
int previousImageIndex = 1;
|
||||
if (!showSingleImageOnFirstPage() || oldImagesPerPage == 1) {
|
||||
previousImageIndex = (page - 1) * oldImagesPerPage + 1;
|
||||
@@ -434,7 +471,7 @@ abstract mixin class _ImagePerPageHandler {
|
||||
newPage = previousImageIndex;
|
||||
}
|
||||
|
||||
page = newPage>0 ? newPage : 1;
|
||||
page = newPage > 0 ? newPage : 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,10 +506,7 @@ abstract mixin class _VolumeListener {
|
||||
if (volumeListener != null) {
|
||||
volumeListener?.cancel();
|
||||
}
|
||||
volumeListener = VolumeListener(
|
||||
onDown: onDown,
|
||||
onUp: onUp,
|
||||
)..listen();
|
||||
volumeListener = VolumeListener(onDown: onDown, onUp: onUp)..listen();
|
||||
}
|
||||
|
||||
void stopVolumeEvent() {
|
||||
@@ -507,7 +541,8 @@ abstract mixin class _ReaderLocation {
|
||||
|
||||
void update();
|
||||
|
||||
bool enablePageAnimation(String cid, ComicType type) => appdata.settings.getReaderSetting(cid, type.sourceKey, 'enablePageAnimation');
|
||||
bool enablePageAnimation(String cid, ComicType type) => appdata.settings
|
||||
.getReaderSetting(cid, type.sourceKey, 'enablePageAnimation');
|
||||
|
||||
_ImageViewController? _imageViewController;
|
||||
|
||||
@@ -588,7 +623,11 @@ abstract mixin class _ReaderLocation {
|
||||
autoPageTurningTimer!.cancel();
|
||||
autoPageTurningTimer = null;
|
||||
} else {
|
||||
int interval = appdata.settings.getReaderSetting(cid, type.sourceKey, 'autoPageTurningInterval');
|
||||
int interval = appdata.settings.getReaderSetting(
|
||||
cid,
|
||||
type.sourceKey,
|
||||
'autoPageTurningInterval',
|
||||
);
|
||||
autoPageTurningTimer = Timer.periodic(Duration(seconds: interval), (_) {
|
||||
if (page == maxPage) {
|
||||
autoPageTurningTimer!.cancel();
|
||||
|
||||
@@ -183,6 +183,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (shouldShowChapterComments())
|
||||
Tooltip(
|
||||
message: "Chapter Comments".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.comment),
|
||||
onPressed: openChapterComments,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Settings".tl,
|
||||
child: IconButton(
|
||||
@@ -605,7 +613,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
}
|
||||
var (imageIndex, data) = result;
|
||||
var fileType = detectFileType(data);
|
||||
var filename = "${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
|
||||
var filename =
|
||||
"${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
|
||||
saveFile(data: data, filename: filename);
|
||||
}
|
||||
|
||||
@@ -616,7 +625,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
}
|
||||
var (imageIndex, data) = result;
|
||||
var fileType = detectFileType(data);
|
||||
var filename = "${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
|
||||
var filename =
|
||||
"${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
|
||||
Share.shareFile(data: data, filename: filename, mime: fileType.mime);
|
||||
}
|
||||
|
||||
@@ -650,6 +660,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
if (key == "quickCollectImage") {
|
||||
addDragListener();
|
||||
}
|
||||
if (key == "showChapterComments") {
|
||||
update();
|
||||
}
|
||||
context.reader.update();
|
||||
},
|
||||
),
|
||||
@@ -657,6 +670,48 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
);
|
||||
}
|
||||
|
||||
bool shouldShowChapterComments() {
|
||||
// Check if chapters exist
|
||||
if (context.reader.widget.chapters == null) return false;
|
||||
|
||||
// Check if setting is enabled
|
||||
var showChapterComments = appdata.settings.getReaderSetting(
|
||||
context.reader.cid,
|
||||
context.reader.type.sourceKey,
|
||||
'showChapterComments',
|
||||
);
|
||||
if (showChapterComments != true) return false;
|
||||
|
||||
// Check if comic source supports chapter comments
|
||||
var source = ComicSource.find(context.reader.type.sourceKey);
|
||||
if (source == null || source.chapterCommentsLoader == null) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void openChapterComments() {
|
||||
var source = ComicSource.find(context.reader.type.sourceKey);
|
||||
if (source == null) return;
|
||||
|
||||
var chapters = context.reader.widget.chapters;
|
||||
if (chapters == null) return;
|
||||
|
||||
var chapterIndex = context.reader.chapter - 1;
|
||||
var epId = chapters.ids.elementAt(chapterIndex);
|
||||
var chapterTitle = chapters.titles.elementAt(chapterIndex);
|
||||
|
||||
showSideBar(
|
||||
context,
|
||||
ChapterCommentsPage(
|
||||
comicId: context.reader.cid,
|
||||
epId: epId,
|
||||
source: source,
|
||||
comicTitle: context.reader.widget.name,
|
||||
chapterTitle: chapterTitle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEpChangeButton() {
|
||||
if (context.reader.widget.chapters == null) return const SizedBox();
|
||||
switch (showFloatingButtonValue) {
|
||||
|
||||
@@ -303,6 +303,15 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Show Chapter Comments".tl,
|
||||
settingKey: "showChapterComments",
|
||||
onChanged: () {
|
||||
widget.onChanged?.call("showChapterComments");
|
||||
},
|
||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||
).toSliver(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user