mangayomi-mirror/lib/modules/manga/reader/widgets/reader_gesture_handler.dart
Moustapha Kodjo Amadou 2f0fc85316 feat(reader): add navigation layout options and settings for screen behavior
- Implemented multiple navigation layouts for the reader, including L-shaped, Kindle, Edge, Right & Left, and Disabled modes.
- Added settings for keeping the screen on, showing page gaps, and adjusting webtoon side padding.
- Enhanced the reader settings modal to include new options and improved UI for navigation layout selection.
- Color filters: invert/gray/BCS
2026-03-04 16:04:07 +01:00

285 lines
8.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
/// Navigation layout variants matching Mihon.
///
/// 0 = Default current three-column + top/bottom zones
/// 1 = L-shaped top-left = prev, bottom-right = next, rest = UI
/// 2 = Kindle top = UI, bottom split left = prev / right = next
/// 3 = Edge thin side strips for prev/next, rest = UI
/// 4 = Right & Left simple two-zone left = prev / right = next
/// 5 = Disabled full screen = toggle UI
///
/// For horizontal reading (LTR), the default layout is:
/// ```
/// ┌─────────────────────────┐
/// │ TOP (prev page) │
/// ├───────┬───────┬─────────┤
/// │ LEFT │CENTER │ RIGHT │
/// │(prev) │ (UI) │ (next) │
/// ├───────┴───────┴─────────┤
/// │ BOTTOM (next page) │
/// └─────────────────────────┘
/// ```
///
/// For RTL mode, LEFT and RIGHT actions are reversed.
class ReaderGestureHandler extends StatelessWidget {
/// Whether tap zones are enabled for navigation
final bool usePageTapZones;
/// Whether the reader is in RTL mode
final bool isRTL;
/// Whether there's an image loading error
final bool hasImageError;
/// Whether the reader is in continuous scroll mode
final bool isContinuousMode;
/// Navigation layout index (0-5), see class docs.
final int navigationLayout;
/// Callback when UI should be toggled
final VoidCallback onToggleUI;
/// Callback to go to previous page
final VoidCallback onPreviousPage;
/// Callback to go to next page
final VoidCallback onNextPage;
/// Callback for double-tap to zoom (with position)
final void Function(Offset position)? onDoubleTapDown;
/// Callback for double-tap gesture complete
final VoidCallback? onDoubleTap;
/// Callback for secondary tap (right-click on desktop)
final void Function(Offset position)? onSecondaryTapDown;
/// Callback for secondary tap complete
final VoidCallback? onSecondaryTap;
const ReaderGestureHandler({
super.key,
required this.usePageTapZones,
required this.isRTL,
required this.hasImageError,
required this.isContinuousMode,
required this.onToggleUI,
required this.onPreviousPage,
required this.onNextPage,
this.navigationLayout = 0,
this.onDoubleTapDown,
this.onDoubleTap,
this.onSecondaryTapDown,
this.onSecondaryTap,
});
@override
Widget build(BuildContext context) {
return switch (navigationLayout) {
1 => _buildLShaped(context),
2 => _buildKindle(context),
3 => _buildEdge(context),
4 => _buildRightAndLeft(context),
5 => _buildDisabled(context),
_ => _buildDefault(context),
};
}
// ── helpers ──
VoidCallback _prev() => isRTL ? onNextPage : onPreviousPage;
VoidCallback _next() => isRTL ? onPreviousPage : onNextPage;
_ZoneGestureDetector _zone(VoidCallback onTap) => _ZoneGestureDetector(
onTap: usePageTapZones ? onTap : onToggleUI,
onDoubleTapDown: isContinuousMode ? onDoubleTapDown : null,
onDoubleTap: isContinuousMode ? onDoubleTap : null,
onSecondaryTapDown: isContinuousMode ? onSecondaryTapDown : null,
onSecondaryTap: isContinuousMode ? onSecondaryTap : null,
);
_ZoneGestureDetector _uiZone() => _ZoneGestureDetector(
onTap: onToggleUI,
onDoubleTapDown: isContinuousMode ? onDoubleTapDown : null,
onDoubleTap: isContinuousMode ? onDoubleTap : null,
onSecondaryTapDown: isContinuousMode ? onSecondaryTapDown : null,
onSecondaryTap: isContinuousMode ? onSecondaryTap : null,
);
// ── Layout 0: Default (original 3-col + top/bottom) ──
Widget _buildDefault(BuildContext context) {
return Stack(
children: [
_buildDefaultHorizontalZones(context),
_buildDefaultVerticalZones(context),
],
);
}
Widget _buildDefaultHorizontalZones(BuildContext context) {
return Row(
children: [
Expanded(flex: 2, child: _zone(_prev())),
Expanded(
flex: 2,
child: hasImageError
? SizedBox(width: context.width(1), height: context.height(0.7))
: _uiZone(),
),
Expanded(flex: 2, child: _zone(_next())),
],
);
}
Widget _buildDefaultVerticalZones(BuildContext context) {
return Column(
children: [
Expanded(
flex: 2,
child: _zone(hasImageError ? onToggleUI : onPreviousPage),
),
const Expanded(flex: 5, child: SizedBox.shrink()),
Expanded(
flex: 2,
child: _zone(hasImageError ? onToggleUI : onNextPage),
),
],
);
}
// ── Layout 1: L-shaped ──
// ┌───────┬───────────────┐
// │ PREV │ │
// ├───────┘ │
// │ UI │
// │ ┌───────┤
// │ │ NEXT │
// └───────────────┴───────┘
Widget _buildLShaped(BuildContext context) {
return Column(
children: [
Expanded(
flex: 1,
child: Row(
children: [
Expanded(flex: 1, child: _zone(_prev())),
Expanded(flex: 2, child: _uiZone()),
],
),
),
Expanded(flex: 2, child: _uiZone()),
Expanded(
flex: 1,
child: Row(
children: [
Expanded(flex: 2, child: _uiZone()),
Expanded(flex: 1, child: _zone(_next())),
],
),
),
],
);
}
// ── Layout 2: Kindle ──
// ┌───────────────────────┐
// │ UI (toggle) │
// ├───────────┬───────────┤
// │ PREV │ NEXT │
// └───────────┴───────────┘
Widget _buildKindle(BuildContext context) {
return Column(
children: [
Expanded(flex: 1, child: _uiZone()),
Expanded(
flex: 3,
child: Row(
children: [
Expanded(child: _zone(_prev())),
Expanded(child: _zone(_next())),
],
),
),
],
);
}
// ── Layout 3: Edge ──
// ┌──┬──────────────┬──┐
// │P │ │N │
// │R │ UI │E │
// │E │ (toggle) │X │
// │V │ │T │
// └──┴──────────────┴──┘
Widget _buildEdge(BuildContext context) {
return Row(
children: [
Expanded(flex: 1, child: _zone(_prev())),
Expanded(flex: 5, child: _uiZone()),
Expanded(flex: 1, child: _zone(_next())),
],
);
}
// ── Layout 4: Right and Left ──
// ┌───────────┬───────────┐
// │ │ │
// │ PREV │ NEXT │
// │ │ │
// └───────────┴───────────┘
Widget _buildRightAndLeft(BuildContext context) {
return Row(
children: [
Expanded(child: _zone(_prev())),
Expanded(child: _zone(_next())),
],
);
}
// ── Layout 5: Disabled ──
Widget _buildDisabled(BuildContext context) {
return SizedBox.expand(child: _uiZone());
}
}
/// Individual gesture detector for a zone.
class _ZoneGestureDetector extends StatelessWidget {
final VoidCallback onTap;
final void Function(Offset position)? onDoubleTapDown;
final VoidCallback? onDoubleTap;
final void Function(Offset position)? onSecondaryTapDown;
final VoidCallback? onSecondaryTap;
const _ZoneGestureDetector({
required this.onTap,
this.onDoubleTapDown,
this.onDoubleTap,
this.onSecondaryTapDown,
this.onSecondaryTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onTap,
onDoubleTapDown: onDoubleTapDown != null
? (details) => onDoubleTapDown!(details.globalPosition)
: null,
onDoubleTap: onDoubleTap,
onSecondaryTapDown: onSecondaryTapDown != null
? (details) => onSecondaryTapDown!(details.globalPosition)
: null,
onSecondaryTap: onSecondaryTap,
);
}
}