viewer: keyboard shortcuts for navigation

This commit is contained in:
Thibault Deckers 2022-02-24 18:00:09 +09:00
parent 365ddc0f92
commit fc1234ca63
3 changed files with 106 additions and 50 deletions

View file

@ -84,7 +84,7 @@ mixin FeedbackMixin {
itemCount: itemCount, itemCount: itemCount,
onCancel: onCancel, onCancel: onCancel,
onDone: (processed) { onDone: (processed) {
Navigator.of(context).pop(); Navigator.pop(context);
onDone?.call(processed); onDone?.call(processed);
}, },
), ),

View file

@ -11,6 +11,7 @@ import 'package:aves/widgets/viewer/entry_horizontal_pager.dart';
import 'package:aves/widgets/viewer/info/info_page.dart'; import 'package:aves/widgets/viewer/info/info_page.dart';
import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:aves/widgets/viewer/info/notifications.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
class ViewerVerticalPageView extends StatefulWidget { class ViewerVerticalPageView extends StatefulWidget {
@ -93,18 +94,7 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
// fake page for opacity transition between collection and viewer // fake page for opacity transition between collection and viewer
const transitionPage = SizedBox(); const transitionPage = SizedBox();
final imagePage = hasCollection final imagePage = _buildImagePage();
? MultiEntryScroller(
collection: collection!,
pageController: widget.horizontalPager,
onPageChanged: widget.onHorizontalPageChanged,
onViewDisposed: widget.onViewDisposed,
)
: entry != null
? SingleEntryScroller(
entry: entry!,
)
: const SizedBox();
final infoPage = NotificationListener<ShowImageNotification>( final infoPage = NotificationListener<ShowImageNotification>(
onNotification: (notification) { onNotification: (notification) {
@ -150,6 +140,58 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
); );
} }
Widget _buildImagePage() {
Widget? child;
Map<ShortcutActivator, Intent>? shortcuts;
if (hasCollection) {
child = MultiEntryScroller(
collection: collection!,
pageController: widget.horizontalPager,
onPageChanged: widget.onHorizontalPageChanged,
onViewDisposed: widget.onViewDisposed,
);
shortcuts = const {
SingleActivator(LogicalKeyboardKey.arrowLeft): ShowPreviousIntent(),
SingleActivator(LogicalKeyboardKey.arrowRight): ShowNextIntent(),
SingleActivator(LogicalKeyboardKey.arrowUp): LeaveIntent(),
SingleActivator(LogicalKeyboardKey.arrowDown): ShowInfoIntent(),
};
} else if (entry != null) {
child = SingleEntryScroller(
entry: entry!,
);
shortcuts = const {
SingleActivator(LogicalKeyboardKey.arrowUp): LeaveIntent(),
SingleActivator(LogicalKeyboardKey.arrowDown): ShowInfoIntent(),
};
}
if (child != null) {
return FocusableActionDetector(
autofocus: true,
shortcuts: shortcuts,
actions: {
ShowPreviousIntent: CallbackAction<Intent>(onInvoke: (intent) => _jumpHorizontalPage(-1)),
ShowNextIntent: CallbackAction<Intent>(onInvoke: (intent) => _jumpHorizontalPage(1)),
LeaveIntent: CallbackAction<Intent>(onInvoke: (intent) => Navigator.pop(context)),
ShowInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => ShowInfoNotification().dispatch(context)),
},
child: child,
);
}
return const SizedBox();
}
void _jumpHorizontalPage(int delta) {
final pageController = widget.horizontalPager;
final page = pageController.page?.round();
final _collection = collection;
if (page != null && _collection != null) {
final target = (page + delta).clamp(0, _collection.entryCount - 1);
pageController.jumpToPage(target);
}
}
void _onVerticalPageControllerChanged() { void _onVerticalPageControllerChanged() {
final page = widget.verticalPager.page!; final page = widget.verticalPager.page!;
@ -205,3 +247,21 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
} }
} }
} }
// keyboard shortcut intents
class ShowPreviousIntent extends Intent {
const ShowPreviousIntent();
}
class ShowNextIntent extends Intent {
const ShowNextIntent();
}
class LeaveIntent extends Intent {
const LeaveIntent();
}
class ShowInfoIntent extends Intent {
const ShowInfoIntent();
}

View file

@ -198,32 +198,34 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
_goToCollection(notification.filter); _goToCollection(notification.filter);
} else if (notification is EntryRemovedNotification) { } else if (notification is EntryRemovedNotification) {
_onEntryRemoved(context, notification.entry); _onEntryRemoved(context, notification.entry);
} } else if (notification is ToggleOverlayNotification) {
return false;
},
child: NotificationListener<ToggleOverlayNotification>(
onNotification: (notification) {
_overlayVisible.value = notification.visible ?? !_overlayVisible.value; _overlayVisible.value = notification.visible ?? !_overlayVisible.value;
return true; } else if (notification is ShowInfoNotification) {
}, // remove focus, if any, to prevent viewer shortcuts activation from the Info page
child: Stack( FocusManager.instance.primaryFocus?.unfocus();
children: [ _goToVerticalPage(infoPage);
ViewerVerticalPageView( } else {
collection: collection, return false;
entryNotifier: _entryNotifier, }
verticalPager: _verticalPager, return true;
horizontalPager: _horizontalPager, },
onVerticalPageChanged: _onVerticalPageChanged, child: Stack(
onHorizontalPageChanged: _onHorizontalPageChanged, children: [
onImagePageRequested: () => _goToVerticalPage(imagePage), ViewerVerticalPageView(
onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry), collection: collection,
), entryNotifier: _entryNotifier,
_buildTopOverlay(), verticalPager: _verticalPager,
_buildBottomOverlay(), horizontalPager: _horizontalPager,
const SideGestureAreaProtector(), onVerticalPageChanged: _onVerticalPageChanged,
const BottomGestureAreaProtector(), onHorizontalPageChanged: _onHorizontalPageChanged,
], onImagePageRequested: () => _goToVerticalPage(imagePage),
), onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry),
),
_buildTopOverlay(),
_buildBottomOverlay(),
const SideGestureAreaProtector(),
const BottomGestureAreaProtector(),
],
), ),
), ),
), ),
@ -249,18 +251,12 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
); );
} }
return NotificationListener<ShowInfoNotification>( return mainEntry.isMultiPage
onNotification: (notification) { ? PageEntryBuilder(
_goToVerticalPage(infoPage); multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
return true; builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
}, )
child: mainEntry.isMultiPage : _buildContent();
? PageEntryBuilder(
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
)
: _buildContent(),
);
}, },
); );