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 页", "Last Reading: @epName Page @page": "上次阅读: @epName 第 @page 页",
"WebDAV Auto Sync": "WebDAV 自动同步", "WebDAV Auto Sync": "WebDAV 自动同步",
"Mark all as read": "全部标记为已读", "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": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -725,6 +727,8 @@
"Last Reading: @epName Page @page": "上次閱讀: @epName 第 @page 頁", "Last Reading: @epName Page @page": "上次閱讀: @epName 第 @page 頁",
"WebDAV Auto Sync": "WebDAV 自動同步", "WebDAV Auto Sync": "WebDAV 自動同步",
"Mark all as read": "全部標記為已讀", "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) { builder: (BuildContext context, int index) {
if (index == 0 || index == totalPages + 1) { if (index == 0 || index == totalPages + 1) {
return PhotoViewGalleryPageOptions.customChild( return PhotoViewGalleryPageOptions.customChild(
scaleStateController: PhotoViewScaleStateController(),
child: const SizedBox(), child: const SizedBox(),
); );
} else { } else {
@@ -168,7 +167,7 @@ class _GalleryModeState extends State<_GalleryMode>
cached[index] = true; cached[index] = true;
cache(index); cache(index);
photoViewControllers[index] = PhotoViewController(); photoViewControllers[index] ??= PhotoViewController();
if (reader.imagesPerPage == 1) { if (reader.imagesPerPage == 1) {
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
@@ -350,6 +349,8 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
PointerDeviceKind.unknown PointerDeviceKind.unknown
}; };
const double _kChangeChapterOffset = 200;
class _ContinuousMode extends StatefulWidget { class _ContinuousMode extends StatefulWidget {
const _ContinuousMode({super.key}); const _ContinuousMode({super.key});
@@ -364,7 +365,9 @@ class _ContinuousModeState extends State<_ContinuousMode>
var itemScrollController = ItemScrollController(); var itemScrollController = ItemScrollController();
var itemPositionsListener = ItemPositionsListener.create(); var itemPositionsListener = ItemPositionsListener.create();
var photoViewController = PhotoViewController(); var photoViewController = PhotoViewController();
late ScrollController scrollController; ScrollController? _scrollController;
ScrollController get scrollController => _scrollController!;
var isCTRLPressed = false; var isCTRLPressed = false;
static var _isMouseScrolling = false; static var _isMouseScrolling = false;
@@ -372,6 +375,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
bool disableScroll = false; bool disableScroll = false;
late List<bool> cached; late List<bool> cached;
int get preCacheCount => appdata.settings["preloadImageCount"]; int get preCacheCount => appdata.settings["preloadImageCount"];
/// Whether the user was scrolling the page. /// 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 @override
void initState() { void initState() {
reader = context.reader; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget widget = ScrollablePositionedList.builder( Widget widget = ScrollablePositionedList.builder(
@@ -471,7 +492,11 @@ class _ContinuousModeState extends State<_ContinuousMode>
itemScrollController: itemScrollController, itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener, itemPositionsListener: itemPositionsListener,
scrollControllerCallback: (scrollController) { scrollControllerCallback: (scrollController) {
this.scrollController = scrollController; if (_scrollController != null) {
_scrollController!.removeListener(onScroll);
}
_scrollController = scrollController;
_scrollController!.addListener(onScroll);
}, },
itemCount: reader.maxPage + 2, itemCount: reader.maxPage + 2,
addSemanticIndexes: false, addSemanticIndexes: false,
@@ -481,7 +506,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
reverse: reader.mode == ReaderMode.continuousRightToLeft, reverse: reader.mode == ReaderMode.continuousRightToLeft,
physics: isCTRLPressed || _isMouseScrolling || disableScroll physics: isCTRLPressed || _isMouseScrolling || disableScroll
? const NeverScrollableScrollPhysics() ? const NeverScrollableScrollPhysics()
: const ClampingScrollPhysics(), : const BouncingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0 || index == reader.maxPage + 1) { if (index == 0 || index == reader.maxPage + 1) {
return const SizedBox(); return const SizedBox();
@@ -496,18 +521,28 @@ class _ContinuousModeState extends State<_ContinuousMode>
ImageProvider image = _createImageProvider(index, context); ImageProvider image = _createImageProvider(index, context);
return ComicImage( return ColoredBox(
filterQuality: FilterQuality.medium, color: context.colorScheme.surface,
image: image, child: ComicImage(
width: width, filterQuality: FilterQuality.medium,
height: height, image: image,
fit: BoxFit.contain, width: width,
height: height,
fit: BoxFit.contain,
),
); );
}, },
scrollBehavior: const MaterialScrollBehavior() scrollBehavior: const MaterialScrollBehavior()
.copyWith(scrollbars: false, dragDevices: _kTouchLikeDeviceTypes), .copyWith(scrollbars: false, dragDevices: _kTouchLikeDeviceTypes),
); );
widget = Stack(
children: [
Positioned.fill(child: buildBackground(context)),
Positioned.fill(child: widget),
],
);
widget = Listener( widget = Listener(
onPointerDown: (event) { onPointerDown: (event) {
fingers++; fingers++;
@@ -530,6 +565,13 @@ class _ContinuousModeState extends State<_ContinuousMode>
disableScroll = false; disableScroll = false;
}); });
} }
if (fingers == 0) {
if (jumpToPrevChapter) {
reader.toPrevChapter();
} else if (jumpToNextChapter) {
reader.toNextChapter();
}
}
}, },
onPointerCancel: (event) { onPointerCancel: (event) {
fingers--; fingers--;
@@ -577,15 +619,37 @@ class _ContinuousModeState extends State<_ContinuousMode>
if (notification is ScrollUpdateNotification) { if (notification is ScrollUpdateNotification) {
if (!scrollController.hasClients) return false; if (!scrollController.hasClients) return false;
if (scrollController.position.pixels <= if (scrollController.position.pixels <=
scrollController.position.minScrollExtent && scrollController.position.minScrollExtent &&
!reader.isFirstChapterOfGroup) { !reader.isFirstChapterOfGroup) {
context.readerScaffold.setFloatingButton(-1); if (!prepareToPrevChapter) {
jumpToPrevChapter = false;
jumpToNextChapter = false;
context.readerScaffold.setFloatingButton(-1);
setState(() {
prepareToPrevChapter = true;
});
}
} else if (scrollController.position.pixels >= } else if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent && scrollController.position.maxScrollExtent &&
!reader.isLastChapterOfGroup) { !reader.isLastChapterOfGroup) {
context.readerScaffold.setFloatingButton(1); if (!prepareToNextChapter) {
jumpToPrevChapter = false;
jumpToNextChapter = false;
context.readerScaffold.setFloatingButton(1);
setState(() {
prepareToNextChapter = true;
});
}
} else { } else {
context.readerScaffold.setFloatingButton(0); 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 @override
Future<void> animateToPage(int page) { Future<void> animateToPage(int page) {
return itemScrollController.scrollTo( return itemScrollController.scrollTo(
@@ -758,3 +842,127 @@ void _precacheImage(int page, BuildContext context) {
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 lastValue = 0;
var fABValue = ValueNotifier<double>(0);
_ReaderGestureDetectorState? _gestureDetectorState; _ReaderGestureDetectorState? _gestureDetectorState;
_DragListener? _floatingButtonDragListener;
void setFloatingButton(int value) { void setFloatingButton(int value) {
lastValue = showFloatingButtonValue; lastValue = showFloatingButtonValue;
if (value == 0) { if (value == 0) {
if (showFloatingButtonValue != 0) { if (showFloatingButtonValue != 0) {
showFloatingButtonValue = 0; showFloatingButtonValue = 0;
fABValue.value = 0;
update(); update();
} }
if (_floatingButtonDragListener != null) {
_gestureDetectorState!.removeDragListener(_floatingButtonDragListener!);
_floatingButtonDragListener = null;
}
} }
var readerMode = context.reader.mode;
if (value == 1 && showFloatingButtonValue == 0) { if (value == 1 && showFloatingButtonValue == 0) {
showFloatingButtonValue = 1; 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(); update();
} else if (value == -1 && showFloatingButtonValue == 0) { } else if (value == -1 && showFloatingButtonValue == 0) {
showFloatingButtonValue = -1; 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(); update();
} }
} }
@@ -778,62 +726,35 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
); );
case -1: case -1:
case 1: case 1:
return Container( return SizedBox(
width: 58, width: 58,
height: 58, height: 58,
clipBehavior: Clip.antiAlias, child: Material(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), elevation: 2,
child: ValueListenableBuilder( child: InkWell(
valueListenable: fABValue, onTap: () {
builder: (context, value, child) { if (showFloatingButtonValue == 1) {
return Stack( context.reader.toNextChapter();
children: [ } else if (showFloatingButtonValue == -1) {
Positioned.fill( context.reader.toPrevChapter();
child: Material( }
color: Colors.transparent, setFloatingButton(0);
child: InkWell( },
onTap: () { borderRadius: BorderRadius.circular(16),
if (showFloatingButtonValue == 1) { child: Center(
context.reader.toNextChapter(); child: Icon(
} else if (showFloatingButtonValue == -1) { showFloatingButtonValue == 1
context.reader.toPrevChapter(); ? Icons.arrow_forward_ios
} : Icons.arrow_back_ios_outlined,
setFloatingButton(0); size: 24,
}, color: Theme.of(context)
borderRadius: BorderRadius.circular(16), .colorScheme
child: Center( .onPrimaryContainer,
child: Icon( ),
showFloatingButtonValue == 1 ),
? Icons.arrow_forward_ios ),
: Icons.arrow_back_ios_outlined,
size: 24,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
),
),
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(),
),
),
],
);
},
), ),
); );
} }