mirror of
https://github.com/venera-app/venera.git
synced 2025-12-15 14:41:15 +00:00
Chapter comments.
This commit is contained in:
@@ -379,6 +379,8 @@
|
|||||||
"Continuous": "连续",
|
"Continuous": "连续",
|
||||||
"Display mode of comic list": "漫画列表的显示模式",
|
"Display mode of comic list": "漫画列表的显示模式",
|
||||||
"Show Page Number": "显示页码",
|
"Show Page Number": "显示页码",
|
||||||
|
"Show Chapter Comments": "显示章节评论",
|
||||||
|
"Chapter Comments": "章节评论",
|
||||||
"Jump to page": "跳转到页面",
|
"Jump to page": "跳转到页面",
|
||||||
"Page": "页面",
|
"Page": "页面",
|
||||||
"Jump": "跳转",
|
"Jump": "跳转",
|
||||||
@@ -796,6 +798,8 @@
|
|||||||
"Continuous": "連續",
|
"Continuous": "連續",
|
||||||
"Display mode of comic list": "漫畫列表的顯示模式",
|
"Display mode of comic list": "漫畫列表的顯示模式",
|
||||||
"Show Page Number": "顯示頁碼",
|
"Show Page Number": "顯示頁碼",
|
||||||
|
"Show Chapter Comments": "顯示章節評論",
|
||||||
|
"Chapter Comments": "章節評論",
|
||||||
"Jump to page": "跳轉到頁面",
|
"Jump to page": "跳轉到頁面",
|
||||||
"Page": "頁面",
|
"Page": "頁面",
|
||||||
"Jump": "跳轉",
|
"Jump": "跳轉",
|
||||||
|
|||||||
@@ -553,6 +553,51 @@ If `load` function is implemented, `loadNext` function will be ignored.
|
|||||||
*/
|
*/
|
||||||
sendComment: async (comicId, subId, content, replyTo) => {
|
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
|
* [Optional] like or unlike a comment
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ class Settings with ChangeNotifier {
|
|||||||
'readerScrollSpeed': 1.0, // 0.5 - 3.0
|
'readerScrollSpeed': 1.0, // 0.5 - 3.0
|
||||||
'localFavoritesFirst': true,
|
'localFavoritesFirst': true,
|
||||||
'autoCloseFavoritePanel': false,
|
'autoCloseFavoritePanel': false,
|
||||||
|
'showChapterComments': true, // show chapter comments in reader
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
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);
|
setReaderSetting(comicId, sourceKey, "enabled", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +220,8 @@ class Settings with ChangeNotifier {
|
|||||||
if (comicId == null || sourceKey == null) {
|
if (comicId == null || sourceKey == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] == true;
|
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] ==
|
||||||
|
true;
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic getReaderSetting(String comicId, String sourceKey, String key) {
|
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()) {
|
await for (var entity in Directory(path).list()) {
|
||||||
if (entity is File && entity.path.endsWith(".js")) {
|
if (entity is File && entity.path.endsWith(".js")) {
|
||||||
try {
|
try {
|
||||||
var source = await ComicSourceParser()
|
var source = await ComicSourceParser().parse(
|
||||||
.parse(await entity.readAsString(), entity.absolute.path);
|
await entity.readAsString(),
|
||||||
|
entity.absolute.path,
|
||||||
|
);
|
||||||
_sources.add(source);
|
_sources.add(source);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("ComicSource", "$e\n$s");
|
Log.error("ComicSource", "$e\n$s");
|
||||||
@@ -154,7 +156,7 @@ class ComicSource {
|
|||||||
final GetImageLoadingConfigFunc? getImageLoadingConfig;
|
final GetImageLoadingConfigFunc? getImageLoadingConfig;
|
||||||
|
|
||||||
final Map<String, dynamic> Function(String imageKey)?
|
final Map<String, dynamic> Function(String imageKey)?
|
||||||
getThumbnailLoadingConfig;
|
getThumbnailLoadingConfig;
|
||||||
|
|
||||||
var data = <String, dynamic>{};
|
var data = <String, dynamic>{};
|
||||||
|
|
||||||
@@ -170,6 +172,10 @@ class ComicSource {
|
|||||||
|
|
||||||
final SendCommentFunc? sendCommentFunc;
|
final SendCommentFunc? sendCommentFunc;
|
||||||
|
|
||||||
|
final ChapterCommentsLoader? chapterCommentsLoader;
|
||||||
|
|
||||||
|
final SendChapterCommentFunc? sendChapterCommentFunc;
|
||||||
|
|
||||||
final RegExp? idMatcher;
|
final RegExp? idMatcher;
|
||||||
|
|
||||||
final LikeOrUnlikeComicFunc? likeOrUnlikeComic;
|
final LikeOrUnlikeComicFunc? likeOrUnlikeComic;
|
||||||
@@ -256,6 +262,8 @@ class ComicSource {
|
|||||||
this.version,
|
this.version,
|
||||||
this.commentsLoader,
|
this.commentsLoader,
|
||||||
this.sendCommentFunc,
|
this.sendCommentFunc,
|
||||||
|
this.chapterCommentsLoader,
|
||||||
|
this.sendChapterCommentFunc,
|
||||||
this.likeOrUnlikeComic,
|
this.likeOrUnlikeComic,
|
||||||
this.voteCommentFunc,
|
this.voteCommentFunc,
|
||||||
this.likeCommentFunc,
|
this.likeCommentFunc,
|
||||||
@@ -367,11 +375,19 @@ enum ExplorePageType {
|
|||||||
override,
|
override,
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef SearchFunction = Future<Res<List<Comic>>> Function(
|
typedef SearchFunction =
|
||||||
String keyword, int page, List<String> searchOption);
|
Future<Res<List<Comic>>> Function(
|
||||||
|
String keyword,
|
||||||
|
int page,
|
||||||
|
List<String> searchOption,
|
||||||
|
);
|
||||||
|
|
||||||
typedef SearchNextFunction = Future<Res<List<Comic>>> Function(
|
typedef SearchNextFunction =
|
||||||
String keyword, String? next, List<String> searchOption);
|
Future<Res<List<Comic>>> Function(
|
||||||
|
String keyword,
|
||||||
|
String? next,
|
||||||
|
List<String> searchOption,
|
||||||
|
);
|
||||||
|
|
||||||
class SearchPageData {
|
class SearchPageData {
|
||||||
/// If this is not null, the default value of search options will be first element.
|
/// 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 ?? "";
|
String get defaultValue => defaultVal ?? options.keys.firstOrNull ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
typedef CategoryComicsLoader =
|
||||||
String category, String? param, List<String> options, int page);
|
Future<Res<List<Comic>>> Function(
|
||||||
|
String category,
|
||||||
|
String? param,
|
||||||
|
List<String> options,
|
||||||
|
int page,
|
||||||
|
);
|
||||||
|
|
||||||
typedef CategoryOptionsLoader = Future<Res<List<CategoryComicsOptions>>> Function(
|
typedef CategoryOptionsLoader =
|
||||||
String category, String? param);
|
Future<Res<List<CategoryComicsOptions>>> Function(
|
||||||
|
String category,
|
||||||
|
String? param,
|
||||||
|
);
|
||||||
|
|
||||||
class CategoryComicsData {
|
class CategoryComicsData {
|
||||||
/// options
|
/// options
|
||||||
@@ -419,7 +443,12 @@ class CategoryComicsData {
|
|||||||
|
|
||||||
final RankingData? rankingData;
|
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 {
|
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, int page)? load;
|
||||||
|
|
||||||
final Future<Res<List<Comic>>> Function(String option, String? next)?
|
final Future<Res<List<Comic>>> Function(String option, String? next)?
|
||||||
loadWithNext;
|
loadWithNext;
|
||||||
|
|
||||||
const RankingData(this.options, this.load, this.loadWithNext);
|
const RankingData(this.options, this.load, this.loadWithNext);
|
||||||
}
|
}
|
||||||
@@ -447,7 +476,12 @@ class CategoryComicsOptions {
|
|||||||
|
|
||||||
final List<String>? showWhen;
|
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 {
|
class LinkHandler {
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ class ComicSourceParser {
|
|||||||
version ?? "1.0.0",
|
version ?? "1.0.0",
|
||||||
_parseCommentsLoader(),
|
_parseCommentsLoader(),
|
||||||
_parseSendCommentFunc(),
|
_parseSendCommentFunc(),
|
||||||
|
_parseChapterCommentsLoader(),
|
||||||
|
_parseSendChapterCommentFunc(),
|
||||||
_parseLikeFunc(),
|
_parseLikeFunc(),
|
||||||
_parseVoteCommentFunc(),
|
_parseVoteCommentFunc(),
|
||||||
_parseLikeCommentFunc(),
|
_parseLikeCommentFunc(),
|
||||||
@@ -560,12 +562,16 @@ class ComicSourceParser {
|
|||||||
res = await res;
|
res = await res;
|
||||||
}
|
}
|
||||||
if (res is! List) {
|
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>[];
|
var options = <CategoryComicsOptions>[];
|
||||||
for (var element in res) {
|
for (var element in res) {
|
||||||
if (element is! Map) {
|
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>();
|
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
||||||
for (var option in element["options"] ?? []) {
|
for (var option in element["options"] ?? []) {
|
||||||
@@ -582,13 +588,14 @@ class ComicSourceParser {
|
|||||||
element["label"] ?? "",
|
element["label"] ?? "",
|
||||||
map,
|
map,
|
||||||
List.from(element["notShowWhen"] ?? []),
|
List.from(element["notShowWhen"] ?? []),
|
||||||
element["showWhen"] == null ? null : List.from(element["showWhen"]),
|
element["showWhen"] == null
|
||||||
|
? null
|
||||||
|
: List.from(element["showWhen"]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Res(options);
|
return Res(options);
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
|
||||||
Log.error("Data Analysis", "Failed to load category options.\n$e");
|
Log.error("Data Analysis", "Failed to load category options.\n$e");
|
||||||
return Res.error(e.toString());
|
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() {
|
GetImageLoadingConfigFunc? _parseImageLoadingConfigFunc() {
|
||||||
if (!_checkExists("comic.onImageLoad")) {
|
if (!_checkExists("comic.onImageLoad")) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -4,50 +4,90 @@ part of 'comic_source.dart';
|
|||||||
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
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.
|
/// 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(
|
typedef ComicListBuilderWithNext =
|
||||||
String? next);
|
Future<Res<List<Comic>>> Function(String? next);
|
||||||
|
|
||||||
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
||||||
|
|
||||||
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
||||||
|
|
||||||
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
|
typedef LoadComicPagesFunc =
|
||||||
String id, String? ep);
|
Future<Res<List<String>>> Function(String id, String? ep);
|
||||||
|
|
||||||
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
|
typedef CommentsLoader =
|
||||||
String id, String? subId, int page, String? replyTo);
|
Future<Res<List<Comment>>> Function(
|
||||||
|
String id,
|
||||||
|
String? subId,
|
||||||
|
int page,
|
||||||
|
String? replyTo,
|
||||||
|
);
|
||||||
|
|
||||||
typedef SendCommentFunc = Future<Res<bool>> Function(
|
typedef ChapterCommentsLoader =
|
||||||
String id, String? subId, String content, String? replyTo);
|
Future<Res<List<Comment>>> Function(
|
||||||
|
String comicId,
|
||||||
|
String epId,
|
||||||
|
int page,
|
||||||
|
String? replyTo,
|
||||||
|
);
|
||||||
|
|
||||||
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
|
typedef SendCommentFunc =
|
||||||
String imageKey, String comicId, String epId)?;
|
Future<Res<bool>> Function(
|
||||||
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
String id,
|
||||||
String imageKey)?;
|
String? subId,
|
||||||
|
String content,
|
||||||
|
String? replyTo,
|
||||||
|
);
|
||||||
|
|
||||||
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
|
typedef SendChapterCommentFunc =
|
||||||
String comicId, String? next);
|
Future<Res<bool>> Function(
|
||||||
|
String comicId,
|
||||||
|
String epId,
|
||||||
|
String content,
|
||||||
|
String? replyTo,
|
||||||
|
);
|
||||||
|
|
||||||
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
|
typedef GetImageLoadingConfigFunc =
|
||||||
String comicId, bool isLiking);
|
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.
|
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
||||||
/// return the new likes count or null.
|
/// return the new likes count or null.
|
||||||
typedef LikeCommentFunc = Future<Res<int?>> Function(
|
typedef LikeCommentFunc =
|
||||||
String comicId, String? subId, String commentId, bool isLiking);
|
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.
|
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
||||||
/// return the new vote count or null.
|
/// return the new vote count or null.
|
||||||
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
typedef VoteCommentFunc =
|
||||||
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
Future<Res<int?>> Function(
|
||||||
|
String comicId,
|
||||||
|
String? subId,
|
||||||
|
String commentId,
|
||||||
|
bool isUp,
|
||||||
|
bool isCancel,
|
||||||
|
);
|
||||||
|
|
||||||
typedef HandleClickTagEvent = PageJumpTarget? Function(
|
typedef HandleClickTagEvent =
|
||||||
String namespace, String tag);
|
PageJumpTarget? Function(String namespace, String tag);
|
||||||
|
|
||||||
/// Handle tag suggestion selection event. Should return the text to insert
|
/// Handle tag suggestion selection event. Should return the text to insert
|
||||||
/// into the search field.
|
/// into the search field.
|
||||||
typedef TagSuggestionSelectFunc = String Function(
|
typedef TagSuggestionSelectFunc = String Function(String namespace, String tag);
|
||||||
String namespace, String tag);
|
|
||||||
|
|
||||||
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
/// [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/favorites.dart';
|
||||||
import 'package:venera/foundation/global_state.dart';
|
import 'package:venera/foundation/global_state.dart';
|
||||||
import 'package:venera/foundation/history.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/image_provider/reader_image.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
@@ -54,6 +55,8 @@ part 'loading.dart';
|
|||||||
|
|
||||||
part 'chapters.dart';
|
part 'chapters.dart';
|
||||||
|
|
||||||
|
part 'chapter_comments.dart';
|
||||||
|
|
||||||
extension _ReaderContext on BuildContext {
|
extension _ReaderContext on BuildContext {
|
||||||
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
|
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
|
||||||
|
|
||||||
@@ -165,12 +168,22 @@ class _ReaderState extends State<Reader>
|
|||||||
page = widget.initialPage!;
|
page = widget.initialPage!;
|
||||||
}
|
}
|
||||||
// mode = ReaderMode.fromKey(appdata.settings['readerMode']);
|
// 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;
|
history = widget.history;
|
||||||
if (!appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSystemStatusBar')) {
|
if (!appdata.settings.getReaderSetting(
|
||||||
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'showSystemStatusBar',
|
||||||
|
)) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
}
|
}
|
||||||
if (appdata.settings.getReaderSetting(cid, type.sourceKey, 'enableTurnPageByVolumeKey')) {
|
if (appdata.settings.getReaderSetting(
|
||||||
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'enableTurnPageByVolumeKey',
|
||||||
|
)) {
|
||||||
handleVolumeEvent();
|
handleVolumeEvent();
|
||||||
}
|
}
|
||||||
setImageCacheSize();
|
setImageCacheSize();
|
||||||
@@ -208,8 +221,10 @@ class _ReaderState extends State<Reader>
|
|||||||
} else {
|
} else {
|
||||||
maxImageCacheSize = 500 << 20;
|
maxImageCacheSize = 500 << 20;
|
||||||
}
|
}
|
||||||
Log.info("Reader",
|
Log.info(
|
||||||
"Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize");
|
"Reader",
|
||||||
|
"Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize",
|
||||||
|
);
|
||||||
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
|
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,13 +254,15 @@ class _ReaderState extends State<Reader>
|
|||||||
onKeyEvent: onKeyEvent,
|
onKeyEvent: onKeyEvent,
|
||||||
child: Overlay(
|
child: Overlay(
|
||||||
initialEntries: [
|
initialEntries: [
|
||||||
OverlayEntry(builder: (context) {
|
OverlayEntry(
|
||||||
return _ReaderScaffold(
|
builder: (context) {
|
||||||
child: _ReaderGestureDetector(
|
return _ReaderScaffold(
|
||||||
child: _ReaderImages(key: Key(chapter.toString())),
|
child: _ReaderGestureDetector(
|
||||||
),
|
child: _ReaderImages(key: Key(chapter.toString())),
|
||||||
);
|
),
|
||||||
})
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -382,16 +399,29 @@ abstract mixin class _ImagePerPageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool showSingleImageOnFirstPage() =>
|
bool showSingleImageOnFirstPage() => appdata.settings.getReaderSetting(
|
||||||
appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSingleImageOnFirstPage');
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'showSingleImageOnFirstPage',
|
||||||
|
);
|
||||||
|
|
||||||
/// The number of images displayed on one screen
|
/// The number of images displayed on one screen
|
||||||
int get imagesPerPage {
|
int get imagesPerPage {
|
||||||
if (mode.isContinuous) return 1;
|
if (mode.isContinuous) return 1;
|
||||||
if (isPortrait) {
|
if (isPortrait) {
|
||||||
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForPortrait') ?? 1;
|
return appdata.settings.getReaderSetting(
|
||||||
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'readerScreenPicNumberForPortrait',
|
||||||
|
) ??
|
||||||
|
1;
|
||||||
} else {
|
} else {
|
||||||
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForLandscape') ?? 1;
|
return appdata.settings.getReaderSetting(
|
||||||
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'readerScreenPicNumberForLandscape',
|
||||||
|
) ??
|
||||||
|
1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,15 +430,22 @@ abstract mixin class _ImagePerPageHandler {
|
|||||||
int currentImagesPerPage = imagesPerPage;
|
int currentImagesPerPage = imagesPerPage;
|
||||||
bool currentOrientation = isPortrait;
|
bool currentOrientation = isPortrait;
|
||||||
|
|
||||||
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {
|
if (_lastImagesPerPage != currentImagesPerPage ||
|
||||||
_adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
|
_lastOrientation != currentOrientation) {
|
||||||
|
_adjustPageForImagesPerPageChange(
|
||||||
|
_lastImagesPerPage,
|
||||||
|
currentImagesPerPage,
|
||||||
|
);
|
||||||
_lastImagesPerPage = currentImagesPerPage;
|
_lastImagesPerPage = currentImagesPerPage;
|
||||||
_lastOrientation = currentOrientation;
|
_lastOrientation = currentOrientation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adjust the page number when the number of images per page changes
|
/// 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;
|
int previousImageIndex = 1;
|
||||||
if (!showSingleImageOnFirstPage() || oldImagesPerPage == 1) {
|
if (!showSingleImageOnFirstPage() || oldImagesPerPage == 1) {
|
||||||
previousImageIndex = (page - 1) * oldImagesPerPage + 1;
|
previousImageIndex = (page - 1) * oldImagesPerPage + 1;
|
||||||
@@ -431,7 +468,7 @@ abstract mixin class _ImagePerPageHandler {
|
|||||||
newPage = previousImageIndex;
|
newPage = previousImageIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
page = newPage>0 ? newPage : 1;
|
page = newPage > 0 ? newPage : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,10 +503,7 @@ abstract mixin class _VolumeListener {
|
|||||||
if (volumeListener != null) {
|
if (volumeListener != null) {
|
||||||
volumeListener?.cancel();
|
volumeListener?.cancel();
|
||||||
}
|
}
|
||||||
volumeListener = VolumeListener(
|
volumeListener = VolumeListener(onDown: onDown, onUp: onUp)..listen();
|
||||||
onDown: onDown,
|
|
||||||
onUp: onUp,
|
|
||||||
)..listen();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopVolumeEvent() {
|
void stopVolumeEvent() {
|
||||||
@@ -504,7 +538,8 @@ abstract mixin class _ReaderLocation {
|
|||||||
|
|
||||||
void update();
|
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;
|
_ImageViewController? _imageViewController;
|
||||||
|
|
||||||
@@ -585,7 +620,11 @@ abstract mixin class _ReaderLocation {
|
|||||||
autoPageTurningTimer!.cancel();
|
autoPageTurningTimer!.cancel();
|
||||||
autoPageTurningTimer = null;
|
autoPageTurningTimer = null;
|
||||||
} else {
|
} 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), (_) {
|
autoPageTurningTimer = Timer.periodic(Duration(seconds: interval), (_) {
|
||||||
if (page == maxPage) {
|
if (page == maxPage) {
|
||||||
autoPageTurningTimer!.cancel();
|
autoPageTurningTimer!.cancel();
|
||||||
|
|||||||
@@ -183,6 +183,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
if (shouldShowChapterComments())
|
||||||
|
Tooltip(
|
||||||
|
message: "Chapter Comments".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.comment),
|
||||||
|
onPressed: openChapterComments,
|
||||||
|
),
|
||||||
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Settings".tl,
|
message: "Settings".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@@ -605,7 +613,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
var (imageIndex, data) = result;
|
var (imageIndex, data) = result;
|
||||||
var fileType = detectFileType(data);
|
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);
|
saveFile(data: data, filename: filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +625,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
var (imageIndex, data) = result;
|
var (imageIndex, data) = result;
|
||||||
var fileType = detectFileType(data);
|
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);
|
Share.shareFile(data: data, filename: filename, mime: fileType.mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,6 +660,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
if (key == "quickCollectImage") {
|
if (key == "quickCollectImage") {
|
||||||
addDragListener();
|
addDragListener();
|
||||||
}
|
}
|
||||||
|
if (key == "showChapterComments") {
|
||||||
|
update();
|
||||||
|
}
|
||||||
context.reader.update();
|
context.reader.update();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -657,6 +670,49 @@ 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,
|
||||||
|
),
|
||||||
|
showBarrier: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildEpChangeButton() {
|
Widget buildEpChangeButton() {
|
||||||
if (context.reader.widget.chapters == null) return const SizedBox();
|
if (context.reader.widget.chapters == null) return const SizedBox();
|
||||||
switch (showFloatingButtonValue) {
|
switch (showFloatingButtonValue) {
|
||||||
|
|||||||
@@ -303,6 +303,15 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).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