Add swipe gesture to navigate between videos within the player
Implement touch and mouse drag gestures for seamless video navigation, allowing users to swipe left or right to move to the next or previous video, respectively. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 2eb1084e-b728-4449-9231-f1665924c8d5 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/2eb1084e-b728-4449-9231-f1665924c8d5/aSWZcEZ
This commit is contained in:
parent
19c1358391
commit
b9fa335300
@ -14,6 +14,8 @@ interface BunnyVideoModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onEdit?: () => void;
|
||||
videos?: Video[];
|
||||
onVideoChange?: (video: Video) => void;
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
@ -49,8 +51,11 @@ function formatDate(date: Date | string): string {
|
||||
return `Pred ${Math.floor(diffDays / 30)} mesec${Math.floor(diffDays / 30) > 1 ? 'i' : 'em'}`;
|
||||
}
|
||||
|
||||
export default function BunnyVideoModal({ video, isOpen, onClose, onEdit }: BunnyVideoModalProps) {
|
||||
export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos = [], onVideoChange }: BunnyVideoModalProps) {
|
||||
const [showShareMenu, setShowShareMenu] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||
const [dragOffset, setDragOffset] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
@ -137,6 +142,100 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit }: Bunn
|
||||
setShowShareMenu(false);
|
||||
};
|
||||
|
||||
// Navigation functions
|
||||
const getCurrentVideoIndex = () => {
|
||||
if (!video || !videos.length) return -1;
|
||||
return videos.findIndex(v => v.id === video.id);
|
||||
};
|
||||
|
||||
const navigateToVideo = (direction: 'next' | 'prev') => {
|
||||
const currentIndex = getCurrentVideoIndex();
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
let newIndex;
|
||||
if (direction === 'next') {
|
||||
newIndex = currentIndex + 1 >= videos.length ? 0 : currentIndex + 1;
|
||||
} else {
|
||||
newIndex = currentIndex - 1 < 0 ? videos.length - 1 : currentIndex - 1;
|
||||
}
|
||||
|
||||
const newVideo = videos[newIndex];
|
||||
if (newVideo && onVideoChange) {
|
||||
onVideoChange(newVideo);
|
||||
}
|
||||
};
|
||||
|
||||
// Touch and mouse drag handlers
|
||||
const handleDragStart = (clientX: number, clientY: number) => {
|
||||
setIsDragging(true);
|
||||
setDragStart({ x: clientX, y: clientY });
|
||||
setDragOffset(0);
|
||||
};
|
||||
|
||||
const handleDragMove = (clientX: number, clientY: number) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const deltaX = clientX - dragStart.x;
|
||||
const deltaY = Math.abs(clientY - dragStart.y);
|
||||
|
||||
// Only allow horizontal drag if it's more horizontal than vertical
|
||||
if (deltaY < Math.abs(deltaX)) {
|
||||
setDragOffset(deltaX);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const threshold = 100; // minimum drag distance to trigger navigation
|
||||
|
||||
if (Math.abs(dragOffset) > threshold && videos.length > 1) {
|
||||
if (dragOffset > 0) {
|
||||
navigateToVideo('prev'); // drag right = previous video
|
||||
} else {
|
||||
navigateToVideo('next'); // drag left = next video
|
||||
}
|
||||
}
|
||||
|
||||
setIsDragging(false);
|
||||
setDragOffset(0);
|
||||
};
|
||||
|
||||
// Mouse events
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) return; // only on video area
|
||||
handleDragStart(e.clientX, e.clientY);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
handleDragMove(e.clientX, e.clientY);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
handleDragEnd();
|
||||
};
|
||||
|
||||
// Touch events
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
const touch = e.touches[0];
|
||||
handleDragStart(touch.clientX, touch.clientY);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
if (!isDragging) return;
|
||||
const touch = e.touches[0];
|
||||
handleDragMove(touch.clientX, touch.clientY);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleTouchEnd = (e: React.TouchEvent) => {
|
||||
handleDragEnd();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
if (!isOpen || !video) return null;
|
||||
|
||||
return (
|
||||
@ -231,7 +330,17 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit }: Bunn
|
||||
<div className="flex-1 flex flex-col lg:flex-row gap-4 min-h-0">
|
||||
{/* Main video player */}
|
||||
<div className="flex-1">
|
||||
<div className="relative w-full h-0 pb-[56.25%] bg-black rounded-lg overflow-hidden">
|
||||
<div
|
||||
className="relative w-full h-0 pb-[56.25%] bg-black rounded-lg overflow-hidden cursor-grab active:cursor-grabbing select-none"
|
||||
style={{ transform: `translateX(${dragOffset}px)`, transition: isDragging ? 'none' : 'transform 0.3s ease-out' }}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{video.videoUrlIframe ? (
|
||||
<iframe
|
||||
src={video.videoUrlIframe}
|
||||
@ -248,6 +357,25 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit }: Bunn
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation indicators */}
|
||||
{videos.length > 1 && (
|
||||
<div className="absolute top-1/2 left-4 right-4 flex justify-between items-center pointer-events-none z-10">
|
||||
<div className={`bg-black bg-opacity-50 rounded-full p-2 text-white transition-opacity ${dragOffset > 50 ? 'opacity-100' : 'opacity-30'}`}>
|
||||
<span className="text-sm">← Prejšnji</span>
|
||||
</div>
|
||||
<div className={`bg-black bg-opacity-50 rounded-full p-2 text-white transition-opacity ${dragOffset < -50 ? 'opacity-100' : 'opacity-30'}`}>
|
||||
<span className="text-sm">Naslednji →</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Video counter */}
|
||||
{videos.length > 1 && (
|
||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-50 rounded-full px-3 py-1 text-white text-sm pointer-events-none">
|
||||
{getCurrentVideoIndex() + 1} od {videos.length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Video info sidebar */}
|
||||
|
||||
@ -27,6 +27,10 @@ export default function VideoGrid({ videos, isLoading, hasMore, onLoadMore, view
|
||||
setSelectedVideo(null);
|
||||
};
|
||||
|
||||
const handleVideoChange = (video: Video) => {
|
||||
setSelectedVideo(video);
|
||||
};
|
||||
|
||||
if (isLoading && videos.length === 0) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" data-testid="grid-loading">
|
||||
@ -99,6 +103,8 @@ export default function VideoGrid({ videos, isLoading, hasMore, onLoadMore, view
|
||||
video={selectedVideo}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
videos={videos}
|
||||
onVideoChange={handleVideoChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user