Improve changing chapter gesture with continuous mode.

This commit is contained in:
2025-02-22 10:41:56 +08:00
parent 7fe81ae418
commit 5645d805f5
3 changed files with 254 additions and 121 deletions

View File

@@ -361,7 +361,9 @@
"Last Reading: @epName Page @page": "上次阅读: @epName 第 @page 页",
"WebDAV Auto Sync": "WebDAV 自动同步",
"Mark all as read": "全部标记为已读",
"Do you want to mark all as read?" : "您要全部标记为已读吗?"
"Do you want to mark all as read?" : "您要全部标记为已读吗?",
"Swipe down for previous chapter": "向下滑动查看上一章",
"Swipe up for next chapter": "向上滑动查看下一章"
},
"zh_TW": {
"Home": "首頁",
@@ -725,6 +727,8 @@
"Last Reading: @epName Page @page": "上次閱讀: @epName 第 @page 頁",
"WebDAV Auto Sync": "WebDAV 自動同步",
"Mark all as read": "全部標記為已讀",
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?"
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?",
"Swipe down for previous chapter": "向下滑動查看上一章",
"Swipe up for next chapter": "向上滑動查看下一章"
}
}

View File

@@ -154,7 +154,6 @@ class _GalleryModeState extends State<_GalleryMode>
builder: (BuildContext context, int index) {
if (index == 0 || index == totalPages + 1) {
return PhotoViewGalleryPageOptions.customChild(
scaleStateController: PhotoViewScaleStateController(),
child: const SizedBox(),
);
} else {
@@ -168,7 +167,7 @@ class _GalleryModeState extends State<_GalleryMode>
cached[index] = true;
cache(index);
photoViewControllers[index] = PhotoViewController();
photoViewControllers[index] ??= PhotoViewController();
if (reader.imagesPerPage == 1) {
return PhotoViewGalleryPageOptions(
@@ -350,6 +349,8 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
PointerDeviceKind.unknown
};
const double _kChangeChapterOffset = 200;
class _ContinuousMode extends StatefulWidget {
const _ContinuousMode({super.key});
@@ -364,7 +365,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
var itemScrollController = ItemScrollController();
var itemPositionsListener = ItemPositionsListener.create();
var photoViewController = PhotoViewController();
late ScrollController scrollController;
ScrollController? _scrollController;
ScrollController get scrollController => _scrollController!;
var isCTRLPressed = false;
static var _isMouseScrolling = false;
@@ -372,6 +375,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
bool disableScroll = false;
late List<bool> cached;
int get preCacheCount => appdata.settings["preloadImageCount"];
/// Whether the user was scrolling the page.
@@ -386,6 +390,11 @@ class _ContinuousModeState extends State<_ContinuousMode>
);
}
bool prepareToPrevChapter = false;
bool prepareToNextChapter = false;
bool jumpToNextChapter = false;
bool jumpToPrevChapter = false;
@override
void initState() {
reader = context.reader;
@@ -464,6 +473,18 @@ class _ContinuousModeState extends State<_ContinuousMode>
}
}
void onScroll() {
if (prepareToPrevChapter) {
jumpToNextChapter = false;
jumpToPrevChapter = scrollController.offset <
scrollController.position.minScrollExtent - _kChangeChapterOffset;
} else if (prepareToNextChapter) {
jumpToNextChapter = scrollController.offset >
scrollController.position.maxScrollExtent + _kChangeChapterOffset;
jumpToPrevChapter = false;
}
}
@override
Widget build(BuildContext context) {
Widget widget = ScrollablePositionedList.builder(
@@ -471,7 +492,11 @@ class _ContinuousModeState extends State<_ContinuousMode>
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
scrollControllerCallback: (scrollController) {
this.scrollController = scrollController;
if (_scrollController != null) {
_scrollController!.removeListener(onScroll);
}
_scrollController = scrollController;
_scrollController!.addListener(onScroll);
},
itemCount: reader.maxPage + 2,
addSemanticIndexes: false,
@@ -481,7 +506,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
reverse: reader.mode == ReaderMode.continuousRightToLeft,
physics: isCTRLPressed || _isMouseScrolling || disableScroll
? const NeverScrollableScrollPhysics()
: const ClampingScrollPhysics(),
: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
if (index == 0 || index == reader.maxPage + 1) {
return const SizedBox();
@@ -496,18 +521,28 @@ class _ContinuousModeState extends State<_ContinuousMode>
ImageProvider image = _createImageProvider(index, context);
return ComicImage(
return ColoredBox(
color: context.colorScheme.surface,
child: ComicImage(
filterQuality: FilterQuality.medium,
image: image,
width: width,
height: height,
fit: BoxFit.contain,
),
);
},
scrollBehavior: const MaterialScrollBehavior()
.copyWith(scrollbars: false, dragDevices: _kTouchLikeDeviceTypes),
);
widget = Stack(
children: [
Positioned.fill(child: buildBackground(context)),
Positioned.fill(child: widget),
],
);
widget = Listener(
onPointerDown: (event) {
fingers++;
@@ -530,6 +565,13 @@ class _ContinuousModeState extends State<_ContinuousMode>
disableScroll = false;
});
}
if (fingers == 0) {
if (jumpToPrevChapter) {
reader.toPrevChapter();
} else if (jumpToNextChapter) {
reader.toNextChapter();
}
}
},
onPointerCancel: (event) {
fingers--;
@@ -579,13 +621,35 @@ class _ContinuousModeState extends State<_ContinuousMode>
if (scrollController.position.pixels <=
scrollController.position.minScrollExtent &&
!reader.isFirstChapterOfGroup) {
if (!prepareToPrevChapter) {
jumpToPrevChapter = false;
jumpToNextChapter = false;
context.readerScaffold.setFloatingButton(-1);
setState(() {
prepareToPrevChapter = true;
});
}
} else if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent &&
!reader.isLastChapterOfGroup) {
if (!prepareToNextChapter) {
jumpToPrevChapter = false;
jumpToNextChapter = false;
context.readerScaffold.setFloatingButton(1);
setState(() {
prepareToNextChapter = true;
});
}
} else {
context.readerScaffold.setFloatingButton(0);
if (prepareToPrevChapter || prepareToNextChapter) {
jumpToPrevChapter = false;
jumpToNextChapter = false;
setState(() {
prepareToPrevChapter = false;
prepareToNextChapter = false;
});
}
}
}
@@ -618,6 +682,26 @@ class _ContinuousModeState extends State<_ContinuousMode>
);
}
Widget buildBackground(BuildContext context) {
return Column(
children: [
SizedBox(height: context.padding.top + 16),
if (prepareToPrevChapter)
_SwipeChangeChapterProgress(
controller: scrollController,
isPrev: true,
),
const Spacer(),
if (prepareToNextChapter)
_SwipeChangeChapterProgress(
controller: scrollController,
isPrev: false,
),
SizedBox(height: context.padding.bottom + 16),
],
);
}
@override
Future<void> animateToPage(int page) {
return itemScrollController.scrollTo(
@@ -758,3 +842,127 @@ void _precacheImage(int page, BuildContext context) {
context,
);
}
class _SwipeChangeChapterProgress extends StatefulWidget {
const _SwipeChangeChapterProgress({
this.controller,
required this.isPrev,
});
final ScrollController? controller;
final bool isPrev;
@override
State<_SwipeChangeChapterProgress> createState() =>
_SwipeChangeChapterProgressState();
}
class _SwipeChangeChapterProgressState
extends State<_SwipeChangeChapterProgress> {
double value = 0;
late final isPrev = widget.isPrev;
ScrollController? controller;
@override
void initState() {
super.initState();
if (widget.controller != null) {
controller = widget.controller;
controller!.addListener(onScroll);
}
}
@override
void didUpdateWidget(covariant _SwipeChangeChapterProgress oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
controller?.removeListener(onScroll);
controller = widget.controller;
controller?.addListener(onScroll);
if (value != 0) {
setState(() {
value = 0;
});
}
}
}
@override
void dispose() {
super.dispose();
controller?.removeListener(onScroll);
}
void onScroll() {
var position = controller!.position.pixels;
var offset = isPrev
? controller!.position.minScrollExtent - position
: position - controller!.position.maxScrollExtent;
var newValue = offset / _kChangeChapterOffset;
newValue = newValue.clamp(0.0, 1.0);
if (newValue != value) {
setState(() {
value = newValue;
});
}
}
@override
Widget build(BuildContext context) {
final msg = widget.isPrev
? "Swipe down for previous chapter".tl
: "Swipe up for next chapter".tl;
return CustomPaint(
painter: _ProgressPainter(
value: value,
backgroundColor: context.colorScheme.surfaceContainer,
color: context.colorScheme.primaryContainer,
),
child: Text(msg).paddingVertical(4).paddingHorizontal(16),
);
}
}
class _ProgressPainter extends CustomPainter {
final double value;
final Color backgroundColor;
final Color color;
const _ProgressPainter({
required this.value,
required this.backgroundColor,
required this.color,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = backgroundColor
..style = PaintingStyle.fill;
canvas.drawRRect(
RRect.fromLTRBR(0, 0, size.width, size.height, Radius.circular(16)),
paint,
);
paint.color = color;
canvas.drawRRect(
RRect.fromLTRBR(
0, 0, size.width * value, size.height, Radius.circular(16)),
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate is! _ProgressPainter ||
oldDelegate.value != value ||
oldDelegate.backgroundColor != backgroundColor ||
oldDelegate.color != color;
}
}

View File

@@ -26,73 +26,21 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
var lastValue = 0;
var fABValue = ValueNotifier<double>(0);
_ReaderGestureDetectorState? _gestureDetectorState;
_DragListener? _floatingButtonDragListener;
void setFloatingButton(int value) {
lastValue = showFloatingButtonValue;
if (value == 0) {
if (showFloatingButtonValue != 0) {
showFloatingButtonValue = 0;
fABValue.value = 0;
update();
}
if (_floatingButtonDragListener != null) {
_gestureDetectorState!.removeDragListener(_floatingButtonDragListener!);
_floatingButtonDragListener = null;
}
}
var readerMode = context.reader.mode;
if (value == 1 && showFloatingButtonValue == 0) {
showFloatingButtonValue = 1;
_floatingButtonDragListener = _DragListener(
onMove: (offset) {
if (readerMode == ReaderMode.continuousTopToBottom) {
fABValue.value -= offset.dy;
} else if (readerMode == ReaderMode.continuousLeftToRight) {
fABValue.value -= offset.dx;
} else if (readerMode == ReaderMode.continuousRightToLeft) {
fABValue.value += offset.dx;
}
},
onEnd: () {
if (fABValue.value.abs() > 58 * 3) {
setState(() {
showFloatingButtonValue = 0;
});
context.reader.toNextChapter();
}
fABValue.value = 0;
},
);
_gestureDetectorState!.addDragListener(_floatingButtonDragListener!);
update();
} else if (value == -1 && showFloatingButtonValue == 0) {
showFloatingButtonValue = -1;
_floatingButtonDragListener = _DragListener(
onMove: (offset) {
if (readerMode == ReaderMode.continuousTopToBottom) {
fABValue.value += offset.dy;
} else if (readerMode == ReaderMode.continuousLeftToRight) {
fABValue.value += offset.dx;
} else if (readerMode == ReaderMode.continuousRightToLeft) {
fABValue.value -= offset.dx;
}
},
onEnd: () {
if (fABValue.value.abs() > 58 * 3) {
setState(() {
showFloatingButtonValue = 0;
});
context.reader.toPrevChapter();
}
fABValue.value = 0;
},
);
_gestureDetectorState!.addDragListener(_floatingButtonDragListener!);
update();
}
}
@@ -778,22 +726,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
);
case -1:
case 1:
return Container(
return SizedBox(
width: 58,
height: 58,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
child: Material(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: ValueListenableBuilder(
valueListenable: fABValue,
builder: (context, value, child) {
return Stack(
children: [
Positioned.fill(
child: Material(
color: Colors.transparent,
elevation: 2,
child: InkWell(
onTap: () {
if (showFloatingButtonValue == 1) {
@@ -817,24 +756,6 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
),
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
height: value.clamp(0, 58 * 3) / 3,
child: ColoredBox(
color: Theme.of(context)
.colorScheme
.surfaceTint
.toOpacity(0.2),
child: const SizedBox.expand(),
),
),
],
);
},
),
);
}
return const SizedBox();