) => {
- if (!showPreview || !videoRef.current) return;
-
- const now = Date.now();
- // Throttle scrubbing to ~60fps for smoother experience
- if (now - lastScrubTime.current < 16) return;
- lastScrubTime.current = now;
-
- const rect = e.currentTarget.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const progress = Math.max(0, Math.min(1, x / rect.width));
-
- // Scrub video based on mouse position with smooth seeking
- if (videoRef.current.duration && !isNaN(videoRef.current.duration)) {
- const targetTime = progress * videoRef.current.duration;
-
- // Use requestAnimationFrame for smoother seeking
- requestAnimationFrame(() => {
- if (videoRef.current) {
- videoRef.current.currentTime = targetTime;
- }
- });
- }
- };
// Delay preview start to avoid loading on quick mouse passes
- // Only enable previews on desktop devices with mouse
useEffect(() => {
- const isMobile = window.innerWidth < 768 || 'ontouchstart' in window;
-
- if (isHovered && !isMobile) {
+ if (isHovered) {
hoverTimeoutRef.current = setTimeout(() => {
setShowPreview(true);
}, 800); // Start preview after 800ms hover
@@ -102,89 +67,23 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
};
}, [isHovered]);
- // Setup HLS when preview is shown
- useEffect(() => {
- if (showPreview && videoRef.current && video.videoUrl) {
- const videoElement = videoRef.current;
-
- if (Hls.isSupported()) {
- console.log('Setting up HLS preview for:', video.title);
- hlsRef.current = new Hls({
- enableWorker: false,
- lowLatencyMode: true,
- backBufferLength: 10,
- maxBufferLength: 15,
- maxMaxBufferLength: 30,
- maxBufferSize: 30 * 1000 * 1000,
- maxBufferHole: 0.1,
- startLevel: -1, // Auto select lowest quality for fast start
- autoStartLoad: true,
- debug: false,
- liveSyncDurationCount: 3,
- liveMaxLatencyDurationCount: 10,
- startFragPrefetch: true,
- testBandwidth: false,
- });
-
- hlsRef.current.loadSource(video.videoUrl);
- hlsRef.current.attachMedia(videoElement);
-
- hlsRef.current.on(Hls.Events.MANIFEST_PARSED, () => {
- console.log('HLS manifest parsed, starting playback');
- // Enable audio and play
- videoElement.muted = false;
- videoElement.volume = 0.3; // Low volume for preview
- videoElement.play().catch(e => console.log('Autoplay failed:', e));
- });
-
- hlsRef.current.on(Hls.Events.ERROR, (event: any, data: any) => {
- console.log('HLS error:', data);
- });
- } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
- // Safari native HLS support
- videoElement.src = video.videoUrl;
- videoElement.muted = false;
- videoElement.volume = 0.3;
- videoElement.play().catch(e => console.log('Autoplay failed:', e));
- }
- }
-
- return () => {
- if (hlsRef.current) {
- hlsRef.current.destroy();
- hlsRef.current = null;
- }
- if (animationFrameRef.current) {
- cancelAnimationFrame(animationFrameRef.current);
- }
- };
- }, [showPreview, video.videoUrl]);
-
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Video preview container */}
onClick?.(video)}
- onMouseMove={handleMouseMove}
>
{/* Static thumbnail - always visible */}
+ onLoadStart={() => console.log('Preview loading for:', video.title)}
+ onError={(e) => console.log('Preview failed for:', video.title)}
+ >
+ {/* Try MP4 source first for faster loading */}
+ {video.mp4Url && (
+
+ )}
+ {/* Fallback to HLS if MP4 fails */}
+ {video.hlsUrl && (
+
+ )}
+
)}
- {/* Title overlay at bottom */}
-
-
onClick?.(video)}
- data-testid={`text-title-${video.id}`}
- >
- {video.title}
-
+ {/* Duration badge */}
+
+ {formatDuration(video.duration)}
+ {/* Play button overlay - hidden during preview */}
+ {!showPreview && (
+
+ )}
-
+
+
onClick?.(video)}
+ data-testid={`text-title-${video.id}`}
+ >
+ {video.title}
+
+
+
+ {formatViews(video.views)}
+
+
+ {formatDate(video.createdAt)}
+
+
+
);
}
diff --git a/client/src/components/video-modal.tsx b/client/src/components/video-modal.tsx
index f75632d..8d8d121 100644
--- a/client/src/components/video-modal.tsx
+++ b/client/src/components/video-modal.tsx
@@ -437,8 +437,7 @@ export default function VideoModal({ video, isOpen, onClose, enableAds = true }:
return (
diff --git a/client/src/index.css b/client/src/index.css
index 44ad051..285bc4d 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -2,32 +2,6 @@
@tailwind components;
@tailwind utilities;
-/* Hide scrollbars only on mobile devices */
-@media (max-width: 768px) {
- * {
- scrollbar-width: none !important;
- -ms-overflow-style: none !important;
- }
-
- *::-webkit-scrollbar {
- display: none !important;
- width: 0 !important;
- height: 0 !important;
- }
-
- html, body {
- scrollbar-width: none !important;
- -ms-overflow-style: none !important;
- }
-
- html::-webkit-scrollbar,
- body::-webkit-scrollbar {
- display: none !important;
- width: 0 !important;
- height: 0 !important;
- }
-}
-
:root {
--background: hsl(222, 84%, 4.9%);
--foreground: hsl(210, 40%, 98%);
@@ -189,27 +163,6 @@
.animation-delay-150 {
animation-delay: 150ms;
}
-
- /* Force modal z-index above everything */
- .modal-overlay {
- z-index: 2147483647 !important;
- position: fixed !important;
- top: 0 !important;
- left: 0 !important;
- right: 0 !important;
- bottom: 0 !important;
- width: 100vw !important;
- height: 100vh !important;
- }
-
- /* Keep video cards low */
- .video-card {
- z-index: 1 !important;
- }
-
- .video-card:hover {
- z-index: 5 !important;
- }
}
/* Video edit modal styles */
@@ -380,82 +333,4 @@ input[data-testid*="search"]::placeholder {
user-select: none;
opacity: 0.08;
filter: blur(0.5px);
-}
-
-/* Individual video hover effect for Top 10 numbers */
-.individual-video-hover:hover .individual-video-hover\:opacity-0 {
- opacity: 0;
-}
-
-/* Maximum possible z-index for video card hover */
-.video-card:hover {
- z-index: 2147483647 !important;
-}
-
-/* Oswald font povsod z fallback za desktop */
-.oswald-text {
- font-family: 'Oswald', 'Arial Black', 'Helvetica', sans-serif;
- font-weight: 600;
- letter-spacing: 0.05em;
- text-transform: uppercase;
- font-display: swap;
-}
-
-/* Container flex wrap styles */
-.container {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- gap: 10px; /* Razmik med slikami */
- padding: 10px;
-}
-
-.poster {
- width: 150px; /* Velikost slike */
- height: auto; /* Ohranja razmerje */
-}
-
-@media (max-width: 768px) {
- .container {
- flex-direction: column; /* Slike ena pod drugo */
- align-items: center;
- }
- .poster {
- width: 120px; /* Manjša velikost za mobilne naprave */
- }
-}
-
-/* Hide picture-in-picture button on all video elements */
-video::-webkit-media-controls-picture-in-picture-button {
- display: none !important;
- visibility: hidden !important;
- opacity: 0 !important;
- pointer-events: none !important;
-}
-
-video::-moz-picture-in-picture-button {
- display: none !important;
- visibility: hidden !important;
- opacity: 0 !important;
- pointer-events: none !important;
-}
-
-/* Hide Video.js picture-in-picture button */
-.video-js .vjs-picture-in-picture-control {
- display: none !important;
- visibility: hidden !important;
-}
-
-/* Hide all picture-in-picture related elements */
-*[aria-label*="picture"],
-*[title*="picture"],
-*[data-title*="picture"],
-*[class*="picture"],
-*[class*="pip"],
-.vjs-picture-in-picture-control,
-.vjs-pip-button {
- display: none !important;
- visibility: hidden !important;
- opacity: 0 !important;
- pointer-events: none !important;
}
\ No newline at end of file
diff --git a/client/src/pages/VideoPage.tsx b/client/src/pages/VideoPage.tsx
index ff5ccb0..2483443 100644
--- a/client/src/pages/VideoPage.tsx
+++ b/client/src/pages/VideoPage.tsx
@@ -252,7 +252,7 @@ export default function VideoPage() {
className="absolute inset-0 w-full h-full"
frameBorder="0"
allowFullScreen
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
onLoad={handleVideoPlay}
title={currentVideo.title}
/>
diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx
index aab667c..4b121b2 100644
--- a/client/src/pages/home.tsx
+++ b/client/src/pages/home.tsx
@@ -78,7 +78,7 @@ export default function Home() {
currentView={viewMode}
/>
-
+
{/* Trikotniki na robovih - ne prekrivajo video kartic */}
diff --git a/server/bunny.ts b/server/bunny.ts
index a1205bc..0f831b2 100644
--- a/server/bunny.ts
+++ b/server/bunny.ts
@@ -73,11 +73,9 @@ export class BunnyService {
private bunnyVideoToVideo(bunnyVideo: BunnyVideo | BunnyVideoDetails): Video {
// Generate optimized thumbnail URL from Bunny CDN with WebP format for better performance
- // Add cache busting timestamp to ensure fresh thumbnails
- const timestamp = Date.now();
const thumbnailUrl = bunnyVideo.thumbnailFileName
- ? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}?width=400&height=225&format=webp&t=${timestamp}`
- : `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg?width=400&height=225&format=webp&t=${timestamp}`;
+ ? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}?width=400&height=225&format=webp`
+ : `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg?width=400&height=225&format=webp`;
// Generate signed HLS URL for private video access
const hlsUrl = this.generateSignedUrl(bunnyVideo.guid);