mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Improve changing chapter gesture with continuous mode.
This commit is contained in:
@@ -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": "向上滑動查看下一章"
|
||||
}
|
||||
}
|
@@ -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(
|
||||
filterQuality: FilterQuality.medium,
|
||||
image: image,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.contain,
|
||||
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--;
|
||||
@@ -577,15 +619,37 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
if (notification is ScrollUpdateNotification) {
|
||||
if (!scrollController.hasClients) return false;
|
||||
if (scrollController.position.pixels <=
|
||||
scrollController.position.minScrollExtent &&
|
||||
scrollController.position.minScrollExtent &&
|
||||
!reader.isFirstChapterOfGroup) {
|
||||
context.readerScaffold.setFloatingButton(-1);
|
||||
if (!prepareToPrevChapter) {
|
||||
jumpToPrevChapter = false;
|
||||
jumpToNextChapter = false;
|
||||
context.readerScaffold.setFloatingButton(-1);
|
||||
setState(() {
|
||||
prepareToPrevChapter = true;
|
||||
});
|
||||
}
|
||||
} else if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent &&
|
||||
scrollController.position.maxScrollExtent &&
|
||||
!reader.isLastChapterOfGroup) {
|
||||
context.readerScaffold.setFloatingButton(1);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -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,62 +726,35 @@ 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,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (showFloatingButtonValue == 1) {
|
||||
context.reader.toNextChapter();
|
||||
} else if (showFloatingButtonValue == -1) {
|
||||
context.reader.toPrevChapter();
|
||||
}
|
||||
setFloatingButton(0);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Center(
|
||||
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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
elevation: 2,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (showFloatingButtonValue == 1) {
|
||||
context.reader.toNextChapter();
|
||||
} else if (showFloatingButtonValue == -1) {
|
||||
context.reader.toPrevChapter();
|
||||
}
|
||||
setFloatingButton(0);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
showFloatingButtonValue == 1
|
||||
? Icons.arrow_forward_ios
|
||||
: Icons.arrow_back_ios_outlined,
|
||||
size: 24,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user