Files
venera/lib/pages/reader/gesture.dart
luckyray d874920c88 Feat: Image favorites (#126)
* feat: 增加图片收藏

* feat: 主体图片收藏页面实现

* feat: 点击打开大图浏览

* feat: 数据结构变更

* feat: 基本完成

* feat: 翻译与bug修复

* feat: 实机测试和问题修复

* feat: jm导入, pica历史记录nhentai有问题, 一键反转

* fix: 大小写不一致, 一个htManga, 一个htmanga

* feat: 拉取收藏优化

* feat: 改成以ep为准

* feat: 兜底一些可能报错场景

* chore: 没有用到

* feat: 尽量保证和网络收藏顺序一致

* feat: 支持显示热点tag

* feat: 支持双击收藏, 不过此时禁止放大图片

* fix: 自动塞封面逻辑完善, 切换快速收藏图片立刻生效

* Refactor

* fix updateValue

* feat: 双击功能提示

* fix: 被确定取消收藏的才删除

* Refactor ImageFavoritesPage

* translate author

* feat: 功能提示改到dialog中

* fix text editing

* fix text editing

* feat: 功能提示放到邮件或长按菜单中

* fix: 修复tag过滤不生效问题

* Improve image loading

* The default value of quickCollectImage should be false.

* Refactor DragListener

* Refactor ImageFavoriteItem & ImageFavoritePhotoView

* Refactor

* Fix `ImageFavoriteManager.has`

* Fix UI

* Improve UI

---------

Co-authored-by: nyne <me@nyne.dev>
2025-01-15 16:07:08 +08:00

291 lines
7.8 KiB
Dart

part of 'reader.dart';
class _ReaderGestureDetector extends StatefulWidget {
const _ReaderGestureDetector({required this.child});
final Widget child;
@override
State<_ReaderGestureDetector> createState() => _ReaderGestureDetectorState();
}
class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
late TapGestureRecognizer _tapGestureRecognizer;
static const _kDoubleTapMaxTime = Duration(milliseconds: 200);
static const _kLongPressMinTime = Duration(milliseconds: 250);
static const _kDoubleTapMaxDistanceSquared = 20.0 * 20.0;
static const _kTapToTurnPagePercent = 0.3;
final _dragListeners = <_DragListener>[];
int fingers = 0;
@override
void initState() {
_tapGestureRecognizer = TapGestureRecognizer()
..onTapUp = onTapUp
..onSecondaryTapUp = (details) {
onSecondaryTapUp(details.globalPosition);
};
super.initState();
context.readerScaffold._gestureDetectorState = this;
}
@override
Widget build(BuildContext context) {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) {
fingers++;
_lastTapPointer = event.pointer;
_lastTapMoveDistance = Offset.zero;
_tapGestureRecognizer.addPointer(event);
if (_dragInProgress) {
for (var dragListener in _dragListeners) {
dragListener.onStart?.call(event.position);
}
_dragInProgress = false;
}
Future.delayed(_kLongPressMinTime, () {
if (_lastTapPointer == event.pointer && fingers == 1) {
if (_lastTapMoveDistance!.distanceSquared < 20.0 * 20.0) {
onLongPressedDown(event.position);
_longPressInProgress = true;
} else {
_dragInProgress = true;
for (var dragListener in _dragListeners) {
dragListener.onStart?.call(event.position);
dragListener.onMove?.call(_lastTapMoveDistance!);
}
}
}
});
},
onPointerMove: (event) {
if (event.pointer == _lastTapPointer) {
_lastTapMoveDistance = event.delta + _lastTapMoveDistance!;
}
if (_dragInProgress) {
for (var dragListener in _dragListeners) {
dragListener.onMove?.call(event.delta);
}
}
},
onPointerUp: (event) {
fingers--;
if (_longPressInProgress) {
onLongPressedUp(event.position);
}
if (_dragInProgress) {
for (var dragListener in _dragListeners) {
dragListener.onEnd?.call();
}
_dragInProgress = false;
}
_lastTapPointer = null;
_lastTapMoveDistance = null;
},
onPointerCancel: (event) {
fingers--;
if (_longPressInProgress) {
onLongPressedUp(event.position);
}
if (_dragInProgress) {
for (var dragListener in _dragListeners) {
dragListener.onEnd?.call();
}
_dragInProgress = false;
}
_lastTapPointer = null;
_lastTapMoveDistance = null;
},
onPointerSignal: (event) {
if (event is PointerScrollEvent) {
onMouseWheel(event.scrollDelta.dy > 0);
}
},
child: widget.child,
);
}
void onMouseWheel(bool forward) {
if (HardwareKeyboard.instance.isControlPressed) {
return;
}
if (context.reader.mode.key.startsWith('gallery')) {
if (forward) {
if (!context.reader.toNextPage()) {
context.reader.toNextChapter();
}
} else {
if (!context.reader.toPrevPage()) {
context.reader.toPrevChapter();
}
}
}
}
TapUpDetails? _previousEvent;
int? _lastTapPointer;
Offset? _lastTapMoveDistance;
bool _longPressInProgress = false;
bool _dragInProgress = false;
void onTapUp(TapUpDetails event) {
if (_longPressInProgress) {
_longPressInProgress = false;
return;
}
final location = event.globalPosition;
final previousLocation = _previousEvent?.globalPosition;
if (previousLocation != null) {
if ((location - previousLocation).distanceSquared <
_kDoubleTapMaxDistanceSquared) {
onDoubleTap(location);
_previousEvent = null;
return;
} else {
onTap(previousLocation);
}
}
_previousEvent = event;
Future.delayed(_kDoubleTapMaxTime, () {
if (_previousEvent == event) {
onTap(location);
_previousEvent = null;
}
});
}
void onTap(Offset location) {
if (context.readerScaffold.isOpen) {
context.readerScaffold.openOrClose();
} else {
if (appdata.settings['enableTapToTurnPages']) {
bool isLeft = false, isRight = false, isTop = false, isBottom = false;
final width = context.width;
final height = context.height;
final x = location.dx;
final y = location.dy;
if (x < width * _kTapToTurnPagePercent) {
isLeft = true;
} else if (x > width * (1 - _kTapToTurnPagePercent)) {
isRight = true;
}
if (y < height * _kTapToTurnPagePercent) {
isTop = true;
} else if (y > height * (1 - _kTapToTurnPagePercent)) {
isBottom = true;
}
bool isCenter = false;
switch (context.reader.mode) {
case ReaderMode.galleryLeftToRight:
case ReaderMode.continuousLeftToRight:
if (isLeft) {
context.reader.toPrevPage();
} else if (isRight) {
context.reader.toNextPage();
} else {
isCenter = true;
}
case ReaderMode.galleryRightToLeft:
case ReaderMode.continuousRightToLeft:
if (isLeft) {
context.reader.toNextPage();
} else if (isRight) {
context.reader.toPrevPage();
} else {
isCenter = true;
}
case ReaderMode.galleryTopToBottom:
case ReaderMode.continuousTopToBottom:
if (isTop) {
context.reader.toPrevPage();
} else if (isBottom) {
context.reader.toNextPage();
} else {
isCenter = true;
}
}
if (!isCenter) {
return;
}
}
context.readerScaffold.openOrClose();
}
}
void onDoubleTap(Offset location) {
context.reader._imageViewController?.handleDoubleTap(location);
}
void onSecondaryTapUp(Offset location) {
showMenuX(
context,
location,
[
MenuEntry(
icon: Icons.settings,
text: "Settings".tl,
onClick: () {
context.readerScaffold.openSetting();
},
),
MenuEntry(
icon: Icons.menu,
text: "Chapters".tl,
onClick: () {
context.readerScaffold.openChapterDrawer();
},
),
MenuEntry(
icon: Icons.fullscreen,
text: "Fullscreen".tl,
onClick: () {
context.reader.fullscreen();
},
),
MenuEntry(
icon: Icons.exit_to_app,
text: "Exit".tl,
onClick: () {
context.pop();
},
),
],
);
}
void onLongPressedUp(Offset location) {
context.reader._imageViewController?.handleLongPressUp(location);
}
void onLongPressedDown(Offset location) {
context.reader._imageViewController?.handleLongPressDown(location);
}
void addDragListener(_DragListener listener) {
_dragListeners.add(listener);
}
void removeDragListener(_DragListener listener) {
_dragListeners.remove(listener);
}
}
class _DragListener {
void Function(Offset point)? onStart;
void Function(Offset offset)? onMove;
void Function()? onEnd;
_DragListener({this.onMove, this.onEnd});
}