diff --git a/bun.lock b/bun.lock
index fe41186b..6b714632 100644
--- a/bun.lock
+++ b/bun.lock
@@ -61,6 +61,7 @@
"react-native-device-info": "^15.0.0",
"react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "~2.28.0",
+ "react-native-glass-effect-view": "^1.0.0",
"react-native-google-cast": "^4.9.1",
"react-native-image-colors": "^2.4.0",
"react-native-ios-context-menu": "^3.2.1",
@@ -1632,6 +1633,8 @@
"react-native-gesture-handler": ["react-native-gesture-handler@2.28.0", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A=="],
+ "react-native-glass-effect-view": ["react-native-glass-effect-view@1.0.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ABYG0oIiqbXsxe2R/cMhNgDn3YgwDLz/2TIN2XOxQopXC+MiGsG9C32VYQvO2sYehcu5JmI3h3EzwLwl6lJhhA=="],
+
"react-native-google-cast": ["react-native-google-cast@4.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/HvIKAaWHtG6aTNCxrNrqA2ftWGkfH0M/2iN+28pdGUXpKmueb33mgL1m8D4zzwEODQMcmpfoCsym1IwDvugBQ=="],
"react-native-image-colors": ["react-native-image-colors@2.5.0", "", { "dependencies": { "node-vibrant": "^4.0.3" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-3zSDgNj5HaZ0PDWaXkc4BpWpZRM5N4gBsoPC7DBfM/+op69Yvwbc0S1T7CnxBWbvShtOvRE+b2BUBadVn+6z/g=="],
diff --git a/components/music/MiniPlayerBar.tsx b/components/music/MiniPlayerBar.tsx
index cf96e57d..f7f533c2 100644
--- a/components/music/MiniPlayerBar.tsx
+++ b/components/music/MiniPlayerBar.tsx
@@ -1,5 +1,4 @@
import { Ionicons } from "@expo/vector-icons";
-import { BlurView } from "expo-blur";
import { Image } from "expo-image";
import { useRouter } from "expo-router";
import { useAtom } from "jotai";
@@ -11,6 +10,17 @@ import {
TouchableOpacity,
View,
} from "react-native";
+import { Gesture, GestureDetector } from "react-native-gesture-handler";
+import { GlassEffectView } from "react-native-glass-effect-view";
+import Animated, {
+ Easing,
+ Extrapolation,
+ interpolate,
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { apiAtom } from "@/providers/JellyfinProvider";
@@ -20,6 +30,18 @@ const HORIZONTAL_MARGIN = Platform.OS === "android" ? 8 : 16;
const BOTTOM_TAB_HEIGHT = Platform.OS === "android" ? 56 : 52;
const BAR_HEIGHT = Platform.OS === "android" ? 58 : 50;
+// Gesture thresholds
+const VELOCITY_THRESHOLD = 1000;
+
+// Logarithmic slowdown - never stops, just gets progressively slower
+const rubberBand = (distance: number, scale: number = 8): number => {
+ "worklet";
+ const absDistance = Math.abs(distance);
+ const sign = distance < 0 ? -1 : 1;
+ // Logarithmic: keeps growing but slower and slower
+ return sign * scale * Math.log(1 + absDistance / scale);
+};
+
export const MiniPlayerBar: React.FC = () => {
const [api] = useAtom(apiAtom);
const insets = useSafeAreaInsets();
@@ -32,8 +54,12 @@ export const MiniPlayerBar: React.FC = () => {
duration,
togglePlayPause,
next,
+ stop,
} = useMusicPlayer();
+ // Gesture state
+ const translateY = useSharedValue(0);
+
const imageUrl = useMemo(() => {
if (!api || !currentTrack) return null;
const albumId = currentTrack.AlbumId || currentTrack.ParentId;
@@ -68,6 +94,66 @@ export const MiniPlayerBar: React.FC = () => {
[next],
);
+ const handleDismiss = useCallback(() => {
+ stop();
+ }, [stop]);
+
+ // Pan gesture for swipe up (open modal) and swipe down (dismiss)
+ const panGesture = Gesture.Pan()
+ .activeOffsetY([-15, 15])
+ .onUpdate((event) => {
+ // Logarithmic slowdown - keeps moving but progressively slower
+ translateY.value = rubberBand(event.translationY, 6);
+ })
+ .onEnd((event) => {
+ const velocity = event.velocityY;
+ const currentPosition = translateY.value;
+
+ // Swipe up - open modal (check position OR velocity)
+ if (currentPosition < -16 || velocity < -VELOCITY_THRESHOLD) {
+ runOnJS(handlePress)();
+ }
+ // Swipe down - stop playback and dismiss (check position OR velocity)
+ else if (currentPosition > 16 || velocity > VELOCITY_THRESHOLD) {
+ runOnJS(handleDismiss)();
+ }
+
+ // Smooth return to original position (no bounce)
+ translateY.value = withTiming(0, {
+ duration: 200,
+ easing: Easing.out(Easing.cubic),
+ });
+ });
+
+ // Tap gesture for opening modal (preserves existing behavior)
+ const tapGesture = Gesture.Tap().onEnd(() => {
+ runOnJS(handlePress)();
+ });
+
+ // Combine gestures - pan takes priority over tap
+ const composedGesture = Gesture.Race(panGesture, tapGesture);
+
+ // Animated styles for the container
+ const animatedContainerStyle = useAnimatedStyle(() => ({
+ transform: [{ translateY: translateY.value }],
+ }));
+
+ // Animated styles for the inner bar
+ const animatedBarStyle = useAnimatedStyle(() => ({
+ height: interpolate(
+ translateY.value,
+ [-50, 0, 50],
+ [BAR_HEIGHT + 12, BAR_HEIGHT, BAR_HEIGHT],
+ Extrapolation.EXTEND,
+ ),
+ opacity: interpolate(
+ translateY.value,
+ [0, 30],
+ [1, 0.6],
+ Extrapolation.CLAMP,
+ ),
+ }));
+
if (!currentTrack) return null;
const content = (
@@ -136,31 +222,40 @@ export const MiniPlayerBar: React.FC = () => {
);
return (
-
-
+
- {Platform.OS === "ios" ? (
-
- {content}
-
- ) : (
- {content}
- )}
-
-
+
+ {Platform.OS === "ios" ? (
+
+
+ {content}
+
+
+ ) : (
+ {content}
+ )}
+
+
+
);
};
@@ -180,20 +275,14 @@ const styles = StyleSheet.create({
overflow: "hidden",
},
blurContainer: {
- flexDirection: "row",
- alignItems: "center",
- paddingRight: 10,
- paddingLeft: 20,
- paddingVertical: 0,
- height: BAR_HEIGHT,
- backgroundColor: "rgba(40, 40, 40, 0.5)",
+ flex: 1,
},
androidContainer: {
+ flex: 1,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 8,
- height: BAR_HEIGHT,
backgroundColor: "rgba(28, 28, 30, 0.97)",
borderRadius: 14,
borderWidth: 0.5,
diff --git a/package.json b/package.json
index 3ed799fb..fa28a72c 100644
--- a/package.json
+++ b/package.json
@@ -80,6 +80,7 @@
"react-native-device-info": "^15.0.0",
"react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "~2.28.0",
+ "react-native-glass-effect-view": "^1.0.0",
"react-native-google-cast": "^4.9.1",
"react-native-image-colors": "^2.4.0",
"react-native-ios-context-menu": "^3.2.1",