mangayomi-mirror/lib/modules/manga/reader/mixins/reader_gestures.dart
Moustapha Kodjo Amadou 4e9af30e8e feat(reader): add page indicator, app bar, bottom bar, gesture handler, and settings modal
- PageIndicator widget to display current page and total pages.
- Created ReaderAppBar for navigation and chapter information.
- ReaderBottomBar for page navigation and settings access.
- Added ReaderGestureHandler for managing tap zones and gestures.
- ReaderSettingsModal for user-configurable settings.
2025-12-05 16:54:10 +01:00

212 lines
5.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Widget providing horizontal tap zones for reader navigation.
class HorizontalTapZones extends StatelessWidget {
/// Callback for left region tap.
final VoidCallback onLeftTap;
/// Callback for center region tap.
final VoidCallback onCenterTap;
/// Callback for right region tap.
final VoidCallback onRightTap;
/// Callback for double-tap with position.
final void Function(Offset position)? onDoubleTap;
/// Whether to show overlay for failed images.
final bool showFailedOverlay;
/// Widget to show when image failed to load.
final Widget? failedWidget;
const HorizontalTapZones({
super.key,
required this.onLeftTap,
required this.onCenterTap,
required this.onRightTap,
this.onDoubleTap,
this.showFailedOverlay = false,
this.failedWidget,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
// Left region (2 flex)
Expanded(
flex: 2,
child: _TapZone(onTap: onLeftTap, onDoubleTap: onDoubleTap),
),
// Center region (2 flex)
Expanded(
flex: 2,
child: showFailedOverlay && failedWidget != null
? failedWidget!
: _TapZone(onTap: onCenterTap, onDoubleTap: onDoubleTap),
),
// Right region (2 flex)
Expanded(
flex: 2,
child: _TapZone(onTap: onRightTap, onDoubleTap: onDoubleTap),
),
],
);
}
}
/// Widget providing vertical tap zones for reader navigation.
class VerticalTapZones extends StatelessWidget {
/// Callback for top region tap.
final VoidCallback onTopTap;
/// Callback for center region tap.
final VoidCallback onCenterTap;
/// Callback for bottom region tap.
final VoidCallback onBottomTap;
/// Callback for double-tap with position.
final void Function(Offset position)? onDoubleTap;
const VerticalTapZones({
super.key,
required this.onTopTap,
required this.onCenterTap,
required this.onBottomTap,
this.onDoubleTap,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Top region (2 flex)
Expanded(
flex: 2,
child: _TapZone(onTap: onTopTap, onDoubleTap: onDoubleTap),
),
// Center region (5 flex) - larger for viewing
const Expanded(flex: 5, child: SizedBox.shrink()),
// Bottom region (2 flex)
Expanded(
flex: 2,
child: _TapZone(onTap: onBottomTap, onDoubleTap: onDoubleTap),
),
],
);
}
}
class _TapZone extends StatelessWidget {
final VoidCallback onTap;
final void Function(Offset position)? onDoubleTap;
const _TapZone({required this.onTap, this.onDoubleTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onTap,
onDoubleTapDown: onDoubleTap != null
? (details) => onDoubleTap!(details.globalPosition)
: null,
onDoubleTap: onDoubleTap != null ? () {} : null,
onSecondaryTapDown: onDoubleTap != null
? (details) => onDoubleTap!(details.globalPosition)
: null,
onSecondaryTap: onDoubleTap != null ? () {} : null,
);
}
}
/// Handler for keyboard shortcuts in the reader.
class ReaderKeyboardHandler {
final VoidCallback? onEscape;
final VoidCallback? onFullScreen;
final VoidCallback? onPreviousPage;
final VoidCallback? onNextPage;
final VoidCallback? onNextChapter;
final VoidCallback? onPreviousChapter;
const ReaderKeyboardHandler({
this.onEscape,
this.onFullScreen,
this.onPreviousPage,
this.onNextPage,
this.onNextChapter,
this.onPreviousChapter,
});
/// Handles a key event and returns true if it was handled.
bool handleKeyEvent(KeyEvent event, {bool isReverseHorizontal = false}) {
if (event is! KeyDownEvent) return false;
switch (event.logicalKey) {
case LogicalKeyboardKey.f11:
onFullScreen?.call();
return true;
case LogicalKeyboardKey.escape:
case LogicalKeyboardKey.backspace:
onEscape?.call();
return true;
case LogicalKeyboardKey.arrowUp:
onPreviousPage?.call();
return true;
case LogicalKeyboardKey.arrowDown:
onNextPage?.call();
return true;
case LogicalKeyboardKey.arrowLeft:
if (isReverseHorizontal) {
onNextPage?.call();
} else {
onPreviousPage?.call();
}
return true;
case LogicalKeyboardKey.arrowRight:
if (isReverseHorizontal) {
onPreviousPage?.call();
} else {
onNextPage?.call();
}
return true;
case LogicalKeyboardKey.keyN:
case LogicalKeyboardKey.pageDown:
case LogicalKeyboardKey.shiftRight:
onNextChapter?.call();
return true;
case LogicalKeyboardKey.keyP:
case LogicalKeyboardKey.pageUp:
case LogicalKeyboardKey.shiftLeft:
onPreviousChapter?.call();
return true;
default:
return false;
}
}
/// Creates a KeyboardListener widget with this handler.
Widget wrapWithKeyboardListener({
required Widget child,
bool isReverseHorizontal = false,
FocusNode? focusNode,
}) {
return KeyboardListener(
autofocus: true,
focusNode: focusNode ?? FocusNode(),
onKeyEvent: (event) =>
handleKeyEvent(event, isReverseHorizontal: isReverseHorizontal),
child: child,
);
}
}