Skip to content

Commit

Permalink
feat: sliding up player support
Browse files Browse the repository at this point in the history
  • Loading branch information
KRTirtho committed Sep 30, 2023
1 parent b1d7942 commit 083319f
Show file tree
Hide file tree
Showing 5 changed files with 1,001 additions and 48 deletions.
48 changes: 36 additions & 12 deletions lib/components/player/player_overlay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_track_details.dart';
import 'package:spotube/components/root/spotube_navigation_bar.dart';
import 'package:spotube/components/shared/panels/sliding_up_panel.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/collections/intents.dart';
import 'package:spotube/hooks/use_progress.dart';
import 'package:spotube/pages/player/player.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/utils/service_utils.dart';

class PlayerOverlay extends HookConsumerWidget {
final String albumArt;
Expand Down Expand Up @@ -39,22 +41,31 @@ class PlayerOverlay extends HookConsumerWidget {
topRight: Radius.circular(10),
);

return GestureDetector(
onVerticalDragEnd: (details) {
int sensitivity = 8;
if (details.primaryVelocity != null &&
details.primaryVelocity! < -sensitivity) {
ServiceUtils.push(context, "/player");
}
final mediaQuery = MediaQuery.of(context);

final panelController = useMemoized(() => PanelController(), []);

useEffect(() {
return () {
panelController.dispose();
};
}, []);

return SlidingUpPanel(
maxHeight: mediaQuery.size.height - mediaQuery.padding.top,
backdropEnabled: false,
minHeight: canShow ? 53 : 0,
onPanelSlide: (position) {
final invertedPosition = 1 - position;
ref.read(navigationPanelHeight.notifier).state = 50 * invertedPosition;
},
child: ClipRRect(
collapsed: ClipRRect(
borderRadius: radius,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: MediaQuery.of(context).size.width,
height: canShow ? 53 : 0,
width: mediaQuery.size.width,
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer.withOpacity(.8),
borderRadius: radius,
Expand Down Expand Up @@ -165,6 +176,19 @@ class PlayerOverlay extends HookConsumerWidget {
),
),
),
panelBuilder: (position) {
final navigationHeight = ref.watch(navigationPanelHeight);
if (navigationHeight == 50) return const SizedBox();

return AnimatedContainer(
clipBehavior: Clip.antiAlias,
duration: const Duration(milliseconds: 250),
decoration: navigationHeight == 0
? const BoxDecoration(borderRadius: BorderRadius.zero)
: const BoxDecoration(borderRadius: radius),
child: const PlayerView(),
);
},
);
}
}
80 changes: 44 additions & 36 deletions lib/components/root/spotube_navigation_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import 'package:spotube/hooks/use_brightness_value.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart';

final navigationPanelHeight = StateProvider<double>((ref) => 50);

class SpotubeNavigationBar extends HookConsumerWidget {
final int selectedIndex;
final void Function(int) onSelectedIndexChanged;
Expand Down Expand Up @@ -41,6 +43,8 @@ class SpotubeNavigationBar extends HookConsumerWidget {
final navbarTileList =
useMemoized(() => getNavbarTileList(context.l10n), [context.l10n]);

final panelHeight = ref.watch(navigationPanelHeight);

useEffect(() {
insideSelectedIndex.value = selectedIndex;
return null;
Expand All @@ -51,44 +55,48 @@ class SpotubeNavigationBar extends HookConsumerWidget {
return const SizedBox();
}

return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: CurvedNavigationBar(
backgroundColor:
theme.colorScheme.secondaryContainer.withOpacity(0.72),
buttonBackgroundColor: buttonColor,
color: theme.colorScheme.background,
height: 50,
animationDuration: const Duration(milliseconds: 350),
items: navbarTileList.map(
(e) {
/// Using this [Builder] as an workaround for the first item's
/// icon color not updating unless navigating to another page
return Builder(builder: (context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: Badge(
isLabelVisible: e.id == "library" && downloadCount > 0,
label: Text(downloadCount.toString()),
child: Icon(
e.icon,
color: Theme.of(context).colorScheme.primary,
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
height: panelHeight,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: CurvedNavigationBar(
backgroundColor:
theme.colorScheme.secondaryContainer.withOpacity(0.72),
buttonBackgroundColor: buttonColor,
color: theme.colorScheme.background,
height: panelHeight,
animationDuration: const Duration(milliseconds: 350),
items: navbarTileList.map(
(e) {
/// Using this [Builder] as an workaround for the first item's
/// icon color not updating unless navigating to another page
return Builder(builder: (context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: Badge(
isLabelVisible: e.id == "library" && downloadCount > 0,
label: Text(downloadCount.toString()),
child: Icon(
e.icon,
color: Theme.of(context).colorScheme.primary,
),
),
),
);
});
);
});
},
).toList(),
index: insideSelectedIndex.value,
onTap: (i) {
insideSelectedIndex.value = i;
if (navbarTileList[i].id == "settings") {
Sidebar.goToSettings(context);
return;
}
onSelectedIndexChanged(i);
},
).toList(),
index: insideSelectedIndex.value,
onTap: (i) {
insideSelectedIndex.value = i;
if (navbarTileList[i].id == "settings") {
Sidebar.goToSettings(context);
return;
}
onSelectedIndexChanged(i);
},
),
),
),
);
Expand Down
142 changes: 142 additions & 0 deletions lib/components/shared/panels/controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
part of panels;

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<void> 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<void> close() {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._close();
}

/// Opens the sliding panel fully
/// (i.e. to the maxHeight)
Future<void> open() {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._open();
}

/// Hides the sliding panel (i.e. is invisible)
Future<void> hide() {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._hide();
}

/// Shows the sliding panel in its collapsed state
/// (i.e. "un-hide" the sliding panel)
Future<void> show() {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._show();
}

/// 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<void> 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<void> 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;
}
}
Loading

0 comments on commit 083319f

Please sign in to comment.