Fixed feed and back gesture issue
Browse files
.gitignore
CHANGED
|
@@ -43,4 +43,6 @@ graph/
|
|
| 43 |
# Python / Agent
|
| 44 |
|
| 45 |
context.md
|
| 46 |
-
i-want-you-to-spicy-dusk.md
|
|
|
|
|
|
|
|
|
| 43 |
# Python / Agent
|
| 44 |
|
| 45 |
context.md
|
| 46 |
+
i-want-you-to-spicy-dusk.md
|
| 47 |
+
CACHY_OVERVIEW.md
|
| 48 |
+
presenter
|
app/lib/ui/core/home_shell.dart
CHANGED
|
@@ -14,7 +14,7 @@ import 'package:provider/provider.dart';
|
|
| 14 |
import '../features/actions/views/actions_screen.dart';
|
| 15 |
import '../features/capture/views/capture_sheet.dart';
|
| 16 |
import '../features/collections/views/collections_screen.dart';
|
| 17 |
-
import '../features/
|
| 18 |
import '../features/library/view_models/library_view_model.dart';
|
| 19 |
import '../features/library/views/library_screen.dart';
|
| 20 |
import '../features/profile/views/profile_screen.dart';
|
|
@@ -41,7 +41,7 @@ const _navItems = [
|
|
| 41 |
_NavDef(icon: PhosphorIconsRegular.house, activeIcon: PhosphorIconsFill.house, label: 'HOME'),
|
| 42 |
_NavDef(icon: PhosphorIconsRegular.folder, activeIcon: PhosphorIconsFill.folder, label: 'FOLDERS'),
|
| 43 |
_NavDef(icon: PhosphorIconsRegular.listChecks, activeIcon: PhosphorIconsFill.listChecks, label: 'TO-DO'),
|
| 44 |
-
_NavDef(icon: PhosphorIconsRegular.
|
| 45 |
_NavDef(icon: PhosphorIconsRegular.user, activeIcon: PhosphorIconsFill.user, label: 'YOU'),
|
| 46 |
];
|
| 47 |
|
|
@@ -61,7 +61,7 @@ class _HomeShellState extends State<HomeShell> {
|
|
| 61 |
LibraryScreen(), // 0 β HOME
|
| 62 |
CollectionsScreen(), // 1 β FOLDERS
|
| 63 |
ActionsScreen(), // 2 β TO-DO
|
| 64 |
-
|
| 65 |
ProfileScreen(), // 4 β YOU
|
| 66 |
];
|
| 67 |
|
|
|
|
| 14 |
import '../features/actions/views/actions_screen.dart';
|
| 15 |
import '../features/capture/views/capture_sheet.dart';
|
| 16 |
import '../features/collections/views/collections_screen.dart';
|
| 17 |
+
import '../features/feed/views/knowledge_feed_screen.dart';
|
| 18 |
import '../features/library/view_models/library_view_model.dart';
|
| 19 |
import '../features/library/views/library_screen.dart';
|
| 20 |
import '../features/profile/views/profile_screen.dart';
|
|
|
|
| 41 |
_NavDef(icon: PhosphorIconsRegular.house, activeIcon: PhosphorIconsFill.house, label: 'HOME'),
|
| 42 |
_NavDef(icon: PhosphorIconsRegular.folder, activeIcon: PhosphorIconsFill.folder, label: 'FOLDERS'),
|
| 43 |
_NavDef(icon: PhosphorIconsRegular.listChecks, activeIcon: PhosphorIconsFill.listChecks, label: 'TO-DO'),
|
| 44 |
+
_NavDef(icon: PhosphorIconsRegular.cardsThree, activeIcon: PhosphorIconsFill.cardsThree, label: 'FEED'),
|
| 45 |
_NavDef(icon: PhosphorIconsRegular.user, activeIcon: PhosphorIconsFill.user, label: 'YOU'),
|
| 46 |
];
|
| 47 |
|
|
|
|
| 61 |
LibraryScreen(), // 0 β HOME
|
| 62 |
CollectionsScreen(), // 1 β FOLDERS
|
| 63 |
ActionsScreen(), // 2 β TO-DO
|
| 64 |
+
KnowledgeFeedScreen(inShell: true), // 3 β FEED
|
| 65 |
ProfileScreen(), // 4 β YOU
|
| 66 |
];
|
| 67 |
|
app/lib/ui/core/ios_back_gesture.dart
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// A genuine iOS-style interactive back gesture for all platforms.
|
| 2 |
+
///
|
| 3 |
+
/// Unlike a plain "swipe β pop" (which fires instantly with no feedback), this
|
| 4 |
+
/// drives the route's transition animation directly: the top screen tracks your
|
| 5 |
+
/// finger, the previous screen parallax-slides in behind it, and the gesture
|
| 6 |
+
/// commits (pops) or springs back based on how far / how fast you dragged β the
|
| 7 |
+
/// real iOS "back screen" feel.
|
| 8 |
+
///
|
| 9 |
+
/// Adapted from Flutter's private CupertinoPageTransitionsBuilder internals
|
| 10 |
+
/// (flutter/lib/src/cupertino/route.dart), with two deliberate changes:
|
| 11 |
+
/// 1. it's used on every platform (the app wants iOS transitions everywhere), and
|
| 12 |
+
/// 2. the edge activation zone is a little wider than iOS's 20dp for an easier
|
| 13 |
+
/// grab β but still EDGE-based, so it never fights horizontal content like
|
| 14 |
+
/// the library's tab swipe or horizontal lists.
|
| 15 |
+
library;
|
| 16 |
+
|
| 17 |
+
import 'dart:math';
|
| 18 |
+
import 'dart:ui' show lerpDouble;
|
| 19 |
+
|
| 20 |
+
import 'package:flutter/cupertino.dart';
|
| 21 |
+
import 'package:flutter/gestures.dart';
|
| 22 |
+
|
| 23 |
+
/// Width of the left-edge zone that starts the back gesture. iOS uses ~20; a bit
|
| 24 |
+
/// wider is easier to hit without interfering with centre-screen content.
|
| 25 |
+
const double _kBackGestureWidth = 44.0;
|
| 26 |
+
const double _kMinFlingVelocity = 1.0; // screen widths per second
|
| 27 |
+
const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // ms
|
| 28 |
+
const int _kMaxPageBackAnimationTime = 300; // ms
|
| 29 |
+
|
| 30 |
+
/// Routes whose back gesture is mid-drag β used to switch the transition to a
|
| 31 |
+
/// linear curve so it tracks the finger 1:1 while dragging.
|
| 32 |
+
final Set<PageRoute<dynamic>> _gesturesInProgress = <PageRoute<dynamic>>{};
|
| 33 |
+
|
| 34 |
+
class IOSPageTransitionsBuilder extends PageTransitionsBuilder {
|
| 35 |
+
const IOSPageTransitionsBuilder();
|
| 36 |
+
|
| 37 |
+
@override
|
| 38 |
+
Widget buildTransitions<T>(
|
| 39 |
+
PageRoute<T> route,
|
| 40 |
+
BuildContext context,
|
| 41 |
+
Animation<double> animation,
|
| 42 |
+
Animation<double> secondaryAnimation,
|
| 43 |
+
Widget child,
|
| 44 |
+
) {
|
| 45 |
+
return CupertinoPageTransition(
|
| 46 |
+
primaryRouteAnimation: animation,
|
| 47 |
+
secondaryRouteAnimation: secondaryAnimation,
|
| 48 |
+
// Linear (no easing) while the user is actively dragging so the page
|
| 49 |
+
// stays glued to the finger; eased otherwise.
|
| 50 |
+
linearTransition: _gesturesInProgress.contains(route),
|
| 51 |
+
child: _BackGestureDetector<T>(
|
| 52 |
+
enabledCallback: () => _isPopGestureEnabled<T>(route),
|
| 53 |
+
onStartPopGesture: () => _startPopGesture<T>(route),
|
| 54 |
+
child: child,
|
| 55 |
+
),
|
| 56 |
+
);
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
bool _isPopGestureEnabled<T>(PageRoute<T> route) {
|
| 61 |
+
// Can't go back from the first route, or one handling its own pop, or a
|
| 62 |
+
// fullscreen dialog; and only when the route is settled and idle.
|
| 63 |
+
if (route.isFirst) return false;
|
| 64 |
+
if (route.willHandlePopInternally) return false;
|
| 65 |
+
if (route.popDisposition == RoutePopDisposition.doNotPop) return false;
|
| 66 |
+
if (route.fullscreenDialog) return false;
|
| 67 |
+
if (route.animation!.status != AnimationStatus.completed) return false;
|
| 68 |
+
if (route.secondaryAnimation!.status != AnimationStatus.dismissed) return false;
|
| 69 |
+
if (_gesturesInProgress.contains(route)) return false;
|
| 70 |
+
return true;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
_BackGestureController<T> _startPopGesture<T>(PageRoute<T> route) {
|
| 74 |
+
_gesturesInProgress.add(route);
|
| 75 |
+
return _BackGestureController<T>(
|
| 76 |
+
navigator: route.navigator!,
|
| 77 |
+
// The transition animation the route is driven by. Protected on
|
| 78 |
+
// TransitionRoute, but reading it here mirrors what Cupertino does internally.
|
| 79 |
+
// ignore: invalid_use_of_protected_member
|
| 80 |
+
controller: route.controller!,
|
| 81 |
+
onEnded: () => _gesturesInProgress.remove(route),
|
| 82 |
+
);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/// Drives the route's [AnimationController] from drag input, then settles it
|
| 86 |
+
/// forward (cancel) or in reverse (pop) on release. Mirrors Cupertino's
|
| 87 |
+
/// `_CupertinoBackGestureController`.
|
| 88 |
+
class _BackGestureController<T> {
|
| 89 |
+
_BackGestureController({
|
| 90 |
+
required this.navigator,
|
| 91 |
+
required this.controller,
|
| 92 |
+
required this.onEnded,
|
| 93 |
+
}) {
|
| 94 |
+
navigator.didStartUserGesture();
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
final AnimationController controller;
|
| 98 |
+
final NavigatorState navigator;
|
| 99 |
+
final VoidCallback onEnded;
|
| 100 |
+
|
| 101 |
+
/// Fraction of screen width dragged this frame (positive = toward dismiss).
|
| 102 |
+
void dragUpdate(double delta) {
|
| 103 |
+
controller.value -= delta;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
void dragEnd(double velocity) {
|
| 107 |
+
const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
|
| 108 |
+
|
| 109 |
+
final bool animateForward;
|
| 110 |
+
if (velocity.abs() >= _kMinFlingVelocity) {
|
| 111 |
+
// A fling: direction wins over position.
|
| 112 |
+
animateForward = velocity <= 0;
|
| 113 |
+
} else {
|
| 114 |
+
// Released: past the halfway point commits the pop.
|
| 115 |
+
animateForward = controller.value > 0.5;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
if (animateForward) {
|
| 119 |
+
final int droppedPageForwardAnimationTime = min(
|
| 120 |
+
lerpDouble(
|
| 121 |
+
_kMaxDroppedSwipePageForwardAnimationTime,
|
| 122 |
+
0,
|
| 123 |
+
controller.value,
|
| 124 |
+
)!.floor(),
|
| 125 |
+
_kMaxPageBackAnimationTime,
|
| 126 |
+
);
|
| 127 |
+
controller.animateTo(
|
| 128 |
+
1.0,
|
| 129 |
+
duration: Duration(milliseconds: droppedPageForwardAnimationTime),
|
| 130 |
+
curve: animationCurve,
|
| 131 |
+
);
|
| 132 |
+
} else {
|
| 133 |
+
navigator.pop();
|
| 134 |
+
if (controller.isAnimating) {
|
| 135 |
+
final int droppedPageBackAnimationTime = lerpDouble(
|
| 136 |
+
0,
|
| 137 |
+
_kMaxDroppedSwipePageForwardAnimationTime,
|
| 138 |
+
controller.value,
|
| 139 |
+
)!.floor();
|
| 140 |
+
controller.animateBack(
|
| 141 |
+
0.0,
|
| 142 |
+
duration: Duration(milliseconds: droppedPageBackAnimationTime),
|
| 143 |
+
curve: animationCurve,
|
| 144 |
+
);
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
if (controller.isAnimating) {
|
| 149 |
+
late AnimationStatusListener animationStatusCallback;
|
| 150 |
+
animationStatusCallback = (AnimationStatus status) {
|
| 151 |
+
_finish();
|
| 152 |
+
controller.removeStatusListener(animationStatusCallback);
|
| 153 |
+
};
|
| 154 |
+
controller.addStatusListener(animationStatusCallback);
|
| 155 |
+
} else {
|
| 156 |
+
_finish();
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
void _finish() {
|
| 161 |
+
onEnded();
|
| 162 |
+
navigator.didStopUserGesture();
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/// Left-edge drag recognizer that hands input to a [_BackGestureController].
|
| 167 |
+
class _BackGestureDetector<T> extends StatefulWidget {
|
| 168 |
+
const _BackGestureDetector({
|
| 169 |
+
required this.enabledCallback,
|
| 170 |
+
required this.onStartPopGesture,
|
| 171 |
+
required this.child,
|
| 172 |
+
});
|
| 173 |
+
|
| 174 |
+
final ValueGetter<bool> enabledCallback;
|
| 175 |
+
final ValueGetter<_BackGestureController<T>> onStartPopGesture;
|
| 176 |
+
final Widget child;
|
| 177 |
+
|
| 178 |
+
@override
|
| 179 |
+
State<_BackGestureDetector<T>> createState() => _BackGestureDetectorState<T>();
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
class _BackGestureDetectorState<T> extends State<_BackGestureDetector<T>> {
|
| 183 |
+
_BackGestureController<T>? _backGestureController;
|
| 184 |
+
late HorizontalDragGestureRecognizer _recognizer;
|
| 185 |
+
|
| 186 |
+
@override
|
| 187 |
+
void initState() {
|
| 188 |
+
super.initState();
|
| 189 |
+
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
|
| 190 |
+
..onStart = _handleDragStart
|
| 191 |
+
..onUpdate = _handleDragUpdate
|
| 192 |
+
..onEnd = _handleDragEnd
|
| 193 |
+
..onCancel = _handleDragCancel;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
@override
|
| 197 |
+
void dispose() {
|
| 198 |
+
_recognizer.dispose();
|
| 199 |
+
super.dispose();
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
void _handleDragStart(DragStartDetails details) {
|
| 203 |
+
_backGestureController = widget.onStartPopGesture();
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
void _handleDragUpdate(DragUpdateDetails details) {
|
| 207 |
+
_backGestureController?.dragUpdate(
|
| 208 |
+
_convertToLogical(details.primaryDelta! / context.size!.width),
|
| 209 |
+
);
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
void _handleDragEnd(DragEndDetails details) {
|
| 213 |
+
_backGestureController?.dragEnd(
|
| 214 |
+
_convertToLogical(details.velocity.pixelsPerSecond.dx / context.size!.width),
|
| 215 |
+
);
|
| 216 |
+
_backGestureController = null;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
void _handleDragCancel() {
|
| 220 |
+
_backGestureController?.dragEnd(0.0);
|
| 221 |
+
_backGestureController = null;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
void _handlePointerDown(PointerDownEvent event) {
|
| 225 |
+
if (widget.enabledCallback()) {
|
| 226 |
+
_recognizer.addPointer(event);
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
double _convertToLogical(double value) {
|
| 231 |
+
return switch (Directionality.of(context)) {
|
| 232 |
+
TextDirection.rtl => -value,
|
| 233 |
+
TextDirection.ltr => value,
|
| 234 |
+
};
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
@override
|
| 238 |
+
Widget build(BuildContext context) {
|
| 239 |
+
final double dragAreaWidth = Directionality.of(context) == TextDirection.ltr
|
| 240 |
+
? MediaQuery.paddingOf(context).left + _kBackGestureWidth
|
| 241 |
+
: MediaQuery.paddingOf(context).right + _kBackGestureWidth;
|
| 242 |
+
|
| 243 |
+
return Stack(
|
| 244 |
+
fit: StackFit.passthrough,
|
| 245 |
+
children: [
|
| 246 |
+
widget.child,
|
| 247 |
+
PositionedDirectional(
|
| 248 |
+
start: 0,
|
| 249 |
+
width: dragAreaWidth,
|
| 250 |
+
top: 0,
|
| 251 |
+
bottom: 0,
|
| 252 |
+
child: Listener(
|
| 253 |
+
onPointerDown: _handlePointerDown,
|
| 254 |
+
behavior: HitTestBehavior.translucent,
|
| 255 |
+
),
|
| 256 |
+
),
|
| 257 |
+
],
|
| 258 |
+
);
|
| 259 |
+
}
|
| 260 |
+
}
|
app/lib/ui/core/theme.dart
CHANGED
|
@@ -5,6 +5,7 @@ library;
|
|
| 5 |
import 'package:flutter/material.dart';
|
| 6 |
|
| 7 |
import 'brand.dart';
|
|
|
|
| 8 |
|
| 9 |
class AppTheme {
|
| 10 |
static ThemeData light() => _build(Brightness.light);
|
|
@@ -165,59 +166,5 @@ class Insets {
|
|
| 165 |
static const splitPane = 1100.0;
|
| 166 |
}
|
| 167 |
|
| 168 |
-
//
|
| 169 |
-
//
|
| 170 |
-
class IOSPageTransitionsBuilder extends PageTransitionsBuilder {
|
| 171 |
-
const IOSPageTransitionsBuilder();
|
| 172 |
-
|
| 173 |
-
@override
|
| 174 |
-
Widget buildTransitions<T>(
|
| 175 |
-
PageRoute<T> route,
|
| 176 |
-
BuildContext context,
|
| 177 |
-
Animation<double> animation,
|
| 178 |
-
Animation<double> secondaryAnimation,
|
| 179 |
-
Widget child,
|
| 180 |
-
) {
|
| 181 |
-
return const CupertinoPageTransitionsBuilder().buildTransitions<T>(
|
| 182 |
-
route,
|
| 183 |
-
context,
|
| 184 |
-
animation,
|
| 185 |
-
secondaryAnimation,
|
| 186 |
-
_SwipeBackGate(child: child),
|
| 187 |
-
);
|
| 188 |
-
}
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
class _SwipeBackGate extends StatefulWidget {
|
| 192 |
-
const _SwipeBackGate({required this.child});
|
| 193 |
-
final Widget child;
|
| 194 |
-
|
| 195 |
-
@override
|
| 196 |
-
State<_SwipeBackGate> createState() => _SwipeBackGateState();
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
class _SwipeBackGateState extends State<_SwipeBackGate> {
|
| 200 |
-
double _dragDistance = 0;
|
| 201 |
-
|
| 202 |
-
@override
|
| 203 |
-
Widget build(BuildContext context) {
|
| 204 |
-
return GestureDetector(
|
| 205 |
-
behavior: HitTestBehavior.translucent,
|
| 206 |
-
onHorizontalDragStart: (_) => _dragDistance = 0,
|
| 207 |
-
onHorizontalDragUpdate: (details) {
|
| 208 |
-
_dragDistance += details.delta.dx;
|
| 209 |
-
},
|
| 210 |
-
onHorizontalDragEnd: (details) {
|
| 211 |
-
final velocity = details.primaryVelocity ?? 0;
|
| 212 |
-
if ((velocity > 300 || _dragDistance > 70) && _dragDistance > 0) {
|
| 213 |
-
final nav = Navigator.of(context);
|
| 214 |
-
if (nav.canPop()) {
|
| 215 |
-
nav.pop();
|
| 216 |
-
}
|
| 217 |
-
}
|
| 218 |
-
_dragDistance = 0;
|
| 219 |
-
},
|
| 220 |
-
child: widget.child,
|
| 221 |
-
);
|
| 222 |
-
}
|
| 223 |
-
}
|
|
|
|
| 5 |
import 'package:flutter/material.dart';
|
| 6 |
|
| 7 |
import 'brand.dart';
|
| 8 |
+
import 'ios_back_gesture.dart';
|
| 9 |
|
| 10 |
class AppTheme {
|
| 11 |
static ThemeData light() => _build(Brightness.light);
|
|
|
|
| 166 |
static const splitPane = 1100.0;
|
| 167 |
}
|
| 168 |
|
| 169 |
+
// The iOS-style interactive back gesture + transition lives in
|
| 170 |
+
// ios_back_gesture.dart (IOSPageTransitionsBuilder), imported above.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/lib/ui/features/feed/views/knowledge_feed_screen.dart
CHANGED
|
@@ -8,7 +8,9 @@
|
|
| 8 |
/// not a filing cabinet.
|
| 9 |
library;
|
| 10 |
|
|
|
|
| 11 |
import 'package:flutter/material.dart';
|
|
|
|
| 12 |
import 'package:flutter_animate/flutter_animate.dart';
|
| 13 |
import 'package:phosphor_flutter/phosphor_flutter.dart';
|
| 14 |
import 'package:provider/provider.dart';
|
|
@@ -26,20 +28,25 @@ import '../../reader/views/reader_screen.dart';
|
|
| 26 |
import '../view_models/knowledge_feed_view_model.dart';
|
| 27 |
|
| 28 |
class KnowledgeFeedScreen extends StatelessWidget {
|
| 29 |
-
const KnowledgeFeedScreen({super.key});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
@override
|
| 32 |
Widget build(BuildContext context) {
|
| 33 |
return ChangeNotifierProvider(
|
| 34 |
create: (ctx) =>
|
| 35 |
KnowledgeFeedViewModel(repository: ctx.read<CardRepository>())..load(),
|
| 36 |
-
child:
|
| 37 |
);
|
| 38 |
}
|
| 39 |
}
|
| 40 |
|
| 41 |
class _FeedView extends StatefulWidget {
|
| 42 |
-
const _FeedView();
|
|
|
|
| 43 |
|
| 44 |
@override
|
| 45 |
State<_FeedView> createState() => _FeedViewState();
|
|
@@ -47,31 +54,80 @@ class _FeedView extends StatefulWidget {
|
|
| 47 |
|
| 48 |
class _FeedViewState extends State<_FeedView> {
|
| 49 |
final _controller = PageController();
|
|
|
|
| 50 |
int _page = 0;
|
| 51 |
|
| 52 |
@override
|
| 53 |
void dispose() {
|
| 54 |
_controller.dispose();
|
|
|
|
| 55 |
super.dispose();
|
| 56 |
}
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
@override
|
| 59 |
Widget build(BuildContext context) {
|
| 60 |
final vm = context.watch<KnowledgeFeedViewModel>();
|
| 61 |
final scheme = Theme.of(context).colorScheme;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
return Scaffold(
|
| 64 |
backgroundColor: scheme.surface,
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
),
|
| 74 |
-
|
| 75 |
),
|
| 76 |
);
|
| 77 |
}
|
|
@@ -101,14 +157,106 @@ class _FeedViewState extends State<_FeedView> {
|
|
| 101 |
);
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
),
|
| 113 |
);
|
| 114 |
}
|
|
@@ -120,11 +268,13 @@ class _TopBar extends StatelessWidget {
|
|
| 120 |
const _TopBar({
|
| 121 |
required this.index,
|
| 122 |
required this.total,
|
|
|
|
| 123 |
required this.onClose,
|
| 124 |
required this.onRefresh,
|
| 125 |
});
|
| 126 |
final int index;
|
| 127 |
final int total;
|
|
|
|
| 128 |
final VoidCallback onClose;
|
| 129 |
final VoidCallback? onRefresh;
|
| 130 |
|
|
@@ -136,11 +286,14 @@ class _TopBar extends StatelessWidget {
|
|
| 136 |
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
| 137 |
child: Row(
|
| 138 |
children: [
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
| 144 |
const Spacer(),
|
| 145 |
if (total > 0)
|
| 146 |
Text('${index + 1} / $total',
|
|
@@ -162,10 +315,13 @@ class _TopBar extends StatelessWidget {
|
|
| 162 |
// ββ One moment (a single full-screen page) ββββββββββββββββββββββββββββββββ //
|
| 163 |
|
| 164 |
class _MomentPage extends StatelessWidget {
|
| 165 |
-
const _MomentPage({required this.item, this.showSwipeHint = false});
|
| 166 |
final FeedItem item;
|
| 167 |
final bool showSwipeHint;
|
| 168 |
|
|
|
|
|
|
|
|
|
|
| 169 |
@override
|
| 170 |
Widget build(BuildContext context) {
|
| 171 |
final accent = ContentAccent.of(item.card.contentType);
|
|
@@ -185,20 +341,36 @@ class _MomentPage extends StatelessWidget {
|
|
| 185 |
),
|
| 186 |
),
|
| 187 |
child: SafeArea(
|
| 188 |
-
child:
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
),
|
| 203 |
),
|
| 204 |
);
|
|
|
|
| 8 |
/// not a filing cabinet.
|
| 9 |
library;
|
| 10 |
|
| 11 |
+
import 'package:flutter/gestures.dart';
|
| 12 |
import 'package:flutter/material.dart';
|
| 13 |
+
import 'package:flutter/services.dart';
|
| 14 |
import 'package:flutter_animate/flutter_animate.dart';
|
| 15 |
import 'package:phosphor_flutter/phosphor_flutter.dart';
|
| 16 |
import 'package:provider/provider.dart';
|
|
|
|
| 28 |
import '../view_models/knowledge_feed_view_model.dart';
|
| 29 |
|
| 30 |
class KnowledgeFeedScreen extends StatelessWidget {
|
| 31 |
+
const KnowledgeFeedScreen({super.key, this.inShell = false});
|
| 32 |
+
|
| 33 |
+
/// True when hosted as a bottom-nav tab (vs pushed as its own route): hides the
|
| 34 |
+
/// close button and pads content clear of the floating nav bar.
|
| 35 |
+
final bool inShell;
|
| 36 |
|
| 37 |
@override
|
| 38 |
Widget build(BuildContext context) {
|
| 39 |
return ChangeNotifierProvider(
|
| 40 |
create: (ctx) =>
|
| 41 |
KnowledgeFeedViewModel(repository: ctx.read<CardRepository>())..load(),
|
| 42 |
+
child: _FeedView(inShell: inShell),
|
| 43 |
);
|
| 44 |
}
|
| 45 |
}
|
| 46 |
|
| 47 |
class _FeedView extends StatefulWidget {
|
| 48 |
+
const _FeedView({this.inShell = false});
|
| 49 |
+
final bool inShell;
|
| 50 |
|
| 51 |
@override
|
| 52 |
State<_FeedView> createState() => _FeedViewState();
|
|
|
|
| 54 |
|
| 55 |
class _FeedViewState extends State<_FeedView> {
|
| 56 |
final _controller = PageController();
|
| 57 |
+
final _focusNode = FocusNode(debugLabel: 'KnowledgeFeed');
|
| 58 |
int _page = 0;
|
| 59 |
|
| 60 |
@override
|
| 61 |
void dispose() {
|
| 62 |
_controller.dispose();
|
| 63 |
+
_focusNode.dispose();
|
| 64 |
super.dispose();
|
| 65 |
}
|
| 66 |
|
| 67 |
+
void _goTo(int page, int total) {
|
| 68 |
+
final target = page.clamp(0, total - 1);
|
| 69 |
+
if (target == _page || !_controller.hasClients) return;
|
| 70 |
+
_controller.animateToPage(
|
| 71 |
+
target,
|
| 72 |
+
duration: const Duration(milliseconds: 320),
|
| 73 |
+
curve: Curves.easeOutCubic,
|
| 74 |
+
);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
void _next(int total) => _goTo(_page + 1, total);
|
| 78 |
+
void _prev(int total) => _goTo(_page - 1, total);
|
| 79 |
+
|
| 80 |
@override
|
| 81 |
Widget build(BuildContext context) {
|
| 82 |
final vm = context.watch<KnowledgeFeedViewModel>();
|
| 83 |
final scheme = Theme.of(context).colorScheme;
|
| 84 |
+
final total = vm.items.length;
|
| 85 |
+
// Show explicit prev/next controls on pointer-first (wide / web) layouts.
|
| 86 |
+
final showControls = total > 1 &&
|
| 87 |
+
MediaQuery.sizeOf(context).width >= Insets.desktop;
|
| 88 |
|
| 89 |
return Scaffold(
|
| 90 |
backgroundColor: scheme.surface,
|
| 91 |
+
// Keyboard navigation (arrows / page keys) for web + desktop. Focus is
|
| 92 |
+
// grabbed on hover so keys work without an explicit click, and autofocus
|
| 93 |
+
// only when pushed as its own route (not as a background shell tab).
|
| 94 |
+
body: CallbackShortcuts(
|
| 95 |
+
bindings: {
|
| 96 |
+
const SingleActivator(LogicalKeyboardKey.arrowDown): () => _next(total),
|
| 97 |
+
const SingleActivator(LogicalKeyboardKey.arrowUp): () => _prev(total),
|
| 98 |
+
const SingleActivator(LogicalKeyboardKey.pageDown): () => _next(total),
|
| 99 |
+
const SingleActivator(LogicalKeyboardKey.pageUp): () => _prev(total),
|
| 100 |
+
const SingleActivator(LogicalKeyboardKey.arrowRight): () => _next(total),
|
| 101 |
+
const SingleActivator(LogicalKeyboardKey.arrowLeft): () => _prev(total),
|
| 102 |
+
},
|
| 103 |
+
child: Focus(
|
| 104 |
+
focusNode: _focusNode,
|
| 105 |
+
autofocus: !widget.inShell,
|
| 106 |
+
child: MouseRegion(
|
| 107 |
+
onEnter: (_) {
|
| 108 |
+
if (!_focusNode.hasFocus) _focusNode.requestFocus();
|
| 109 |
+
},
|
| 110 |
+
child: Stack(
|
| 111 |
+
children: [
|
| 112 |
+
_content(context, vm),
|
| 113 |
+
_TopBar(
|
| 114 |
+
index: _page,
|
| 115 |
+
total: total,
|
| 116 |
+
showClose: !widget.inShell,
|
| 117 |
+
onClose: () => Navigator.of(context).maybePop(),
|
| 118 |
+
onRefresh: vm.loading ? null : vm.refresh,
|
| 119 |
+
),
|
| 120 |
+
if (showControls)
|
| 121 |
+
_NavButtons(
|
| 122 |
+
canPrev: _page > 0,
|
| 123 |
+
canNext: _page < total - 1,
|
| 124 |
+
onPrev: () => _prev(total),
|
| 125 |
+
onNext: () => _next(total),
|
| 126 |
+
),
|
| 127 |
+
],
|
| 128 |
+
),
|
| 129 |
),
|
| 130 |
+
),
|
| 131 |
),
|
| 132 |
);
|
| 133 |
}
|
|
|
|
| 157 |
);
|
| 158 |
}
|
| 159 |
|
| 160 |
+
// Enable click-drag + trackpad paging in the browser (Flutter disables mouse
|
| 161 |
+
// drag on scrollables by default), on top of the native touch swipe + wheel.
|
| 162 |
+
return ScrollConfiguration(
|
| 163 |
+
behavior: const _FeedScrollBehavior(),
|
| 164 |
+
child: PageView.builder(
|
| 165 |
+
controller: _controller,
|
| 166 |
+
scrollDirection: Axis.vertical,
|
| 167 |
+
itemCount: vm.items.length,
|
| 168 |
+
onPageChanged: (i) => setState(() => _page = i),
|
| 169 |
+
itemBuilder: (ctx, i) => _MomentPage(
|
| 170 |
+
item: vm.items[i],
|
| 171 |
+
showSwipeHint: i == 0 && vm.items.length > 1,
|
| 172 |
+
bottomInset: widget.inShell ? 84 : 0,
|
| 173 |
+
),
|
| 174 |
+
),
|
| 175 |
+
);
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/// Lets the vertical feed be dragged with a mouse / trackpad in the browser β
|
| 180 |
+
/// Flutter excludes those pointer kinds from drag-scrolling by default. Touch
|
| 181 |
+
/// swipe and scroll-wheel/trackpad paging keep working alongside this.
|
| 182 |
+
class _FeedScrollBehavior extends MaterialScrollBehavior {
|
| 183 |
+
const _FeedScrollBehavior();
|
| 184 |
+
|
| 185 |
+
@override
|
| 186 |
+
Set<PointerDeviceKind> get dragDevices => PointerDeviceKind.values.toSet();
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/// On-screen previous/next controls, shown on pointer-first (web / desktop)
|
| 190 |
+
/// layouts so mouse users have an obvious affordance beyond swiping.
|
| 191 |
+
class _NavButtons extends StatelessWidget {
|
| 192 |
+
const _NavButtons({
|
| 193 |
+
required this.canPrev,
|
| 194 |
+
required this.canNext,
|
| 195 |
+
required this.onPrev,
|
| 196 |
+
required this.onNext,
|
| 197 |
+
});
|
| 198 |
+
final bool canPrev;
|
| 199 |
+
final bool canNext;
|
| 200 |
+
final VoidCallback onPrev;
|
| 201 |
+
final VoidCallback onNext;
|
| 202 |
+
|
| 203 |
+
@override
|
| 204 |
+
Widget build(BuildContext context) {
|
| 205 |
+
return Positioned(
|
| 206 |
+
right: 16,
|
| 207 |
+
top: 0,
|
| 208 |
+
bottom: 0,
|
| 209 |
+
child: Center(
|
| 210 |
+
child: Column(
|
| 211 |
+
mainAxisSize: MainAxisSize.min,
|
| 212 |
+
children: [
|
| 213 |
+
_NavButton(
|
| 214 |
+
icon: PhosphorIconsRegular.caretUp,
|
| 215 |
+
tooltip: 'Previous',
|
| 216 |
+
onTap: canPrev ? onPrev : null,
|
| 217 |
+
),
|
| 218 |
+
const SizedBox(height: 12),
|
| 219 |
+
_NavButton(
|
| 220 |
+
icon: PhosphorIconsRegular.caretDown,
|
| 221 |
+
tooltip: 'Next',
|
| 222 |
+
onTap: canNext ? onNext : null,
|
| 223 |
+
),
|
| 224 |
+
],
|
| 225 |
+
),
|
| 226 |
+
),
|
| 227 |
+
);
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
class _NavButton extends StatelessWidget {
|
| 232 |
+
const _NavButton({required this.icon, required this.onTap, required this.tooltip});
|
| 233 |
+
final IconData icon;
|
| 234 |
+
final VoidCallback? onTap;
|
| 235 |
+
final String tooltip;
|
| 236 |
+
|
| 237 |
+
@override
|
| 238 |
+
Widget build(BuildContext context) {
|
| 239 |
+
final scheme = Theme.of(context).colorScheme;
|
| 240 |
+
final enabled = onTap != null;
|
| 241 |
+
return Tooltip(
|
| 242 |
+
message: tooltip,
|
| 243 |
+
child: Material(
|
| 244 |
+
color: scheme.surfaceContainerHighest.withValues(alpha: enabled ? 0.9 : 0.35),
|
| 245 |
+
shape: const CircleBorder(),
|
| 246 |
+
clipBehavior: Clip.antiAlias,
|
| 247 |
+
child: InkWell(
|
| 248 |
+
onTap: onTap,
|
| 249 |
+
child: Padding(
|
| 250 |
+
padding: const EdgeInsets.all(10),
|
| 251 |
+
child: PhosphorIcon(
|
| 252 |
+
icon,
|
| 253 |
+
size: 20,
|
| 254 |
+
color: enabled
|
| 255 |
+
? scheme.onSurface
|
| 256 |
+
: scheme.onSurfaceVariant.withValues(alpha: 0.4),
|
| 257 |
+
),
|
| 258 |
+
),
|
| 259 |
+
),
|
| 260 |
),
|
| 261 |
);
|
| 262 |
}
|
|
|
|
| 268 |
const _TopBar({
|
| 269 |
required this.index,
|
| 270 |
required this.total,
|
| 271 |
+
required this.showClose,
|
| 272 |
required this.onClose,
|
| 273 |
required this.onRefresh,
|
| 274 |
});
|
| 275 |
final int index;
|
| 276 |
final int total;
|
| 277 |
+
final bool showClose;
|
| 278 |
final VoidCallback onClose;
|
| 279 |
final VoidCallback? onRefresh;
|
| 280 |
|
|
|
|
| 286 |
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
| 287 |
child: Row(
|
| 288 |
children: [
|
| 289 |
+
if (showClose)
|
| 290 |
+
IconButton(
|
| 291 |
+
onPressed: onClose,
|
| 292 |
+
icon: const PhosphorIcon(PhosphorIconsRegular.x),
|
| 293 |
+
tooltip: 'Close',
|
| 294 |
+
)
|
| 295 |
+
else
|
| 296 |
+
const SizedBox(width: 48),
|
| 297 |
const Spacer(),
|
| 298 |
if (total > 0)
|
| 299 |
Text('${index + 1} / $total',
|
|
|
|
| 315 |
// ββ One moment (a single full-screen page) ββββββββββββββββββββββββββββββββ //
|
| 316 |
|
| 317 |
class _MomentPage extends StatelessWidget {
|
| 318 |
+
const _MomentPage({required this.item, this.showSwipeHint = false, this.bottomInset = 0});
|
| 319 |
final FeedItem item;
|
| 320 |
final bool showSwipeHint;
|
| 321 |
|
| 322 |
+
/// Extra bottom padding to clear the floating nav bar when hosted as a tab.
|
| 323 |
+
final double bottomInset;
|
| 324 |
+
|
| 325 |
@override
|
| 326 |
Widget build(BuildContext context) {
|
| 327 |
final accent = ContentAccent.of(item.card.contentType);
|
|
|
|
| 341 |
),
|
| 342 |
),
|
| 343 |
child: SafeArea(
|
| 344 |
+
child: LayoutBuilder(
|
| 345 |
+
builder: (context, constraints) {
|
| 346 |
+
// Constrain the content to a centered reading column so it doesn't
|
| 347 |
+
// stretch edge-to-edge on a wide browser window (the gradient stays
|
| 348 |
+
// full-bleed behind it). Full width on narrow / mobile layouts.
|
| 349 |
+
final width = constraints.maxWidth > Insets.readingColumn
|
| 350 |
+
? Insets.readingColumn
|
| 351 |
+
: constraints.maxWidth;
|
| 352 |
+
return Center(
|
| 353 |
+
child: SizedBox(
|
| 354 |
+
width: width,
|
| 355 |
+
height: constraints.maxHeight,
|
| 356 |
+
child: Padding(
|
| 357 |
+
padding: EdgeInsets.fromLTRB(28, 64, 28, 28 + bottomInset),
|
| 358 |
+
child: Column(
|
| 359 |
+
crossAxisAlignment: CrossAxisAlignment.start,
|
| 360 |
+
children: [
|
| 361 |
+
_Eyebrow(item: item, accent: accent),
|
| 362 |
+
Expanded(child: _MomentBody(item: item, accent: accent)),
|
| 363 |
+
_SourceFooter(item: item, accent: accent),
|
| 364 |
+
if (showSwipeHint) ...[
|
| 365 |
+
const SizedBox(height: 10),
|
| 366 |
+
const Center(child: _SwipeHint()),
|
| 367 |
+
],
|
| 368 |
+
],
|
| 369 |
+
),
|
| 370 |
+
),
|
| 371 |
+
),
|
| 372 |
+
);
|
| 373 |
+
},
|
| 374 |
),
|
| 375 |
),
|
| 376 |
);
|
app/lib/ui/features/library/views/library_screen.dart
CHANGED
|
@@ -31,7 +31,7 @@ import '../../../core/widgets/spot_art.dart';
|
|
| 31 |
import '../../capture/views/capture_sheet.dart';
|
| 32 |
import '../../catalog/views/catalog_screen.dart';
|
| 33 |
import '../../concepts/views/concepts_screen.dart';
|
| 34 |
-
import '../../
|
| 35 |
import '../../library/views/library_chat_screen.dart';
|
| 36 |
import '../../reader/views/reader_screen.dart';
|
| 37 |
import '../../search/views/search_screen.dart';
|
|
@@ -76,10 +76,10 @@ class _LibraryView extends StatelessWidget {
|
|
| 76 |
),
|
| 77 |
),
|
| 78 |
IconButton(
|
| 79 |
-
tooltip: '
|
| 80 |
-
icon: const PhosphorIcon(PhosphorIconsRegular.
|
| 81 |
onPressed: () => Navigator.of(context).push(
|
| 82 |
-
MaterialPageRoute(builder: (_) => const
|
| 83 |
),
|
| 84 |
),
|
| 85 |
IconButton(
|
|
|
|
| 31 |
import '../../capture/views/capture_sheet.dart';
|
| 32 |
import '../../catalog/views/catalog_screen.dart';
|
| 33 |
import '../../concepts/views/concepts_screen.dart';
|
| 34 |
+
import '../../graph/views/graph_screen.dart';
|
| 35 |
import '../../library/views/library_chat_screen.dart';
|
| 36 |
import '../../reader/views/reader_screen.dart';
|
| 37 |
import '../../search/views/search_screen.dart';
|
|
|
|
| 76 |
),
|
| 77 |
),
|
| 78 |
IconButton(
|
| 79 |
+
tooltip: 'Graph',
|
| 80 |
+
icon: const PhosphorIcon(PhosphorIconsRegular.graph),
|
| 81 |
onPressed: () => Navigator.of(context).push(
|
| 82 |
+
MaterialPageRoute(builder: (_) => const GraphScreen()),
|
| 83 |
),
|
| 84 |
),
|
| 85 |
IconButton(
|