Skip to content

Commit 4a94fe5

Browse files
committed
Mouse drag (with momentum)
1 parent 21253c2 commit 4a94fe5

File tree

4 files changed

+102
-2
lines changed

4 files changed

+102
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@stronk-tech/react-librespot-controller",
33
"description": "`go-librespot` squeezebox-alike web frontend for small touchscreens",
4-
"version": "0.1.9",
4+
"version": "0.1.10",
55
"main": "dist/index.cjs.js",
66
"module": "dist/index.esm.js",
77
"files": [

src/components/Album/AlbumCard.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
position: relative;
144144
z-index: 7;
145145
transition: all 0.3s ease;
146+
pointer-events: none;
146147
}
147148

148149
.spotify-player-album-card-title-container {

src/components/Info/Playlists.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
width: 50px;
9999
height: 50px;
100100
border-radius: 0.5em;
101+
pointer-events: none;
101102
}
102103

103104
.spotify-player-playlist-info {

src/components/Info/Playlists.js

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ const Playlists = ({ playlists, onSelect, onPlay }) => {
88
const [arrowVisible, setArrowVisible] = useState(true);
99
const [arrowOffset, setArrowOffset] = useState(0);
1010
const wrapperRef = useRef(null);
11+
const scrollState = useRef({
12+
isDown: false,
13+
startX: 0,
14+
scrollLeft: 0,
15+
lastX: 0,
16+
lastTime: 0,
17+
velocity: 0,
18+
animationFrame: null
19+
});
1120

1221
const handleSelect = (playlist) => {
1322
setActivePlaylist(playlist.id);
@@ -22,14 +31,103 @@ const Playlists = ({ playlists, onSelect, onPlay }) => {
2231
setArrowOffset(scrollLeft);
2332
};
2433

34+
const animateScroll = () => {
35+
const wrapper = wrapperRef.current;
36+
if (!wrapper) return;
37+
38+
// Apply friction
39+
scrollState.current.velocity *= 0.95;
40+
41+
// Stop if velocity is very small
42+
if (Math.abs(scrollState.current.velocity) < 0.1) {
43+
scrollState.current.velocity = 0;
44+
return;
45+
}
46+
47+
// Update scroll position
48+
wrapper.scrollLeft -= scrollState.current.velocity;
49+
50+
// Bounce at boundaries
51+
if (wrapper.scrollLeft <= 0) {
52+
wrapper.scrollLeft = 0;
53+
scrollState.current.velocity = 0;
54+
} else if (wrapper.scrollLeft >= wrapper.scrollWidth - wrapper.clientWidth) {
55+
wrapper.scrollLeft = wrapper.scrollWidth - wrapper.clientWidth;
56+
scrollState.current.velocity = 0;
57+
}
58+
59+
scrollState.current.animationFrame = requestAnimationFrame(animateScroll);
60+
};
61+
2562
useEffect(() => {
2663
const wrapper = wrapperRef.current;
2764
if (!wrapper) return;
2865

66+
const handleMouseDown = (e) => {
67+
scrollState.current.isDown = true;
68+
scrollState.current.startX = e.pageX - wrapper.offsetLeft;
69+
scrollState.current.scrollLeft = wrapper.scrollLeft;
70+
scrollState.current.lastX = e.pageX;
71+
scrollState.current.lastTime = performance.now();
72+
scrollState.current.velocity = 0;
73+
74+
if (scrollState.current.animationFrame) {
75+
cancelAnimationFrame(scrollState.current.animationFrame);
76+
}
77+
78+
wrapper.style.cursor = 'grabbing';
79+
};
80+
81+
const handleMouseLeave = () => {
82+
scrollState.current.isDown = false;
83+
wrapper.style.cursor = 'grab';
84+
animateScroll();
85+
};
86+
87+
const handleMouseUp = () => {
88+
scrollState.current.isDown = false;
89+
wrapper.style.cursor = 'grab';
90+
animateScroll();
91+
};
92+
93+
const handleMouseMove = (e) => {
94+
if (!scrollState.current.isDown) return;
95+
e.preventDefault();
96+
97+
const now = performance.now();
98+
const deltaTime = now - scrollState.current.lastTime;
99+
const deltaX = e.pageX - scrollState.current.lastX;
100+
101+
// Calculate velocity (pixels per frame)
102+
scrollState.current.velocity = deltaX;
103+
104+
const x = e.pageX - wrapper.offsetLeft;
105+
const walkX = x - scrollState.current.startX;
106+
wrapper.scrollLeft = scrollState.current.scrollLeft - walkX;
107+
108+
scrollState.current.lastX = e.pageX;
109+
scrollState.current.lastTime = now;
110+
};
111+
112+
wrapper.addEventListener('mousedown', handleMouseDown);
113+
wrapper.addEventListener('mouseleave', handleMouseLeave);
114+
wrapper.addEventListener('mouseup', handleMouseUp);
115+
document.addEventListener('mousemove', handleMouseMove);
29116
wrapper.addEventListener("scroll", handleScroll);
117+
118+
wrapper.style.cursor = 'grab';
30119
handleScroll(); // Initialize visibility on mount
31120

32-
return () => wrapper.removeEventListener("scroll", handleScroll);
121+
return () => {
122+
wrapper.removeEventListener('mousedown', handleMouseDown);
123+
wrapper.removeEventListener('mouseleave', handleMouseLeave);
124+
wrapper.removeEventListener('mouseup', handleMouseUp);
125+
document.removeEventListener('mousemove', handleMouseMove);
126+
wrapper.removeEventListener("scroll", handleScroll);
127+
if (scrollState.current.animationFrame) {
128+
cancelAnimationFrame(scrollState.current.animationFrame);
129+
}
130+
};
33131
}, []);
34132

35133
return (

0 commit comments

Comments
 (0)