diff --git a/lib/components/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart index 61bca7b1..1296ae0e 100644 --- a/lib/components/dialogs/track_details_dialog.dart +++ b/lib/components/dialogs/track_details_dialog.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/links/artist_link.dart'; @@ -73,17 +73,15 @@ class TrackDetailsDialog extends HookWidget { }; return AlertDialog( - contentPadding: const EdgeInsets.all(16), - insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 100), - scrollable: true, + surfaceBlur: 0, + surfaceOpacity: 1, title: Row( - mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, children: [ const Icon(SpotubeIcons.info), - const SizedBox(width: 8), Text( context.l10n.details, - style: theme.textTheme.titleMedium, + style: theme.typography.h4, ), ], ), @@ -91,65 +89,64 @@ class TrackDetailsDialog extends HookWidget { width: mediaQuery.mdAndUp ? double.infinity : 700, child: Table( columnWidths: const { - 0: FixedColumnWidth(95), - 1: FixedColumnWidth(10), - 2: FlexColumnWidth(1), + 0: FixedTableSize(95), + 1: FixedTableSize(10), + 2: FlexTableSize(), }, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ + theme: const TableTheme( + backgroundColor: Colors.transparent, + cellTheme: TableCellTheme( + backgroundColor: WidgetStatePropertyAll(Colors.transparent), + ), + ), + rowHeights: const {0: FixedTableSize(40)}, + rows: [ for (final entry in detailsMap.entries) TableRow( - children: [ + cells: [ TableCell( - verticalAlignment: TableCellVerticalAlignment.top, child: Text( entry.key, - style: theme.textTheme.titleMedium, + style: theme.typography.bold, ), ), const TableCell( - verticalAlignment: TableCellVerticalAlignment.top, child: Text(":"), ), - if (entry.value is Widget) - entry.value as Widget - else if (entry.value is String) - Text( - entry.value as String, - style: theme.textTheme.bodyMedium, - ), + TableCell( + child: entry.value is Widget + ? entry.value as Widget + : (entry.value is String) + ? Text( + entry.value as String, + style: theme.typography.normal, + ) + : const Text(""), + ), ], ), - const TableRow( - children: [ - SizedBox(height: 16), - SizedBox(height: 16), - SizedBox(height: 16), - ], - ), for (final entry in ytTracksDetailsMap.entries) TableRow( - children: [ + cells: [ TableCell( - verticalAlignment: TableCellVerticalAlignment.top, child: Text( entry.key, - style: theme.textTheme.titleMedium, + style: theme.typography.bold, ), ), const TableCell( - verticalAlignment: TableCellVerticalAlignment.top, child: Text(":"), ), - if (entry.value is Widget) - entry.value as Widget - else - Text( - entry.value, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyMedium, - ), + TableCell( + child: entry.value is Widget + ? entry.value as Widget + : Text( + entry.value, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.typography.normal, + ), + ), ], ), ], diff --git a/lib/components/panels/controller.dart b/lib/components/panels/controller.dart deleted file mode 100644 index 4e367701..00000000 --- a/lib/components/panels/controller.dart +++ /dev/null @@ -1,146 +0,0 @@ -part of 'sliding_up_panel.dart'; - -class PanelController extends ChangeNotifier { - SlidingUpPanelState? _panelState; - - void _addState(SlidingUpPanelState panelState) { - _panelState = panelState; - notifyListeners(); - } - - bool _forceScrollChange = false; - - /// use this function when scroll change in func - /// Example: - /// panelController.forseScrollChange(scrollController.animateTo(100, duration: Duration(milliseconds: 400), curve: Curves.ease)) - Future forceScrollChange(Future func) async { - _forceScrollChange = true; - _panelState!._scrollingEnabled = true; - await func; - // if (_panelState!._sc.offset == 0) { - // _panelState!._scrollingEnabled = true; - // } - if (panelPosition < 1) { - _panelState!._scMinOffset = _panelState!._scrollController.offset; - } - _forceScrollChange = false; - } - - bool __nowTargetForceDraggable = false; - - bool get _nowTargetForceDraggable => __nowTargetForceDraggable; - - set _nowTargetForceDraggable(bool value) { - __nowTargetForceDraggable = value; - notifyListeners(); - } - - /// Determine if the panelController is attached to an instance - /// of the SlidingUpPanel (this property must return true before any other - /// functions can be used) - bool get isAttached => _panelState != null; - - /// Closes the sliding panel to its collapsed state (i.e. to the minHeight) - Future close() async { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - await _panelState!._close(); - notifyListeners(); - } - - /// Opens the sliding panel fully - /// (i.e. to the maxHeight) - Future open() async { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - await _panelState!._open(); - notifyListeners(); - } - - /// Hides the sliding panel (i.e. is invisible) - Future hide() async { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - await _panelState!._hide(); - notifyListeners(); - } - - /// Shows the sliding panel in its collapsed state - /// (i.e. "un-hide" the sliding panel) - Future show() async { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - await _panelState!._show(); - notifyListeners(); - } - - /// Animates the panel position to the value. - /// The value must between 0.0 and 1.0 - /// where 0.0 is fully collapsed and 1.0 is completely open. - /// (optional) duration specifies the time for the animation to complete - /// (optional) curve specifies the easing behavior of the animation. - Future animatePanelToPosition(double value, - {Duration? duration, Curve curve = Curves.linear}) { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - assert(0.0 <= value && value <= 1.0); - return _panelState! - ._animatePanelToPosition(value, duration: duration, curve: curve); - } - - /// Animates the panel position to the snap point - /// Requires that the SlidingUpPanel snapPoint property is not null - /// (optional) duration specifies the time for the animation to complete - /// (optional) curve specifies the easing behavior of the animation. - Future animatePanelToSnapPoint( - {Duration? duration, Curve curve = Curves.linear}) { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - assert(_panelState!.widget.snapPoint != null, - "SlidingUpPanel snapPoint property must not be null"); - return _panelState! - ._animatePanelToSnapPoint(duration: duration, curve: curve); - } - - /// Sets the panel position (without animation). - /// The value must between 0.0 and 1.0 - /// where 0.0 is fully collapsed and 1.0 is completely open. - set panelPosition(double value) { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - assert(0.0 <= value && value <= 1.0); - _panelState!._panelPosition = value; - } - - /// Gets the current panel position. - /// Returns the % offset from collapsed state - /// to the open state - /// as a decimal between 0.0 and 1.0 - /// where 0.0 is fully collapsed and - /// 1.0 is full open. - double get panelPosition { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - return _panelState!._panelPosition; - } - - /// Returns whether or not the panel is - /// currently animating. - bool get isPanelAnimating { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - return _panelState!._isPanelAnimating; - } - - /// Returns whether or not the - /// panel is open. - bool get isPanelOpen { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - return _panelState!._isPanelOpen; - } - - /// Returns whether or not the - /// panel is closed. - bool get isPanelClosed { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - return _panelState!._isPanelClosed; - } - - /// Returns whether or not the - /// panel is shown/hidden. - bool get isPanelShown { - assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - return _panelState!._isPanelShown; - } -} diff --git a/lib/components/panels/helpers.dart b/lib/components/panels/helpers.dart deleted file mode 100644 index d79fa97c..00000000 --- a/lib/components/panels/helpers.dart +++ /dev/null @@ -1,95 +0,0 @@ -part of "sliding_up_panel.dart"; - -/// if you want to prevent the panel from being dragged using the widget, -/// wrap the widget with this -class IgnoreDraggableWidget extends SingleChildRenderObjectWidget { - const IgnoreDraggableWidget({ - super.key, - required super.child, - }); - - @override - IgnoreDraggableWidgetWidgetRenderBox createRenderObject( - BuildContext context, - ) { - return IgnoreDraggableWidgetWidgetRenderBox(); - } -} - -class IgnoreDraggableWidgetWidgetRenderBox extends RenderPointerListener { - @override - HitTestBehavior get behavior => HitTestBehavior.opaque; -} - -/// if you want to force the panel to be dragged using the widget, -/// wrap the widget with this -/// For example, use [Scrollable] inside to allow the panel to be dragged -/// even if the scroll is not at position 0. -class ForceDraggableWidget extends SingleChildRenderObjectWidget { - const ForceDraggableWidget({ - super.key, - required super.child, - }); - - @override - ForceDraggableWidgetRenderBox createRenderObject( - BuildContext context, - ) { - return ForceDraggableWidgetRenderBox(); - } -} - -class ForceDraggableWidgetRenderBox extends RenderPointerListener { - @override - HitTestBehavior get behavior => HitTestBehavior.opaque; -} - -/// To make [ForceDraggableWidget] work in [Scrollable] widgets -class PanelScrollPhysics extends ScrollPhysics { - final PanelController controller; - const PanelScrollPhysics({required this.controller, super.parent}); - @override - PanelScrollPhysics applyTo(ScrollPhysics? ancestor) { - return PanelScrollPhysics( - controller: controller, parent: buildParent(ancestor)); - } - - @override - double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { - if (controller._nowTargetForceDraggable) return 0.0; - return super.applyPhysicsToUserOffset(position, offset); - } - - @override - Simulation? createBallisticSimulation( - ScrollMetrics position, double velocity) { - if (controller._nowTargetForceDraggable) { - return super.createBallisticSimulation(position, 0); - } - return super.createBallisticSimulation(position, velocity); - } - - @override - bool get allowImplicitScrolling => false; -} - -/// if you want to prevent unwanted panel dragging when scrolling widgets [Scrollable] with horizontal axis -/// wrap the widget with this -class HorizontalScrollableWidget extends SingleChildRenderObjectWidget { - const HorizontalScrollableWidget({ - super.key, - required super.child, - }); - - @override - HorizontalScrollableWidgetRenderBox createRenderObject( - BuildContext context, - ) { - return HorizontalScrollableWidgetRenderBox(); - } -} - -class HorizontalScrollableWidgetRenderBox extends RenderPointerListener { - @override - HitTestBehavior get behavior => HitTestBehavior.opaque; -} diff --git a/lib/components/panels/sliding_up_panel.dart b/lib/components/panels/sliding_up_panel.dart deleted file mode 100644 index e99fe261..00000000 --- a/lib/components/panels/sliding_up_panel.dart +++ /dev/null @@ -1,685 +0,0 @@ -/* -Name: Zotov Vladimir -Date: 18/06/22 -Purpose: Defines the package: sliding_up_panel2 -Copyright: © 2022, Zotov Vladimir. All rights reserved. -Licensing: More information can be found here: https://github.com/Zotov-VD/sliding_up_panel/blob/master/LICENSE - -This product includes software developed by Akshath Jain (https://akshathjain.com) -*/ - -library panels; - -import 'dart:math'; - -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/physics.dart'; -import 'package:flutter/rendering.dart'; - -part 'controller.dart'; -part 'helpers.dart'; - -enum SlideDirection { up, down } - -enum PanelState { open, closed } - -class SlidingUpPanel extends StatefulWidget { - /// Returns the Widget that slides into view. When the - /// panel is collapsed and if [collapsed] is null, - /// then top portion of this Widget will be displayed; - /// otherwise, [collapsed] will be displayed overtop - /// of this Widget. - final Widget? Function(double position)? panelBuilder; - - /// The Widget displayed overtop the [panel] when collapsed. - /// This fades out as the panel is opened. - final Widget? collapsed; - - /// The Widget that lies underneath the sliding panel. - /// This Widget automatically sizes itself - /// to fill the screen. - final Widget? body; - - /// Optional persistent widget that floats above the [panel] and attaches - /// to the top of the [panel]. Content at the top of the panel will be covered - /// by this widget. Add padding to the bottom of the `panel` to - /// avoid coverage. - final Widget? header; - - /// Optional persistent widget that floats above the [panel] and - /// attaches to the bottom of the [panel]. Content at the bottom of the panel - /// will be covered by this widget. Add padding to the bottom of the `panel` - /// to avoid coverage. - final Widget? footer; - - /// The height of the sliding panel when fully collapsed. - final double minHeight; - - /// The height of the sliding panel when fully open. - final double maxHeight; - - /// A point between [minHeight] and [maxHeight] that the panel snaps to - /// while animating. A fast swipe on the panel will disregard this point - /// and go directly to the open/close position. This value is represented as a - /// percentage of the total animation distance ([maxHeight] - [minHeight]), - /// so it must be between 0.0 and 1.0, exclusive. - final double? snapPoint; - - /// The amount to inset the children of the sliding panel sheet. - final EdgeInsetsGeometry? padding; - - /// Empty space surrounding the sliding panel sheet. - final EdgeInsetsGeometry? margin; - - /// Set to false to disable the panel from snapping open or closed. - final bool panelSnapping; - - /// Disable panel draggable on scrolling. Defaults to false. - final bool disableDraggableOnScrolling; - - /// If non-null, this can be used to control the state of the panel. - final PanelController? controller; - - /// If non-null, shows a darkening shadow over the [body] as the panel slides open. - final bool backdropEnabled; - - /// Shows a darkening shadow of this [Color] over the [body] as the panel slides open. - final Color backdropColor; - - /// The opacity of the backdrop when the panel is fully open. - /// This value can range from 0.0 to 1.0 where 0.0 is completely transparent - /// and 1.0 is completely opaque. - final double backdropOpacity; - - /// Flag that indicates whether or not tapping the - /// backdrop closes the panel. Defaults to true. - final bool backdropTapClosesPanel; - - /// If non-null, this callback - /// is called as the panel slides around with the - /// current position of the panel. The position is a double - /// between 0.0 and 1.0 where 0.0 is fully collapsed and 1.0 is fully open. - final void Function(double position)? onPanelSlide; - - /// If non-null, this callback is called when the - /// panel is fully opened - final VoidCallback? onPanelOpened; - - /// If non-null, this callback is called when the panel - /// is fully collapsed. - final VoidCallback? onPanelClosed; - - /// If non-null and true, the SlidingUpPanel exhibits a - /// parallax effect as the panel slides up. Essentially, - /// the body slides up as the panel slides up. - final bool parallaxEnabled; - - /// Allows for specifying the extent of the parallax effect in terms - /// of the percentage the panel has slid up/down. Recommended values are - /// within 0.0 and 1.0 where 0.0 is no parallax and 1.0 mimics a - /// one-to-one scrolling effect. Defaults to a 10% parallax. - final double parallaxOffset; - - /// Allows toggling of the draggability of the SlidingUpPanel. - /// Set this to false to prevent the user from being able to drag - /// the panel up and down. Defaults to true. - final bool isDraggable; - - /// Either SlideDirection.UP or SlideDirection.DOWN. Indicates which way - /// the panel should slide. Defaults to UP. If set to DOWN, the panel attaches - /// itself to the top of the screen and is fully opened when the user swipes - /// down on the panel. - final SlideDirection slideDirection; - - /// The default state of the panel; either PanelState.OPEN or PanelState.CLOSED. - /// This value defaults to PanelState.CLOSED which indicates that the panel is - /// in the closed position and must be opened. PanelState.OPEN indicates that - /// by default the Panel is open and must be swiped closed by the user. - final PanelState defaultPanelState; - - /// To attach to a [Scrollable] on a panel that - /// links the panel's position to the scroll position. Useful for implementing - /// infinite scroll behavior - final ScrollController? scrollController; - - final BoxDecoration? panelDecoration; - - const SlidingUpPanel( - {super.key, - this.body, - this.collapsed, - this.minHeight = 100.0, - this.maxHeight = 500.0, - this.snapPoint, - this.padding, - this.margin, - this.panelDecoration, - this.panelSnapping = true, - this.disableDraggableOnScrolling = false, - this.controller, - this.backdropEnabled = false, - this.backdropColor = Colors.black, - this.backdropOpacity = 0.5, - this.backdropTapClosesPanel = true, - this.onPanelSlide, - this.onPanelOpened, - this.onPanelClosed, - this.parallaxEnabled = false, - this.parallaxOffset = 0.1, - this.isDraggable = true, - this.slideDirection = SlideDirection.up, - this.defaultPanelState = PanelState.closed, - this.header, - this.footer, - this.scrollController, - this.panelBuilder}) - : assert(panelBuilder != null), - assert(0 <= backdropOpacity && backdropOpacity <= 1.0), - assert(snapPoint == null || 0 < snapPoint && snapPoint < 1.0); - - @override - SlidingUpPanelState createState() => SlidingUpPanelState(); -} - -class SlidingUpPanelState extends State - with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late final ScrollController _scrollController; - - bool _scrollingEnabled = false; - final VelocityTracker _velocityTracker = - VelocityTracker.withKind(PointerDeviceKind.touch); - - bool _isPanelVisible = true; - - @override - void initState() { - super.initState(); - - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - value: widget.defaultPanelState == PanelState.closed - ? 0.0 - : 1.0 //set the default panel state (i.e. set initial value of _ac) - ) - ..addListener(() { - if (widget.onPanelSlide != null) { - widget.onPanelSlide!(_animationController.value); - } - - if (widget.onPanelOpened != null && - (_animationController.value == 1.0 || - _animationController.value == 0.0)) { - widget.onPanelOpened!(); - } - }); - - // prevent the panel content from being scrolled only if the widget is - // draggable and panel scrolling is enabled - _scrollController = widget.scrollController ?? ScrollController(); - _scrollController.addListener(() { - if (widget.isDraggable && - !widget.disableDraggableOnScrolling && - (!_scrollingEnabled || _panelPosition < 1) && - widget.controller?._forceScrollChange != true) { - _scrollController.jumpTo(_scMinOffset); - } - }); - - widget.controller?._addState(this); - } - - @override - Widget build(BuildContext context) { - final mediaQuery = MediaQuery.of(context); - - return Stack( - alignment: widget.slideDirection == SlideDirection.up - ? Alignment.bottomCenter - : Alignment.topCenter, - children: [ - //make the back widget take up the entire back side - if (widget.body != null) - AnimatedBuilder( - animation: _animationController, - builder: (context, child) { - return Positioned( - top: widget.parallaxEnabled ? _getParallax() : 0.0, - child: child ?? const SizedBox(), - ); - }, - child: SizedBox( - height: mediaQuery.size.height, - width: mediaQuery.size.width, - child: widget.body, - ), - ), - - //the backdrop to overlay on the body - if (widget.backdropEnabled) - GestureDetector( - onVerticalDragEnd: widget.backdropTapClosesPanel - ? (DragEndDetails details) { - // only trigger a close if the drag is towards panel close position - if ((widget.slideDirection == SlideDirection.up ? 1 : -1) * - details.velocity.pixelsPerSecond.dy > - 0) _close(); - } - : null, - onTap: widget.backdropTapClosesPanel ? () => _close() : null, - child: AnimatedBuilder( - animation: _animationController, - builder: (context, _) { - return Container( - height: mediaQuery.size.height, - width: mediaQuery.size.width, - - //set color to null so that touch events pass through - //to the body when the panel is closed, otherwise, - //if a color exists, then touch events won't go through - color: _animationController.value == 0.0 - ? null - : widget.backdropColor.withOpacity( - widget.backdropOpacity * _animationController.value, - ), - ); - }), - ), - - //the actual sliding part - if (_isPanelVisible) - _gestureHandler( - child: AnimatedBuilder( - animation: _animationController, - builder: (context, child) { - return Container( - height: _animationController.value * - (widget.maxHeight - widget.minHeight) + - widget.minHeight, - margin: widget.margin, - padding: widget.padding, - decoration: widget.panelDecoration, - child: child, - ); - }, - child: Stack( - children: [ - //open panel - Positioned( - top: - widget.slideDirection == SlideDirection.up ? 0.0 : null, - bottom: widget.slideDirection == SlideDirection.down - ? 0.0 - : null, - width: mediaQuery.size.width - - (widget.margin != null - ? widget.margin!.horizontal - : 0) - - (widget.padding != null - ? widget.padding!.horizontal - : 0), - child: SizedBox( - height: widget.maxHeight, - child: widget.panelBuilder!( - _animationController.value, - ), - ), - ), - - // footer - if (widget.footer != null) - Positioned( - top: widget.slideDirection == SlideDirection.up - ? null - : 0.0, - bottom: widget.slideDirection == SlideDirection.down - ? null - : 0.0, - child: widget.footer ?? const SizedBox()), - - // header - if (widget.header != null) - Positioned( - top: widget.slideDirection == SlideDirection.up - ? 0.0 - : null, - bottom: widget.slideDirection == SlideDirection.down - ? 0.0 - : null, - child: widget.header ?? const SizedBox(), - ), - - // collapsed panel - Positioned( - top: - widget.slideDirection == SlideDirection.up ? 0.0 : null, - bottom: widget.slideDirection == SlideDirection.down - ? 0.0 - : null, - width: mediaQuery.size.width - - (widget.margin != null - ? widget.margin!.horizontal - : 0) - - (widget.padding != null - ? widget.padding!.horizontal - : 0), - child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - height: widget.minHeight, - child: widget.collapsed == null - ? null - : FadeTransition( - opacity: Tween(begin: 1.0, end: 0.0) - .animate(_animationController), - - // if the panel is open ignore pointers (touch events) on the collapsed - // child so that way touch events go through to whatever is underneath - child: IgnorePointer( - ignoring: _animationController.value == 1.0, - child: widget.collapsed, - ), - ), - ), - ), - ], - ), - ), - ), - ], - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - double _getParallax() { - if (widget.slideDirection == SlideDirection.up) { - return -_animationController.value * - (widget.maxHeight - widget.minHeight) * - widget.parallaxOffset; - } else { - return _animationController.value * - (widget.maxHeight - widget.minHeight) * - widget.parallaxOffset; - } - } - - bool _ignoreScrollable = false; - bool _isHorizontalScrollableWidget = false; - Axis? _scrollableAxis; - - // returns a gesture detector if panel is used - // and a listener if panelBuilder is used. - // this is because the listener is designed only for use with linking the scrolling of - // panels and using it for panels that don't want to linked scrolling yields odd results - Widget _gestureHandler({required Widget child}) { - if (!widget.isDraggable) return child; - - return Listener( - onPointerDown: (PointerDownEvent e) { - var rb = context.findRenderObject() as RenderBox; - var result = BoxHitTestResult(); - rb.hitTest(result, position: e.position); - - if (_panelPosition == 1) { - _scMinOffset = 0.0; - } - // if there any widget in the path that must force graggable, - // stop it right here - if (result.path.any((entry) => - entry.target.runtimeType == ForceDraggableWidgetRenderBox)) { - widget.controller?._nowTargetForceDraggable = true; - _scMinOffset = _scrollController.offset; - _isHorizontalScrollableWidget = false; - } else if (result.path.any((entry) => - entry.target.runtimeType == HorizontalScrollableWidgetRenderBox)) { - _isHorizontalScrollableWidget = true; - widget.controller?._nowTargetForceDraggable = false; - } else if (result.path.any((entry) => - entry.target.runtimeType == IgnoreDraggableWidgetWidgetRenderBox)) { - _ignoreScrollable = true; - widget.controller?._nowTargetForceDraggable = false; - _isHorizontalScrollableWidget = false; - return; - } else { - widget.controller?._nowTargetForceDraggable = false; - _isHorizontalScrollableWidget = false; - } - _ignoreScrollable = false; - _velocityTracker.addPosition(e.timeStamp, e.position); - }, - onPointerMove: (PointerMoveEvent e) { - if (_scrollableAxis == null) { - if (e.delta.dx.abs() > e.delta.dy.abs()) { - _scrollableAxis = Axis.horizontal; - } else { - _scrollableAxis = Axis.vertical; - } - } - - if (_isHorizontalScrollableWidget && - _scrollableAxis == Axis.horizontal) { - return; - } - - if (_ignoreScrollable) return; - _velocityTracker.addPosition( - e.timeStamp, - e.position, - ); // add current position for velocity tracking - _onGestureSlide(e.delta.dy); - }, - onPointerUp: (PointerUpEvent e) { - if (_ignoreScrollable) return; - _scrollableAxis = null; - _onGestureEnd(_velocityTracker.getVelocity()); - }, - child: child, - ); - } - - double _scMinOffset = 0.0; - - // handles the sliding gesture - void _onGestureSlide(double dy) { - // only slide the panel if scrolling is not enabled - if (widget.controller?._nowTargetForceDraggable == false && - widget.disableDraggableOnScrolling) { - return; - } - if ((!_scrollingEnabled) || - _panelPosition < 1 || - widget.controller?._nowTargetForceDraggable == true) { - if (widget.slideDirection == SlideDirection.up) { - _animationController.value -= - dy / (widget.maxHeight - widget.minHeight); - } else { - _animationController.value += - dy / (widget.maxHeight - widget.minHeight); - } - } - - // if the panel is open and the user hasn't scrolled, we need to determine - // whether to enable scrolling if the user swipes up, or disable closing and - // begin to close the panel if the user swipes down - if (_isPanelOpen && - _scrollController.hasClients && - _scrollController.offset <= _scMinOffset) { - setState(() { - if (dy < 0) { - _scrollingEnabled = true; - } else { - _scrollingEnabled = false; - } - }); - } - } - - // handles when user stops sliding - void _onGestureEnd(Velocity v) { - if (widget.controller?._nowTargetForceDraggable == false && - widget.disableDraggableOnScrolling) { - return; - } - double minFlingVelocity = 365.0; - double kSnap = 8; - - //let the current animation finish before starting a new one - if (_animationController.isAnimating) return; - - // if scrolling is allowed and the panel is open, we don't want to close - // the panel if they swipe up on the scrollable - if (_isPanelOpen && _scrollingEnabled) return; - - //check if the velocity is sufficient to constitute fling to end - double visualVelocity = - -v.pixelsPerSecond.dy / (widget.maxHeight - widget.minHeight); - - // reverse visual velocity to account for slide direction - if (widget.slideDirection == SlideDirection.down) { - visualVelocity = -visualVelocity; - } - - // get minimum distances to figure out where the panel is at - double d2Close = _animationController.value; - double d2Open = 1 - _animationController.value; - double d2Snap = ((widget.snapPoint ?? 3) - _animationController.value) - .abs(); // large value if null results in not every being the min - double minDistance = min(d2Close, min(d2Snap, d2Open)); - - // check if velocity is sufficient for a fling - if (v.pixelsPerSecond.dy.abs() >= minFlingVelocity) { - // snapPoint exists - if (widget.panelSnapping && widget.snapPoint != null) { - if (v.pixelsPerSecond.dy.abs() >= kSnap * minFlingVelocity || - minDistance == d2Snap) { - _animationController.fling(velocity: visualVelocity); - } else { - _flingPanelToPosition(widget.snapPoint!, visualVelocity); - } - - // no snap point exists - } else if (widget.panelSnapping) { - _animationController.fling(velocity: visualVelocity); - - // panel snapping disabled - } else { - _animationController.animateTo( - _animationController.value + visualVelocity * 0.16, - duration: const Duration(milliseconds: 410), - curve: Curves.decelerate, - ); - } - - return; - } - - // check if the controller is already halfway there - if (widget.panelSnapping) { - if (minDistance == d2Close) { - _close(); - } else if (minDistance == d2Snap) { - _flingPanelToPosition(widget.snapPoint!, visualVelocity); - } else { - _open(); - } - } - } - - void _flingPanelToPosition(double targetPos, double velocity) { - final Simulation simulation = SpringSimulation( - SpringDescription.withDampingRatio( - mass: 1.0, - stiffness: 500.0, - ratio: 1.0, - ), - _animationController.value, - targetPos, - velocity); - - _animationController.animateWith(simulation); - } - - //--------------------------------- - //PanelController related functions - //--------------------------------- - - //close the panel - Future _close() { - return _animationController.fling(velocity: -1.0); - } - - //open the panel - Future _open() { - return _animationController.fling(velocity: 1.0); - } - - //hide the panel (completely offscreen) - Future _hide() { - return _animationController.fling(velocity: -1.0).then((x) { - setState(() { - _isPanelVisible = false; - }); - }); - } - - //show the panel (in collapsed mode) - Future _show() { - return _animationController.fling(velocity: -1.0).then((x) { - setState(() { - _isPanelVisible = true; - }); - }); - } - - //animate the panel position to value - must - //be between 0.0 and 1.0 - Future _animatePanelToPosition(double value, - {Duration? duration, Curve curve = Curves.linear}) { - assert(0.0 <= value && value <= 1.0); - return _animationController.animateTo(value, - duration: duration, curve: curve); - } - - //animate the panel position to the snap point - //REQUIRES that widget.snapPoint != null - Future _animatePanelToSnapPoint( - {Duration? duration, Curve curve = Curves.linear}) { - assert(widget.snapPoint != null); - return _animationController.animateTo(widget.snapPoint!, - duration: duration, curve: curve); - } - - //set the panel position to value - must - //be between 0.0 and 1.0 - set _panelPosition(double value) { - assert(0.0 <= value && value <= 1.0); - _animationController.value = value; - } - - //get the current panel position - //returns the % offset from collapsed state - //as a decimal between 0.0 and 1.0 - double get _panelPosition => _animationController.value; - - //returns whether or not - //the panel is still animating - bool get _isPanelAnimating => _animationController.isAnimating; - - //returns whether or not the - //panel is open - bool get _isPanelOpen => _animationController.value == 1.0; - - //returns whether or not the - //panel is closed - bool get _isPanelClosed => _animationController.value == 0.0; - - //returns whether or not the - //panel is shown/hidden - bool get _isPanelShown => _isPanelVisible; -} diff --git a/lib/components/track_presentation/track_presentation.dart b/lib/components/track_presentation/track_presentation.dart index 96f5f964..4f1db832 100644 --- a/lib/components/track_presentation/track_presentation.dart +++ b/lib/components/track_presentation/track_presentation.dart @@ -51,6 +51,7 @@ class TrackPresentation extends HookConsumerWidget { return Data.inherit( data: options, child: SafeArea( + bottom: false, child: Scaffold( headers: const [TitleBar()], child: CustomScrollView( diff --git a/lib/main.dart b/lib/main.dart index 2f93ea87..57cfbd88 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -222,7 +222,7 @@ class Spotube extends HookConsumerWidget { darkTheme: ThemeData( radius: .5, iconTheme: const IconThemeProperties(), - colorScheme: ColorSchemes.darkNeutral(), + colorScheme: ColorSchemes.darkOrange(), surfaceOpacity: .8, surfaceBlur: 10, ), diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 014e6aef..0c915b08 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -1,11 +1,11 @@ import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' show showModalBottomSheet; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shadcn_flutter/shadcn_flutter.dart' - show openDrawer, OverlayPosition; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; +import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; @@ -14,19 +14,16 @@ import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/volume_slider.dart'; -import 'package:spotube/components/animated_gradient.dart'; import 'package:spotube/components/dialogs/track_details_dialog.dart'; import 'package:spotube/components/links/artist_link.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/image/universal_image.dart'; -import 'package:spotube/components/panels/sliding_up_panel.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; -import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/models/local_track.dart'; +import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/pages/lyrics/lyrics.dart'; import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/authentication/authentication.dart'; @@ -58,6 +55,16 @@ class PlayerView extends HookConsumerWidget { final isLocalTrack = currentTrack is LocalTrack; final mediaQuery = MediaQuery.of(context); + final shouldHide = useState(true); + + ref.listen(navigationPanelHeight, (_, height) { + shouldHide.value = height.ceil() == 50; + }); + + if (shouldHide.value) { + return const SizedBox(); + } + useEffect(() { if (mediaQuery.lgAndUp) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -74,15 +81,6 @@ class PlayerView extends HookConsumerWidget { [currentTrack?.album?.images], ); - final palette = usePaletteGenerator(albumArt); - final titleTextColor = palette.dominantColor?.titleTextColor; - final bodyTextColor = palette.dominantColor?.bodyTextColor; - - final bgColor = palette.dominantColor?.color ?? theme.colorScheme.primary; - - final GlobalKey scaffoldKey = - useMemoized(() => GlobalKey(), []); - useEffect(() { for (final renderView in WidgetsBinding.instance.renderViews) { renderView.automaticSystemUiAdjustment = false; @@ -93,324 +91,230 @@ class PlayerView extends HookConsumerWidget { renderView.automaticSystemUiAdjustment = true; } }; - }, [panelController.isPanelOpen]); - - useCustomStatusBarColor( - bgColor, - panelController.isPanelOpen, - noSetBGColor: true, - automaticSystemUiAdjustment: false, - ); - - final topPadding = MediaQueryData.fromView(View.of(context)).padding.top; + }, [panelController.isAttached && panelController.isPanelOpen]); return AppPopScope( canPop: context.canPop(), onPopInvoked: (didPop) async { await panelController.close(); }, - child: IconTheme( - data: theme.iconTheme.copyWith(color: bodyTextColor), - child: AnimateGradient( - animateAlignments: true, - primaryBegin: Alignment.topLeft, - primaryEnd: Alignment.bottomLeft, - secondaryBegin: Alignment.bottomRight, - secondaryEnd: Alignment.topRight, - duration: const Duration(seconds: 15), - primaryColors: [ - palette.dominantColor?.color ?? theme.colorScheme.primary, - palette.mutedColor?.color ?? theme.colorScheme.secondary, - ], - secondaryColors: [ - (palette.darkVibrantColor ?? palette.lightVibrantColor)?.color ?? - theme.colorScheme.primaryContainer, - (palette.darkMutedColor ?? palette.lightMutedColor)?.color ?? - theme.colorScheme.secondaryContainer, - ], - child: Scaffold( - key: scaffoldKey, - backgroundColor: Colors.transparent, - appBar: PreferredSize( - preferredSize: Size.fromHeight( - kToolbarHeight + topPadding, - ), - child: ForceDraggableWidget( - child: Padding( - padding: EdgeInsets.only(top: topPadding), - child: TitleBar( - backgroundColor: Colors.transparent, - leading: [ - IconButton( - icon: const Icon(SpotubeIcons.angleDown, size: 18), - onPressed: panelController.close, - ) - ], - trailing: [ - if (currentTrack is YoutubeSourcedTrack) - TextButton.icon( - icon: Assets.logos.songlinkTransparent.image( - width: 20, - height: 20, - color: bodyTextColor, - ), - label: Text(context.l10n.song_link), - style: TextButton.styleFrom( - foregroundColor: bodyTextColor, - padding: const EdgeInsets.symmetric(horizontal: 10), - ), - onPressed: () { - final url = - "https://song.link/s/${currentTrack.id}"; + child: Scaffold( + headers: [ + SafeArea( + child: TitleBar( + surfaceOpacity: 0, + surfaceBlur: 0, + leading: [ + IconButton.ghost( + icon: const Icon(SpotubeIcons.angleDown, size: 18), + onPressed: panelController.close, + ) + ], + trailing: [ + if (currentTrack is YoutubeSourcedTrack) + TextButton( + leading: Assets.logos.songlinkTransparent.image( + width: 20, + height: 20, + color: theme.colorScheme.foreground, + ), + onPressed: () { + final url = "https://song.link/s/${currentTrack.id}"; - launchUrlString(url); + launchUrlString(url); + }, + child: Text(context.l10n.song_link), + ), + Tooltip( + tooltip: TooltipContainer( + child: Text(context.l10n.details), + ), + child: IconButton.ghost( + icon: const Icon(SpotubeIcons.info, size: 18), + onPressed: currentTrack == null + ? null + : () { + showDialog( + context: context, + builder: (context) { + return TrackDetailsDialog( + track: currentTrack, + ); + }); }, - ), - IconButton( - icon: const Icon(SpotubeIcons.info, size: 18), - tooltip: context.l10n.details, - style: IconButton.styleFrom( - foregroundColor: bodyTextColor, - ), - onPressed: currentTrack == null - ? null - : () { - showDialog( - context: context, - builder: (context) { - return TrackDetailsDialog( - track: currentTrack, - ); - }); - }, - ) + ), + ) + ], + ), + ), + ], + child: SingleChildScrollView( + controller: scrollController, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(8), + constraints: + const BoxConstraints(maxHeight: 300, maxWidth: 300), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(100), + spreadRadius: 2, + blurRadius: 10, + offset: Offset.zero, + ), ], ), - ), - ), - ), - extendBodyBehindAppBar: true, - body: SingleChildScrollView( - controller: scrollController, - child: Container( - alignment: Alignment.center, - width: double.infinity, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 580), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - ForceDraggableWidget( - child: Container( - margin: const EdgeInsets.all(8), - constraints: const BoxConstraints( - maxHeight: 300, maxWidth: 300), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - spreadRadius: 2, - blurRadius: 10, - offset: Offset(0, 0), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: UniversalImage( - path: albumArt, - placeholder: Assets.albumPlaceholder.path, - fit: BoxFit.cover, - ), - ), - ), - ), - const SizedBox(height: 60), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - alignment: Alignment.centerLeft, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AutoSizeText( - currentTrack?.name ?? - context.l10n.not_playing, - style: TextStyle( - color: titleTextColor, - fontSize: 22, - ), - maxFontSize: 22, - maxLines: 1, - textAlign: TextAlign.start, - ), - if (isLocalTrack) - Text( - currentTrack.artists?.asString() ?? "", - style: theme.textTheme.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, - color: bodyTextColor, - ), - ) - else - ArtistLink( - artists: currentTrack?.artists ?? [], - textStyle: - theme.textTheme.bodyMedium!.copyWith( - fontWeight: FontWeight.bold, - color: bodyTextColor, - ), - onRouteChange: (route) { - panelController.close(); - GoRouter.of(context).push(route); - }, - onOverflowArtistClick: () => - ServiceUtils.pushNamed( - context, - TrackPage.name, - pathParameters: { - "id": currentTrack!.id!, - }, - ), - ), - ], - ), - ), - const SizedBox(height: 10), - PlayerControls(palette: palette), - const SizedBox(height: 25), - const PlayerActions( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - showQueue: false, - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const SizedBox(width: 10), - Expanded( - child: OutlinedButton.icon( - icon: const Icon(SpotubeIcons.queue), - label: Text(context.l10n.queue), - style: OutlinedButton.styleFrom( - foregroundColor: bodyTextColor, - side: BorderSide( - color: bodyTextColor ?? Colors.white, - ), - ), - // enabled: currentTrack != null, - onPressed: () { - openDrawer( - context: context, - barrierDismissible: true, - draggable: true, - barrierColor: Colors.black12, - borderRadius: BorderRadius.circular(10), - transformBackdrop: false, - position: OverlayPosition.bottom, - surfaceBlur: context.theme.surfaceBlur, - surfaceOpacity: 0.7, - expands: true, - builder: (context) => Consumer( - builder: (context, ref, _) { - final playlist = ref.watch( - audioPlayerProvider, - ); - final playlistNotifier = ref.read( - audioPlayerProvider.notifier); - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context) - .size - .height * - 0.8, - ), - child: PlayerQueue - .fromAudioPlayerNotifier( - floating: false, - playlist: playlist, - notifier: playlistNotifier, - ), - ); - }, - ), - ); - }, - ), - ), - if (auth.asData?.value != null) - const SizedBox(width: 10), - if (auth.asData?.value != null) - Expanded( - child: OutlinedButton.icon( - label: Text(context.l10n.lyrics), - icon: const Icon(SpotubeIcons.music), - style: OutlinedButton.styleFrom( - foregroundColor: bodyTextColor, - side: BorderSide( - color: bodyTextColor ?? Colors.white, - ), - ), - onPressed: () { - showModalBottomSheet( - context: context, - isDismissible: true, - enableDrag: true, - isScrollControlled: true, - backgroundColor: Colors.black38, - barrierColor: Colors.black12, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - builder: (context) => - const LyricsPage(isModal: true), - ); - }, - ), - ), - const SizedBox(width: 10), - ], - ), - const SizedBox(height: 25), - SliderTheme( - data: theme.sliderTheme.copyWith( - activeTrackColor: titleTextColor, - inactiveTrackColor: bodyTextColor, - thumbColor: titleTextColor, - overlayColor: titleTextColor?.withOpacity(0.2), - trackHeight: 2, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 8, - ), - ), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16), - child: Consumer(builder: (context, ref, _) { - final volume = ref.watch(volumeProvider); - return VolumeSlider( - fullWidth: true, - value: volume, - onChanged: (value) { - ref - .read(volumeProvider.notifier) - .setVolume(value); - }, - ); - }), - ), - ), - ], - ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: UniversalImage( + path: albumArt, + placeholder: Assets.albumPlaceholder.path, + fit: BoxFit.cover, ), ), ), - ), + const SizedBox(height: 60), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText( + currentTrack?.name ?? context.l10n.not_playing, + style: const TextStyle(fontSize: 22), + maxFontSize: 22, + maxLines: 1, + textAlign: TextAlign.start, + ), + if (isLocalTrack) + Text( + currentTrack.artists?.asString() ?? "", + style: theme.typography.normal + .copyWith(fontWeight: FontWeight.bold), + ) + else + ArtistLink( + artists: currentTrack?.artists ?? [], + textStyle: theme.typography.normal + .copyWith(fontWeight: FontWeight.bold), + onRouteChange: (route) { + panelController.close(); + GoRouter.of(context).push(route); + }, + onOverflowArtistClick: () => ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": currentTrack!.id!, + }, + ), + ), + ], + ), + ), + const SizedBox(height: 10), + const PlayerControls(), + const SizedBox(height: 25), + const PlayerActions( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + showQueue: false, + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const SizedBox(width: 10), + Expanded( + child: OutlineButton( + leading: const Icon(SpotubeIcons.queue), + child: Text(context.l10n.queue), + onPressed: () { + openDrawer( + context: context, + barrierDismissible: true, + draggable: true, + barrierColor: Colors.black.withAlpha(100), + borderRadius: BorderRadius.circular(10), + transformBackdrop: false, + position: OverlayPosition.bottom, + surfaceBlur: context.theme.surfaceBlur, + surfaceOpacity: 0.7, + expands: true, + builder: (context) => Consumer( + builder: (context, ref, _) { + final playlist = ref.watch( + audioPlayerProvider, + ); + final playlistNotifier = + ref.read(audioPlayerProvider.notifier); + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context).size.height * + 0.8, + ), + child: PlayerQueue.fromAudioPlayerNotifier( + floating: false, + playlist: playlist, + notifier: playlistNotifier, + ), + ); + }, + ), + ); + }, + ), + ), + if (auth.asData?.value != null) const SizedBox(width: 10), + if (auth.asData?.value != null) + Expanded( + child: OutlineButton( + leading: const Icon(SpotubeIcons.music), + child: Text(context.l10n.lyrics), + onPressed: () { + showModalBottomSheet( + context: context, + isDismissible: true, + enableDrag: true, + isScrollControlled: true, + backgroundColor: Colors.black.withAlpha(100), + barrierColor: Colors.black.withAlpha(100), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + builder: (context) => + const LyricsPage(isModal: true), + ); + }, + ), + ), + const SizedBox(width: 10), + ], + ), + const SizedBox(height: 25), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Consumer(builder: (context, ref, _) { + final volume = ref.watch(volumeProvider); + return VolumeSlider( + fullWidth: true, + value: volume, + onChanged: (value) { + ref.read(volumeProvider.notifier).setVolume(value); + }, + ); + }), + ), + ], ), ), ), diff --git a/lib/modules/player/player_overlay.dart b/lib/modules/player/player_overlay.dart index 2322bcba..a37ac5bc 100644 --- a/lib/modules/player/player_overlay.dart +++ b/lib/modules/player/player_overlay.dart @@ -1,19 +1,12 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:sliding_up_panel/sliding_up_panel.dart'; +import 'package:spotube/modules/player/player_overlay_collapsed.dart'; -import 'package:spotube/modules/player/player_track_details.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart'; -import 'package:spotube/components/panels/sliding_up_panel.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/collections/intents.dart'; -import 'package:spotube/modules/player/use_progress.dart'; import 'package:spotube/modules/player/player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/audio_player/querying_track_info.dart'; -import 'package:spotube/services/audio_player/audio_player.dart'; class PlayerOverlay extends HookConsumerWidget { final String albumArt; @@ -25,180 +18,34 @@ class PlayerOverlay extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final playlist = ref.watch(audioPlayerProvider); final canShow = playlist.activeTrack != null; - final playing = - useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; - - final theme = Theme.of(context); - final textColor = theme.colorScheme.primary; - - const radius = BorderRadius.only( - topLeft: Radius.circular(10), - topRight: Radius.circular(10), - ); - - final mediaQuery = MediaQuery.of(context); + final screenSize = MediaQuery.sizeOf(context); final panelController = useMemoized(() => PanelController(), []); - final scrollController = useScrollController(); - - useEffect(() { - return () { - panelController.dispose(); - }; - }, []); return SlidingUpPanel( - maxHeight: mediaQuery.size.height, + maxHeight: screenSize.height, backdropEnabled: false, - minHeight: canShow ? 53 : 0, + minHeight: canShow ? 63 : 0, onPanelSlide: (position) { final invertedPosition = 1 - position; ref.read(navigationPanelHeight.notifier).state = 50 * invertedPosition; }, controller: panelController, - collapsed: ClipRRect( - borderRadius: radius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), - child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - width: mediaQuery.size.width, - decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer.withOpacity(.8), - borderRadius: radius, - ), - child: AnimatedOpacity( - duration: const Duration(milliseconds: 250), - opacity: canShow ? 1 : 0, - child: Material( - type: MaterialType.transparency, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - HookBuilder( - builder: (context) { - final progress = useProgress(ref); - // animated - return TweenAnimationBuilder( - duration: const Duration(milliseconds: 250), - tween: Tween( - begin: 0, - end: progress.progressStatic, - ), - builder: (context, value, child) { - return LinearProgressIndicator( - value: value, - minHeight: 2, - backgroundColor: Colors.transparent, - valueColor: AlwaysStoppedAnimation( - theme.colorScheme.primary, - ), - ); - }, - ); - }, - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: GestureDetector( - onTap: () { - panelController.open(); - }, - child: Container( - width: double.infinity, - color: Colors.transparent, - child: PlayerTrackDetails( - track: playlist.activeTrack, - color: textColor, - ), - ), - ), - ), - Row( - children: [ - IconButton( - icon: Icon( - SpotubeIcons.skipBack, - color: textColor, - ), - onPressed: isFetchingActiveTrack - ? null - : audioPlayer.skipToPrevious, - ), - Consumer( - builder: (context, ref, _) { - return IconButton( - icon: isFetchingActiveTrack - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ) - : Icon( - playing - ? SpotubeIcons.pause - : SpotubeIcons.play, - color: textColor, - ), - onPressed: Actions.handler( - context, - PlayPauseIntent(ref), - ), - ); - }, - ), - IconButton( - icon: Icon( - SpotubeIcons.skipForward, - color: textColor, - ), - onPressed: isFetchingActiveTrack - ? null - : audioPlayer.skipToNext, - ), - ], - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), + color: Colors.transparent, + parallaxEnabled: true, + renderPanelSheet: false, + header: SizedBox( + height: 63, + width: screenSize.width, + child: PlayerOverlayCollapsedSection(panelController: panelController), + ), + panelBuilder: (scrollController) => PlayerView( + panelController: panelController, + scrollController: scrollController, ), - scrollController: scrollController, - panelBuilder: (position) { - // this is the reason we're getting an update - final navigationHeight = ref.watch(navigationPanelHeight); - - if (navigationHeight == 50) return const SizedBox(); - - return IgnorePointer( - ignoring: !panelController.isPanelOpen, - child: AnimatedContainer( - clipBehavior: Clip.antiAlias, - duration: const Duration(milliseconds: 250), - decoration: navigationHeight == 0 - ? const BoxDecoration(borderRadius: BorderRadius.zero) - : const BoxDecoration(borderRadius: radius), - child: IgnoreDraggableWidget( - child: PlayerView( - panelController: panelController, - scrollController: scrollController, - ), - ), - ), - ); - }, ); } } diff --git a/lib/modules/player/player_overlay_collapsed.dart b/lib/modules/player/player_overlay_collapsed.dart new file mode 100644 index 00000000..52847c40 --- /dev/null +++ b/lib/modules/player/player_overlay_collapsed.dart @@ -0,0 +1,118 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:sliding_up_panel/sliding_up_panel.dart'; +import 'package:spotube/collections/intents.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/modules/player/player_track_details.dart'; +import 'package:spotube/modules/root/spotube_navigation_bar.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/audio_player/querying_track_info.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; + +class PlayerOverlayCollapsedSection extends HookConsumerWidget { + final PanelController panelController; + const PlayerOverlayCollapsedSection({ + super.key, + required this.panelController, + }); + + @override + Widget build(BuildContext context, ref) { + final playlist = ref.watch(audioPlayerProvider); + final canShow = playlist.activeTrack != null; + + final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); + final playing = + useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; + + final theme = Theme.of(context); + + final shouldShow = useState(true); + + ref.listen(navigationPanelHeight, (_, height) { + shouldShow.value = height.ceil() == 50; + }); + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: canShow && shouldShow.value + ? Padding( + padding: const EdgeInsets.all(5), + child: SurfaceCard( + borderWidth: 0, + surfaceBlur: theme.surfaceBlur, + surfaceOpacity: theme.surfaceOpacity, + padding: EdgeInsets.zero, + borderRadius: theme.borderRadiusLg, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + panelController.open(); + }, + child: Container( + width: double.infinity, + color: Colors.transparent, + child: PlayerTrackDetails( + track: playlist.activeTrack, + color: theme.colorScheme.foreground, + ), + ), + ), + ), + Row( + children: [ + IconButton.ghost( + icon: const Icon(SpotubeIcons.skipBack), + onPressed: isFetchingActiveTrack + ? null + : audioPlayer.skipToPrevious, + ), + Consumer( + builder: (context, ref, _) { + return IconButton.ghost( + icon: isFetchingActiveTrack + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(), + ) + : Icon( + playing + ? SpotubeIcons.pause + : SpotubeIcons.play, + ), + onPressed: Actions.handler( + context, + PlayPauseIntent(ref), + ), + ); + }, + ), + IconButton.ghost( + icon: const Icon(SpotubeIcons.skipForward), + onPressed: isFetchingActiveTrack + ? null + : audioPlayer.skipToNext, + ), + const Gap(5), + ], + ), + ], + ), + ), + ], + ), + ), + ) + : const SizedBox.shrink(), + ); + } +} diff --git a/lib/modules/root/spotube_navigation_bar.dart b/lib/modules/root/spotube_navigation_bar.dart index fa585054..8d3e71f4 100644 --- a/lib/modules/root/spotube_navigation_bar.dart +++ b/lib/modules/root/spotube_navigation_bar.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/extensions/constrains.dart'; @@ -52,24 +53,29 @@ class SpotubeNavigationBar extends HookConsumerWidget { return AnimatedContainer( duration: const Duration(milliseconds: 100), - child: NavigationBar( - index: selectedIndex, - onSelected: (i) { - ServiceUtils.navigateNamed(context, navbarTileList[i].name); - }, - children: [ - for (final tile in navbarTileList) - NavigationButton( - style: const ButtonStyle.muted(density: ButtonDensity.icon), - selectedStyle: - const ButtonStyle.fixed(density: ButtonDensity.icon), - child: Badge( - isLabelVisible: tile.id == "library" && downloadCount > 0, - label: Text(downloadCount.toString()), - child: Icon(tile.icon), - ), - ) - ], + height: panelHeight, + child: SingleChildScrollView( + child: NavigationBar( + index: selectedIndex, + surfaceBlur: context.theme.surfaceBlur, + surfaceOpacity: context.theme.surfaceOpacity, + onSelected: (i) { + ServiceUtils.navigateNamed(context, navbarTileList[i].name); + }, + children: [ + for (final tile in navbarTileList) + NavigationButton( + style: const ButtonStyle.muted(density: ButtonDensity.icon), + selectedStyle: + const ButtonStyle.fixed(density: ButtonDensity.icon), + child: Badge( + isLabelVisible: tile.id == "library" && downloadCount > 0, + label: Text(downloadCount.toString()), + child: Icon(tile.icon), + ), + ) + ], + ), ), ); } diff --git a/lib/pages/lyrics/plain_lyrics.dart b/lib/pages/lyrics/plain_lyrics.dart index 6246fd79..0b5354a0 100644 --- a/lib/pages/lyrics/plain_lyrics.dart +++ b/lib/pages/lyrics/plain_lyrics.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/lyrics/zoom_controls.dart'; import 'package:spotube/components/shimmers/shimmer_lyrics.dart'; @@ -53,7 +54,9 @@ class PlainLyrics extends HookConsumerWidget { child: Text( playlist.activeTrack?.artists?.asString() ?? "", style: (mediaQuery.mdAndUp ? typography.h4 : typography.large) - .copyWith(color: palette.bodyTextColor), + .copyWith( + color: palette.bodyTextColor, + ), ), ) ], @@ -103,7 +106,9 @@ class PlainLyrics extends HookConsumerWidget { return AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 200), style: TextStyle( - color: palette.bodyTextColor, + color: isModal == true + ? context.theme.colorScheme.foreground + : palette.bodyTextColor, fontSize: 24 * textZoomLevel.value / 100, height: textZoomLevel.value < 70 ? 1.5 diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 1367fa5a..b7423e14 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -34,9 +34,11 @@ class SyncedLyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final mediaQuery = MediaQuery.sizeOf(context); + final theme = Theme.of(context); + final playlist = ref.watch(audioPlayerProvider); - final mediaQuery = MediaQuery.sizeOf(context); final controller = useAutoScrollController(); final delay = ref.watch(syncedLyricsDelayProvider); @@ -70,7 +72,9 @@ class SyncedLyrics extends HookConsumerWidget { final headlineTextStyle = (mediaQuery.mdAndUp ? typography.h3 : typography.h4.copyWith(fontSize: 25)) - .copyWith(color: palette.titleTextColor); + .copyWith( + color: palette.titleTextColor, + ); final bodyTextTheme = typography.large.copyWith( color: palette.bodyTextColor, @@ -182,9 +186,15 @@ class SyncedLyrics extends HookConsumerWidget { text: lyricSlice.text, textStyle: DefaultTextStyle.of(context).style, - textColor: isActive - ? Colors.white - : palette.bodyTextColor, + textColor: switch (( + isActive, + isModal == true + )) { + (true, _) => Colors.white, + (_, true) => + theme.colorScheme.mutedForeground, + (_, _) => palette.bodyTextColor, + }, strokeColor: isActive ? Colors.black : Colors.transparent, diff --git a/pubspec.lock b/pubspec.lock index 8c8c30be..a43f4f2c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2101,6 +2101,14 @@ packages: description: flutter source: sdk version: "0.0.0" + sliding_up_panel: + dependency: "direct main" + description: + name: sliding_up_panel + sha256: "578e90956a6212d1e406373250b2436a0f3afece29aee3c24c8360094d6cf968" + url: "https://pub.dev" + source: hosted + version: "2.0.0+1" sliver_tools: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e06cf96c..35738548 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -112,6 +112,7 @@ dependencies: shelf_web_socket: ^2.0.0 simple_icons: ^10.1.3 skeletonizer: ^1.1.1 + sliding_up_panel: ^2.0.0+1 sliver_tools: ^0.2.12 smtc_windows: ^1.0.0 spotify: ^0.13.7