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:
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>()!;
|
||||
|
||||
@@ -165,12 +168,22 @@ class _ReaderState extends State<Reader>
|
||||
page = widget.initialPage!;
|
||||
}
|
||||
// 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();
|
||||
@@ -208,8 +221,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;
|
||||
}
|
||||
|
||||
@@ -239,13 +254,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())),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -382,16 +399,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,15 +430,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;
|
||||
@@ -431,7 +468,7 @@ abstract mixin class _ImagePerPageHandler {
|
||||
newPage = previousImageIndex;
|
||||
}
|
||||
|
||||
page = newPage>0 ? newPage : 1;
|
||||
page = newPage > 0 ? newPage : 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,10 +503,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() {
|
||||
@@ -504,7 +538,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;
|
||||
|
||||
@@ -585,7 +620,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,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() {
|
||||
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