mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Draggable scroll bar is implemented from commmunity library
This commit is contained in:
		
							
								
								
									
										617
									
								
								mobile/lib/modules/home/ui/draggable_scrollbar.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										617
									
								
								mobile/lib/modules/home/ui/draggable_scrollbar.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,617 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| /// Build the Scroll Thumb and label using the current configuration | ||||
| typedef ScrollThumbBuilder = Widget Function( | ||||
|   Color backgroundColor, | ||||
|   Animation<double> thumbAnimation, | ||||
|   Animation<double> labelAnimation, | ||||
|   double height, { | ||||
|   Text? labelText, | ||||
|   BoxConstraints? labelConstraints, | ||||
| }); | ||||
|  | ||||
| /// Build a Text widget using the current scroll offset | ||||
| typedef LabelTextBuilder = Text Function(double offsetY); | ||||
|  | ||||
| /// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged | ||||
| /// for quick navigation of the BoxScrollView. | ||||
| class DraggableScrollbar extends StatefulWidget { | ||||
|   /// The view that will be scrolled with the scroll thumb | ||||
|   final CustomScrollView child; | ||||
|  | ||||
|   /// A function that builds a thumb using the current configuration | ||||
|   final ScrollThumbBuilder scrollThumbBuilder; | ||||
|  | ||||
|   /// The height of the scroll thumb | ||||
|   final double heightScrollThumb; | ||||
|  | ||||
|   /// The background color of the label and thumb | ||||
|   final Color backgroundColor; | ||||
|  | ||||
|   /// The amount of padding that should surround the thumb | ||||
|   final EdgeInsetsGeometry? padding; | ||||
|  | ||||
|   /// Determines how quickly the scrollbar will animate in and out | ||||
|   final Duration scrollbarAnimationDuration; | ||||
|  | ||||
|   /// How long should the thumb be visible before fading out | ||||
|   final Duration scrollbarTimeToFade; | ||||
|  | ||||
|   /// Build a Text widget from the current offset in the BoxScrollView | ||||
|   final LabelTextBuilder? labelTextBuilder; | ||||
|  | ||||
|   /// Determines box constraints for Container displaying label | ||||
|   final BoxConstraints? labelConstraints; | ||||
|  | ||||
|   /// The ScrollController for the BoxScrollView | ||||
|   final ScrollController controller; | ||||
|  | ||||
|   /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder] | ||||
|   final bool alwaysVisibleScrollThumb; | ||||
|  | ||||
|   DraggableScrollbar({ | ||||
|     Key? key, | ||||
|     this.alwaysVisibleScrollThumb = false, | ||||
|     required this.heightScrollThumb, | ||||
|     required this.backgroundColor, | ||||
|     required this.scrollThumbBuilder, | ||||
|     required this.child, | ||||
|     required this.controller, | ||||
|     this.padding, | ||||
|     this.scrollbarAnimationDuration = const Duration(milliseconds: 300), | ||||
|     this.scrollbarTimeToFade = const Duration(milliseconds: 600), | ||||
|     this.labelTextBuilder, | ||||
|     this.labelConstraints, | ||||
|   })  : assert(child.scrollDirection == Axis.vertical), | ||||
|         super(key: key); | ||||
|  | ||||
|   DraggableScrollbar.rrect({ | ||||
|     Key? key, | ||||
|     Key? scrollThumbKey, | ||||
|     this.alwaysVisibleScrollThumb = false, | ||||
|     required this.child, | ||||
|     required this.controller, | ||||
|     this.heightScrollThumb = 48.0, | ||||
|     this.backgroundColor = Colors.white, | ||||
|     this.padding, | ||||
|     this.scrollbarAnimationDuration = const Duration(milliseconds: 300), | ||||
|     this.scrollbarTimeToFade = const Duration(milliseconds: 600), | ||||
|     this.labelTextBuilder, | ||||
|     this.labelConstraints, | ||||
|   })  : assert(child.scrollDirection == Axis.vertical), | ||||
|         scrollThumbBuilder = _thumbRRectBuilder(scrollThumbKey, alwaysVisibleScrollThumb), | ||||
|         super(key: key); | ||||
|  | ||||
|   DraggableScrollbar.arrows({ | ||||
|     Key? key, | ||||
|     Key? scrollThumbKey, | ||||
|     this.alwaysVisibleScrollThumb = false, | ||||
|     required this.child, | ||||
|     required this.controller, | ||||
|     this.heightScrollThumb = 48.0, | ||||
|     this.backgroundColor = Colors.white, | ||||
|     this.padding, | ||||
|     this.scrollbarAnimationDuration = const Duration(milliseconds: 300), | ||||
|     this.scrollbarTimeToFade = const Duration(milliseconds: 600), | ||||
|     this.labelTextBuilder, | ||||
|     this.labelConstraints, | ||||
|   })  : assert(child.scrollDirection == Axis.vertical), | ||||
|         scrollThumbBuilder = _thumbArrowBuilder(scrollThumbKey, alwaysVisibleScrollThumb), | ||||
|         super(key: key); | ||||
|  | ||||
|   DraggableScrollbar.semicircle({ | ||||
|     Key? key, | ||||
|     Key? scrollThumbKey, | ||||
|     this.alwaysVisibleScrollThumb = false, | ||||
|     required this.child, | ||||
|     required this.controller, | ||||
|     this.heightScrollThumb = 48.0, | ||||
|     this.backgroundColor = Colors.white, | ||||
|     this.padding, | ||||
|     this.scrollbarAnimationDuration = const Duration(milliseconds: 300), | ||||
|     this.scrollbarTimeToFade = const Duration(milliseconds: 600), | ||||
|     this.labelTextBuilder, | ||||
|     this.labelConstraints, | ||||
|   })  : assert(child.scrollDirection == Axis.vertical), | ||||
|         scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb), | ||||
|         super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _DraggableScrollbarState createState() => _DraggableScrollbarState(); | ||||
|  | ||||
|   static buildScrollThumbAndLabel( | ||||
|       {required Widget scrollThumb, | ||||
|       required Color backgroundColor, | ||||
|       required Animation<double>? thumbAnimation, | ||||
|       required Animation<double>? labelAnimation, | ||||
|       required Text? labelText, | ||||
|       required BoxConstraints? labelConstraints, | ||||
|       required bool alwaysVisibleScrollThumb}) { | ||||
|     var scrollThumbAndLabel = labelText == null | ||||
|         ? scrollThumb | ||||
|         : Row( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             mainAxisAlignment: MainAxisAlignment.end, | ||||
|             children: [ | ||||
|               ScrollLabel( | ||||
|                 animation: labelAnimation, | ||||
|                 child: labelText, | ||||
|                 backgroundColor: backgroundColor, | ||||
|                 constraints: labelConstraints, | ||||
|               ), | ||||
|               scrollThumb, | ||||
|             ], | ||||
|           ); | ||||
|  | ||||
|     if (alwaysVisibleScrollThumb) { | ||||
|       return scrollThumbAndLabel; | ||||
|     } | ||||
|     return SlideFadeTransition( | ||||
|       animation: thumbAnimation!, | ||||
|       child: scrollThumbAndLabel, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { | ||||
|     return ( | ||||
|       Color backgroundColor, | ||||
|       Animation<double> thumbAnimation, | ||||
|       Animation<double> labelAnimation, | ||||
|       double height, { | ||||
|       Text? labelText, | ||||
|       BoxConstraints? labelConstraints, | ||||
|     }) { | ||||
|       final scrollThumb = CustomPaint( | ||||
|         key: scrollThumbKey, | ||||
|         foregroundPainter: ArrowCustomPainter(Colors.grey), | ||||
|         child: Material( | ||||
|           elevation: 4.0, | ||||
|           child: Container( | ||||
|             constraints: BoxConstraints.tight(Size(width, height)), | ||||
|           ), | ||||
|           color: backgroundColor, | ||||
|           borderRadius: BorderRadius.only( | ||||
|             topLeft: Radius.circular(height), | ||||
|             bottomLeft: Radius.circular(height), | ||||
|             topRight: const Radius.circular(4.0), | ||||
|             bottomRight: const Radius.circular(4.0), | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       return buildScrollThumbAndLabel( | ||||
|         scrollThumb: scrollThumb, | ||||
|         backgroundColor: backgroundColor, | ||||
|         thumbAnimation: thumbAnimation, | ||||
|         labelAnimation: labelAnimation, | ||||
|         labelText: labelText, | ||||
|         labelConstraints: labelConstraints, | ||||
|         alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, | ||||
|       ); | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   static ScrollThumbBuilder _thumbArrowBuilder(Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { | ||||
|     return ( | ||||
|       Color backgroundColor, | ||||
|       Animation<double> thumbAnimation, | ||||
|       Animation<double> labelAnimation, | ||||
|       double height, { | ||||
|       Text? labelText, | ||||
|       BoxConstraints? labelConstraints, | ||||
|     }) { | ||||
|       final scrollThumb = ClipPath( | ||||
|         child: Container( | ||||
|           height: height, | ||||
|           width: 20.0, | ||||
|           decoration: BoxDecoration( | ||||
|             color: backgroundColor, | ||||
|             borderRadius: const BorderRadius.all( | ||||
|               Radius.circular(12.0), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|         clipper: ArrowClipper(), | ||||
|       ); | ||||
|  | ||||
|       return buildScrollThumbAndLabel( | ||||
|         scrollThumb: scrollThumb, | ||||
|         backgroundColor: backgroundColor, | ||||
|         thumbAnimation: thumbAnimation, | ||||
|         labelAnimation: labelAnimation, | ||||
|         labelText: labelText, | ||||
|         labelConstraints: labelConstraints, | ||||
|         alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, | ||||
|       ); | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   static ScrollThumbBuilder _thumbRRectBuilder(Key? scrollThumbKey, bool alwaysVisibleScrollThumb) { | ||||
|     return ( | ||||
|       Color backgroundColor, | ||||
|       Animation<double> thumbAnimation, | ||||
|       Animation<double> labelAnimation, | ||||
|       double height, { | ||||
|       Text? labelText, | ||||
|       BoxConstraints? labelConstraints, | ||||
|     }) { | ||||
|       final scrollThumb = Material( | ||||
|         elevation: 4.0, | ||||
|         child: Container( | ||||
|           constraints: BoxConstraints.tight( | ||||
|             Size(16.0, height), | ||||
|           ), | ||||
|         ), | ||||
|         color: backgroundColor, | ||||
|         borderRadius: const BorderRadius.all(Radius.circular(7.0)), | ||||
|       ); | ||||
|  | ||||
|       return buildScrollThumbAndLabel( | ||||
|         scrollThumb: scrollThumb, | ||||
|         backgroundColor: backgroundColor, | ||||
|         thumbAnimation: thumbAnimation, | ||||
|         labelAnimation: labelAnimation, | ||||
|         labelText: labelText, | ||||
|         labelConstraints: labelConstraints, | ||||
|         alwaysVisibleScrollThumb: alwaysVisibleScrollThumb, | ||||
|       ); | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ScrollLabel extends StatelessWidget { | ||||
|   final Animation<double>? animation; | ||||
|   final Color backgroundColor; | ||||
|   final Text child; | ||||
|  | ||||
|   final BoxConstraints? constraints; | ||||
|   static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0); | ||||
|  | ||||
|   const ScrollLabel({ | ||||
|     Key? key, | ||||
|     required this.child, | ||||
|     required this.animation, | ||||
|     required this.backgroundColor, | ||||
|     this.constraints = _defaultConstraints, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return FadeTransition( | ||||
|       opacity: animation!, | ||||
|       child: Container( | ||||
|         margin: const EdgeInsets.only(right: 12.0), | ||||
|         child: Material( | ||||
|           elevation: 4.0, | ||||
|           color: backgroundColor, | ||||
|           borderRadius: const BorderRadius.all(Radius.circular(16.0)), | ||||
|           child: Container( | ||||
|             constraints: constraints ?? _defaultConstraints, | ||||
|             alignment: Alignment.center, | ||||
|             child: child, | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin { | ||||
|   late double _barOffset; | ||||
|   late double _viewOffset; | ||||
|   late bool _isDragInProcess; | ||||
|  | ||||
|   late AnimationController _thumbAnimationController; | ||||
|   late Animation<double> _thumbAnimation; | ||||
|   late AnimationController _labelAnimationController; | ||||
|   late Animation<double> _labelAnimation; | ||||
|   Timer? _fadeoutTimer; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _barOffset = 0.0; | ||||
|     _viewOffset = 0.0; | ||||
|     _isDragInProcess = false; | ||||
|  | ||||
|     _thumbAnimationController = AnimationController( | ||||
|       vsync: this, | ||||
|       duration: widget.scrollbarAnimationDuration, | ||||
|     ); | ||||
|  | ||||
|     _thumbAnimation = CurvedAnimation( | ||||
|       parent: _thumbAnimationController, | ||||
|       curve: Curves.fastOutSlowIn, | ||||
|     ); | ||||
|  | ||||
|     _labelAnimationController = AnimationController( | ||||
|       vsync: this, | ||||
|       duration: widget.scrollbarAnimationDuration, | ||||
|     ); | ||||
|  | ||||
|     _labelAnimation = CurvedAnimation( | ||||
|       parent: _labelAnimationController, | ||||
|       curve: Curves.fastOutSlowIn, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _thumbAnimationController.dispose(); | ||||
|     _labelAnimationController.dispose(); | ||||
|     _fadeoutTimer?.cancel(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb; | ||||
|  | ||||
|   double get barMinScrollExtent => 0.0; | ||||
|  | ||||
|   double get viewMaxScrollExtent => widget.controller.position.maxScrollExtent; | ||||
|  | ||||
|   double get viewMinScrollExtent => widget.controller.position.minScrollExtent; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     Text? labelText; | ||||
|     if (widget.labelTextBuilder != null && _isDragInProcess) { | ||||
|       labelText = widget.labelTextBuilder!( | ||||
|         _viewOffset + _barOffset + widget.heightScrollThumb / 2, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { | ||||
|       //print("LayoutBuilder constraints=$constraints"); | ||||
|  | ||||
|       return NotificationListener<ScrollNotification>( | ||||
|         onNotification: (ScrollNotification notification) { | ||||
|           changePosition(notification); | ||||
|           return false; | ||||
|         }, | ||||
|         child: Stack( | ||||
|           children: <Widget>[ | ||||
|             RepaintBoundary( | ||||
|               child: widget.child, | ||||
|             ), | ||||
|             RepaintBoundary( | ||||
|                 child: GestureDetector( | ||||
|               onVerticalDragStart: _onVerticalDragStart, | ||||
|               onVerticalDragUpdate: _onVerticalDragUpdate, | ||||
|               onVerticalDragEnd: _onVerticalDragEnd, | ||||
|               child: Container( | ||||
|                 alignment: Alignment.topRight, | ||||
|                 margin: EdgeInsets.only(top: _barOffset), | ||||
|                 padding: widget.padding, | ||||
|                 child: widget.scrollThumbBuilder( | ||||
|                   widget.backgroundColor, | ||||
|                   _thumbAnimation, | ||||
|                   _labelAnimation, | ||||
|                   widget.heightScrollThumb, | ||||
|                   labelText: labelText, | ||||
|                   labelConstraints: widget.labelConstraints, | ||||
|                 ), | ||||
|               ), | ||||
|             )), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   //scroll bar has received notification that it's view was scrolled | ||||
|   //so it should also changes his position | ||||
|   //but only if it isn't dragged | ||||
|   changePosition(ScrollNotification notification) { | ||||
|     if (_isDragInProcess) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     setState(() { | ||||
|       if (notification is ScrollUpdateNotification) { | ||||
|         _barOffset += getBarDelta( | ||||
|           notification.scrollDelta!, | ||||
|           barMaxScrollExtent, | ||||
|           viewMaxScrollExtent, | ||||
|         ); | ||||
|  | ||||
|         if (_barOffset < barMinScrollExtent) { | ||||
|           _barOffset = barMinScrollExtent; | ||||
|         } | ||||
|         if (_barOffset > barMaxScrollExtent) { | ||||
|           _barOffset = barMaxScrollExtent; | ||||
|         } | ||||
|  | ||||
|         _viewOffset += notification.scrollDelta!; | ||||
|         if (_viewOffset < widget.controller.position.minScrollExtent) { | ||||
|           _viewOffset = widget.controller.position.minScrollExtent; | ||||
|         } | ||||
|         if (_viewOffset > viewMaxScrollExtent) { | ||||
|           _viewOffset = viewMaxScrollExtent; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (notification is ScrollUpdateNotification || notification is OverscrollNotification) { | ||||
|         if (_thumbAnimationController.status != AnimationStatus.forward) { | ||||
|           _thumbAnimationController.forward(); | ||||
|         } | ||||
|  | ||||
|         _fadeoutTimer?.cancel(); | ||||
|         _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { | ||||
|           _thumbAnimationController.reverse(); | ||||
|           _labelAnimationController.reverse(); | ||||
|           _fadeoutTimer = null; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   double getBarDelta( | ||||
|     double scrollViewDelta, | ||||
|     double barMaxScrollExtent, | ||||
|     double viewMaxScrollExtent, | ||||
|   ) { | ||||
|     return scrollViewDelta * barMaxScrollExtent / viewMaxScrollExtent; | ||||
|   } | ||||
|  | ||||
|   double getScrollViewDelta( | ||||
|     double barDelta, | ||||
|     double barMaxScrollExtent, | ||||
|     double viewMaxScrollExtent, | ||||
|   ) { | ||||
|     return barDelta * viewMaxScrollExtent / barMaxScrollExtent; | ||||
|   } | ||||
|  | ||||
|   void _onVerticalDragStart(DragStartDetails details) { | ||||
|     setState(() { | ||||
|       _isDragInProcess = true; | ||||
|       _labelAnimationController.forward(); | ||||
|       _fadeoutTimer?.cancel(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _onVerticalDragUpdate(DragUpdateDetails details) { | ||||
|     setState(() { | ||||
|       if (_thumbAnimationController.status != AnimationStatus.forward) { | ||||
|         _thumbAnimationController.forward(); | ||||
|       } | ||||
|       if (_isDragInProcess) { | ||||
|         _barOffset += details.delta.dy; | ||||
|  | ||||
|         if (_barOffset < barMinScrollExtent) { | ||||
|           _barOffset = barMinScrollExtent; | ||||
|         } | ||||
|         if (_barOffset > barMaxScrollExtent) { | ||||
|           _barOffset = barMaxScrollExtent; | ||||
|         } | ||||
|  | ||||
|         double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent); | ||||
|  | ||||
|         _viewOffset = widget.controller.position.pixels + viewDelta; | ||||
|         if (_viewOffset < widget.controller.position.minScrollExtent) { | ||||
|           _viewOffset = widget.controller.position.minScrollExtent; | ||||
|         } | ||||
|         if (_viewOffset > viewMaxScrollExtent) { | ||||
|           _viewOffset = viewMaxScrollExtent; | ||||
|         } | ||||
|         widget.controller.jumpTo(_viewOffset); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _onVerticalDragEnd(DragEndDetails details) { | ||||
|     _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { | ||||
|       _thumbAnimationController.reverse(); | ||||
|       _labelAnimationController.reverse(); | ||||
|       _fadeoutTimer = null; | ||||
|     }); | ||||
|     setState(() { | ||||
|       _isDragInProcess = false; | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Draws 2 triangles like arrow up and arrow down | ||||
| class ArrowCustomPainter extends CustomPainter { | ||||
|   Color color; | ||||
|  | ||||
|   ArrowCustomPainter(this.color); | ||||
|  | ||||
|   @override | ||||
|   bool shouldRepaint(covariant CustomPainter oldDelegate) => false; | ||||
|  | ||||
|   @override | ||||
|   void paint(Canvas canvas, Size size) { | ||||
|     final paint = Paint()..color = color; | ||||
|     const width = 12.0; | ||||
|     const height = 8.0; | ||||
|     final baseX = size.width / 2; | ||||
|     final baseY = size.height / 2; | ||||
|  | ||||
|     canvas.drawPath( | ||||
|       _trianglePath(Offset(baseX, baseY - 2.0), width, height, true), | ||||
|       paint, | ||||
|     ); | ||||
|     canvas.drawPath( | ||||
|       _trianglePath(Offset(baseX, baseY + 2.0), width, height, false), | ||||
|       paint, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static Path _trianglePath(Offset o, double width, double height, bool isUp) { | ||||
|     return Path() | ||||
|       ..moveTo(o.dx, o.dy) | ||||
|       ..lineTo(o.dx + width, o.dy) | ||||
|       ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height) | ||||
|       ..close(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| ///This cut 2 lines in arrow shape | ||||
| class ArrowClipper extends CustomClipper<Path> { | ||||
|   @override | ||||
|   Path getClip(Size size) { | ||||
|     Path path = Path(); | ||||
|     path.lineTo(0.0, size.height); | ||||
|     path.lineTo(size.width, size.height); | ||||
|     path.lineTo(size.width, 0.0); | ||||
|     path.lineTo(0.0, 0.0); | ||||
|     path.close(); | ||||
|  | ||||
|     double arrowWidth = 8.0; | ||||
|     double startPointX = (size.width - arrowWidth) / 2; | ||||
|     double startPointY = size.height / 2 - arrowWidth / 2; | ||||
|     path.moveTo(startPointX, startPointY); | ||||
|     path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2); | ||||
|     path.lineTo(startPointX + arrowWidth, startPointY); | ||||
|     path.lineTo(startPointX + arrowWidth, startPointY + 1.0); | ||||
|     path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0); | ||||
|     path.lineTo(startPointX, startPointY + 1.0); | ||||
|     path.close(); | ||||
|  | ||||
|     startPointY = size.height / 2 + arrowWidth / 2; | ||||
|     path.moveTo(startPointX + arrowWidth, startPointY); | ||||
|     path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2); | ||||
|     path.lineTo(startPointX, startPointY); | ||||
|     path.lineTo(startPointX, startPointY - 1.0); | ||||
|     path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0); | ||||
|     path.lineTo(startPointX + arrowWidth, startPointY - 1.0); | ||||
|     path.close(); | ||||
|  | ||||
|     return path; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool shouldReclip(CustomClipper<Path> oldClipper) => false; | ||||
| } | ||||
|  | ||||
| class SlideFadeTransition extends StatelessWidget { | ||||
|   final Animation<double> animation; | ||||
|   final Widget child; | ||||
|  | ||||
|   const SlideFadeTransition({ | ||||
|     Key? key, | ||||
|     required this.animation, | ||||
|     required this.child, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AnimatedBuilder( | ||||
|       animation: animation, | ||||
|       builder: (context, child) => animation.value == 0.0 ? Container() : child!, | ||||
|       child: SlideTransition( | ||||
|         position: Tween( | ||||
|           begin: const Offset(0.3, 0.0), | ||||
|           end: const Offset(0.0, 0.0), | ||||
|         ).animate(animation), | ||||
|         child: FadeTransition( | ||||
|           opacity: animation, | ||||
|           child: child, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,15 +1,12 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/rendering.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/profile_drawer.dart'; | ||||
| import 'package:immich_mobile/shared/models/backup_state.model.dart'; | ||||
| import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/image_grid.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/asset.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/backup.provider.dart'; | ||||
| import 'package:visibility_detector/visibility_detector.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
|  | ||||
| class HomePage extends HookConsumerWidget { | ||||
| @@ -21,8 +18,8 @@ class HomePage extends HookConsumerWidget { | ||||
|     ScrollController _scrollController = useScrollController(); | ||||
|     List<ImmichAssetGroupByDate> assetGroup = ref.watch(assetProvider); | ||||
|     List<Widget> imageGridGroup = []; | ||||
|     List<GlobalKey> monthGroupKey = []; | ||||
|     final monthInView = useState<String>(""); | ||||
|     final scrollLabelText = useState(""); | ||||
|  | ||||
|     _scrollControllerCallback() { | ||||
|       var endOfPage = _scrollController.position.maxScrollExtent; | ||||
|  | ||||
| @@ -35,13 +32,6 @@ class HomePage extends HookConsumerWidget { | ||||
|       } else { | ||||
|         _showBackToTopBtn.value = false; | ||||
|       } | ||||
|  | ||||
|       // Quick Scroll For Jumping to Month | ||||
|       if (_scrollController.position.userScrollDirection == ScrollDirection.forward) { | ||||
|         // Scroll UP | ||||
|       } else if (_scrollController.position.userScrollDirection == ScrollDirection.reverse) { | ||||
|         // SCroll Down | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
| @@ -55,53 +45,29 @@ class HomePage extends HookConsumerWidget { | ||||
|       }; | ||||
|     }, []); | ||||
|  | ||||
|     SliverToBoxAdapter _buildMonthGroupTitle(String dateTitle, BuildContext context) { | ||||
|       return SliverToBoxAdapter( | ||||
|         child: Padding( | ||||
|           padding: const EdgeInsets.only(left: 10.0, top: 32), | ||||
|           child: Text( | ||||
|             DateFormat('MMMM, y').format( | ||||
|               DateTime.parse(dateTitle), | ||||
|             ), | ||||
|             style: TextStyle( | ||||
|               fontSize: 24, | ||||
|               fontWeight: FontWeight.bold, | ||||
|               color: Theme.of(context).primaryColor, | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     SliverToBoxAdapter _buildDateGroupTitle(String dateTitle) { | ||||
|       var currentYear = DateTime.now().year; | ||||
|       var groupYear = DateTime.parse(dateTitle).year; | ||||
|       var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy'; | ||||
|       var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(dateTitle)); | ||||
|       var monthText = DateFormat('MMMM, y').format(DateTime.parse(dateTitle)); | ||||
|  | ||||
|       return SliverToBoxAdapter( | ||||
|         child: VisibilityDetector( | ||||
|           key: Key(dateText), | ||||
|           onVisibilityChanged: (visibilityInfo) { | ||||
|             monthInView.value = monthText; | ||||
|           }, | ||||
|           child: Padding( | ||||
|             padding: const EdgeInsets.only(top: 24.0, bottom: 24.0, left: 3.0), | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 Padding( | ||||
|                   padding: const EdgeInsets.only(left: 8.0, bottom: 5.0, top: 5.0), | ||||
|                   child: Text( | ||||
|                     dateText, | ||||
|                     style: const TextStyle( | ||||
|                       fontSize: 14, | ||||
|                       fontWeight: FontWeight.bold, | ||||
|                       color: Colors.black87, | ||||
|                     ), | ||||
|         child: Padding( | ||||
|           padding: const EdgeInsets.only(top: 24.0, bottom: 24.0, left: 3.0), | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               Padding( | ||||
|                 padding: const EdgeInsets.only(left: 8.0, bottom: 5.0, top: 5.0), | ||||
|                 child: Text( | ||||
|                   dateText, | ||||
|                   style: const TextStyle( | ||||
|                     fontSize: 14, | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                     color: Colors.black87, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
| @@ -121,58 +87,82 @@ class HomePage extends HookConsumerWidget { | ||||
|           if ((currentMonth! - previousMonth!) != 0) { | ||||
|             var monthTitleText = DateFormat('MMMM, y').format(DateTime.parse(dateTitle)); | ||||
|  | ||||
|             imageGridGroup.add(_buildMonthGroupTitle(monthTitleText, context)); | ||||
|             imageGridGroup.add( | ||||
|               MonthlyTitleText(monthTitleText: monthTitleText), | ||||
|             ); | ||||
|           } | ||||
|  | ||||
|           imageGridGroup.add( | ||||
|             _buildDateGroupTitle(dateTitle), | ||||
|           ); | ||||
|  | ||||
|           imageGridGroup.add(ImageGrid(assetGroup: assetGroup)); | ||||
|           imageGridGroup.add( | ||||
|             ImageGrid(assetGroup: assetGroup), | ||||
|           ); | ||||
|  | ||||
|           lastGroupDate = dateTitle; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       return SafeArea( | ||||
|         child: Stack(children: [ | ||||
|           RawScrollbar( | ||||
|             minThumbLength: 50, | ||||
|             isAlwaysShown: false, | ||||
|             interactive: true, | ||||
|         child: DraggableScrollbar.semicircle( | ||||
|           // labelTextBuilder: (offset) { | ||||
|           //   final int currentItem = _scrollController.hasClients | ||||
|           //       ? (_scrollController.offset / _scrollController.position.maxScrollExtent * imageGridGroup.length) | ||||
|           //           .floor() | ||||
|           //       : 0; | ||||
|  | ||||
|           //   if (imageGridGroup[currentItem] is MonthlyTitleText) { | ||||
|           //     MonthlyTitleText item = imageGridGroup[currentItem] as MonthlyTitleText; | ||||
|  | ||||
|           //     scrollLabelText.value = item.monthTitleText; | ||||
|           //   } | ||||
|  | ||||
|           //   return Text(scrollLabelText.value); | ||||
|           // }, | ||||
|           // labelConstraints: const BoxConstraints.tightFor(width: 200.0, height: 30.0), | ||||
|           controller: _scrollController, | ||||
|           heightScrollThumb: 40.0, | ||||
|           child: CustomScrollView( | ||||
|             controller: _scrollController, | ||||
|             thickness: 50, | ||||
|             crossAxisMargin: -20, | ||||
|             mainAxisMargin: 70, | ||||
|             timeToFade: const Duration(seconds: 2), | ||||
|             thumbColor: Colors.blueGrey, | ||||
|             radius: const Radius.circular(30), | ||||
|             child: CustomScrollView( | ||||
|               controller: _scrollController, | ||||
|               slivers: [ | ||||
|                 ImmichSliverAppBar(imageGridGroup: imageGridGroup), | ||||
|                 ...imageGridGroup, | ||||
|               ], | ||||
|             ), | ||||
|             slivers: [ | ||||
|               ImmichSliverAppBar(imageGridGroup: imageGridGroup), | ||||
|               ...imageGridGroup, | ||||
|             ], | ||||
|           ), | ||||
|         ]), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|       drawer: const ProfileDrawer(), | ||||
|       body: _buildBody(), | ||||
|       floatingActionButton: _showBackToTopBtn.value | ||||
|           ? FloatingActionButton.small( | ||||
|               enableFeedback: true, | ||||
|               backgroundColor: Theme.of(context).secondaryHeaderColor, | ||||
|               foregroundColor: Theme.of(context).primaryColor, | ||||
|               onPressed: () { | ||||
|                 _scrollController.animateTo(0, duration: const Duration(seconds: 1), curve: Curves.easeOutExpo); | ||||
|               }, | ||||
|               child: const Icon(Icons.keyboard_arrow_up_rounded), | ||||
|             ) | ||||
|           : null, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class MonthlyTitleText extends StatelessWidget { | ||||
|   const MonthlyTitleText({ | ||||
|     Key? key, | ||||
|     required this.monthTitleText, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final String monthTitleText; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SliverToBoxAdapter( | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.only(left: 10.0, top: 32), | ||||
|         child: Text( | ||||
|           monthTitleText, | ||||
|           style: TextStyle( | ||||
|             fontSize: 24, | ||||
|             fontWeight: FontWeight.bold, | ||||
|             color: Theme.of(context).primaryColor, | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user