feat: show real neighboring pages during back and forward swipes
This commit is contained in:
parent
4e07c9a305
commit
c104c2026b
7 changed files with 283 additions and 71 deletions
|
|
@ -12,10 +12,10 @@ import 'reader/reader_screen.dart';
|
|||
/// Everything is local-first: the database is created up front and the fetch
|
||||
/// actor is the only path to the network.
|
||||
class AppDependencies {
|
||||
AppDependencies({MewsDatabase? database})
|
||||
AppDependencies({MewsDatabase? database, Actor<FetchCommand, FetchResult>? fetch})
|
||||
: db = database ?? MewsDatabase(),
|
||||
fetch = Actor.create(FetchHandler.new) {
|
||||
browser = BrowserController(db: db, fetch: fetch);
|
||||
fetch = fetch ?? Actor.create(FetchHandler.new) {
|
||||
browser = BrowserController(db: db, fetch: this.fetch);
|
||||
settings = ReaderSettingsController(db);
|
||||
library = LibraryController(db);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,8 +100,10 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
|||
return SwipeNavigator(
|
||||
canGoBack: browser.canGoBack,
|
||||
canGoForward: browser.canGoForward,
|
||||
backTitle: browser.backTitle.value,
|
||||
forwardTitle: browser.forwardTitle.value,
|
||||
backView: _neighborView(browser.backPage.value),
|
||||
forwardView: _neighborView(browser.forwardPage.value),
|
||||
backTitle: browser.backPage.value?.title,
|
||||
forwardTitle: browser.forwardPage.value?.title,
|
||||
background: peekBackground,
|
||||
onBack: browser.back,
|
||||
onForward: browser.forward,
|
||||
|
|
@ -179,6 +181,31 @@ class _ReaderScreenState extends State<ReaderScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Renders a neighboring page for the swipe peek, non-interactive: it is a
|
||||
/// look ahead, and committing the navigation brings the real thing.
|
||||
Widget? _neighborView(MewsPage? page) {
|
||||
if (page == null) return null;
|
||||
final deps = AppScope.of(context).deps;
|
||||
final settings = deps.settings;
|
||||
final palette = paletteFor(
|
||||
pageVariant: page.variant,
|
||||
readerVariant: settings.variant.value,
|
||||
dark: Theme.of(context).brightness == Brightness.dark,
|
||||
);
|
||||
return ColoredBox(
|
||||
color: palette.background,
|
||||
child: MewsPageView(
|
||||
page: page,
|
||||
palette: palette,
|
||||
fontFamily: settings.font.value,
|
||||
fontScale: settings.fontScale.value,
|
||||
visitedUrls: deps.browser.visitedUrls.value,
|
||||
onLinkTap: (_) {},
|
||||
onFormSubmit: (_, _) {},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _emptyPage(BuildContext context) {
|
||||
// No page yet: the address bar is the whole UI.
|
||||
return const SizedBox.expand();
|
||||
|
|
|
|||
|
|
@ -4,27 +4,37 @@ import 'package:flutter/material.dart';
|
|||
|
||||
/// Wraps the current page and turns the back/forward swipes into visible page
|
||||
/// transitions: dragging slides the current page away and reveals the
|
||||
/// neighboring page peeking in from the side (its title, when known).
|
||||
/// Releasing past the threshold or with a flick commits the navigation;
|
||||
/// otherwise everything springs back. Directions without a page resist the
|
||||
/// drag with a rubber band.
|
||||
/// neighboring page itself ([backView]/[forwardView]) sliding in from the
|
||||
/// side, falling back to a title panel while the neighbor is still being
|
||||
/// parsed. Releasing past the threshold or with a flick commits the
|
||||
/// navigation; otherwise everything springs back. Directions without a page
|
||||
/// resist the drag with a rubber band.
|
||||
class SwipeNavigator extends StatefulWidget {
|
||||
const SwipeNavigator({
|
||||
required this.canGoBack,
|
||||
required this.canGoForward,
|
||||
required this.backTitle,
|
||||
required this.forwardTitle,
|
||||
required this.background,
|
||||
required this.onBack,
|
||||
required this.onForward,
|
||||
required this.child,
|
||||
this.backView,
|
||||
this.forwardView,
|
||||
this.backTitle,
|
||||
this.forwardTitle,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool canGoBack;
|
||||
final bool canGoForward;
|
||||
|
||||
/// Pre-rendered neighboring pages, shown while swiping.
|
||||
final Widget? backView;
|
||||
final Widget? forwardView;
|
||||
|
||||
/// Fallback titles for the neighbor panels while the pages are parsed.
|
||||
final String? backTitle;
|
||||
final String? forwardTitle;
|
||||
|
||||
final Color background;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback onForward;
|
||||
|
|
@ -135,14 +145,21 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
|||
// rubber-band the current page over the bare background.
|
||||
final showsBack = dx > 0 && widget.canGoBack;
|
||||
final showsForward = dx < 0 && widget.canGoForward;
|
||||
final peek = showsBack || showsForward
|
||||
? _Peek(
|
||||
fromLeft: showsBack,
|
||||
offset: showsBack ? dx - _width : _width + dx,
|
||||
background: widget.background,
|
||||
title: showsBack ? widget.backTitle : widget.forwardTitle,
|
||||
final Widget? peek = showsBack || showsForward
|
||||
? Transform.translate(
|
||||
offset: Offset(showsBack ? dx - _width : _width + dx, 0),
|
||||
child: IgnorePointer(
|
||||
// The neighbor is a look ahead, not an interactive page;
|
||||
// committing the navigation brings the real one.
|
||||
child: (showsBack ? widget.backView : widget.forwardView) ??
|
||||
_Peek(
|
||||
fromLeft: showsBack,
|
||||
background: widget.background,
|
||||
title: showsBack ? widget.backTitle : widget.forwardTitle,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
: null;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
|
|
@ -151,7 +168,7 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
|||
onHorizontalDragEnd: _onDragEnd,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: peek),
|
||||
if (peek != null) Positioned.fill(child: peek),
|
||||
Transform.translate(
|
||||
offset: Offset(dx, 0),
|
||||
child: widget.child,
|
||||
|
|
@ -164,52 +181,47 @@ class _SwipeNavigatorState extends State<SwipeNavigator>
|
|||
}
|
||||
}
|
||||
|
||||
/// The neighboring page sliding in under the current one.
|
||||
/// The neighboring page's fallback panel while it is still being parsed.
|
||||
class _Peek extends StatelessWidget {
|
||||
const _Peek({
|
||||
required this.fromLeft,
|
||||
required this.offset,
|
||||
required this.background,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final bool fromLeft;
|
||||
final double offset;
|
||||
final Color background;
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform.translate(
|
||||
offset: Offset(offset, 0),
|
||||
child: ColoredBox(
|
||||
color: background,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Align(
|
||||
alignment: fromLeft ? Alignment.centerLeft : Alignment.centerRight,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: fromLeft
|
||||
? CrossAxisAlignment.start
|
||||
: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Icon(
|
||||
fromLeft ? Icons.west : Icons.east,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
if (title != null)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
child: Text(
|
||||
title!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
return ColoredBox(
|
||||
color: background,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Align(
|
||||
alignment: fromLeft ? Alignment.centerLeft : Alignment.centerRight,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: fromLeft
|
||||
? CrossAxisAlignment.start
|
||||
: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Icon(
|
||||
fromLeft ? Icons.west : Icons.east,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
if (title != null)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
child: Text(
|
||||
title!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue