Restored to '7fd7127003398a269f1f765208c8baced728aafb'
Replit-Restored-To: 7fd7127003
1
.replit
@ -40,3 +40,4 @@ args = "npm run dev"
|
|||||||
waitForPort = 5000
|
waitForPort = 5000
|
||||||
|
|
||||||
[agent]
|
[agent]
|
||||||
|
integrations = ["javascript_log_in_with_replit==1.0.0", "javascript_database==1.0.0", "javascript_object_storage==1.0.0"]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.5 MiB |
|
Before Width: | Height: | Size: 741 KiB |
|
Before Width: | Height: | Size: 618 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 355 KiB |
|
Before Width: | Height: | Size: 600 KiB |
|
Before Width: | Height: | Size: 629 KiB |
|
Before Width: | Height: | Size: 529 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 177 KiB |
@ -26,19 +26,6 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="theme-color" content="#3b82f6" />
|
<meta name="theme-color" content="#3b82f6" />
|
||||||
|
|
||||||
<!-- Oswald Google Font z improved loading -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
/* Fallback font za desktop če se Oswald ne naloži */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Oswald-fallback';
|
|
||||||
src: local('Arial Black'), local('Helvetica');
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export default function AdSettings({ isOpen, onClose }: AdSettingsProps) {
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4" style={{ zIndex: 2147483647, position: 'fixed' }}>
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
<div className="bg-white dark:bg-gray-900 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<div className="sticky top-0 bg-white dark:bg-gray-900 p-6 border-b border-gray-200 dark:border-gray-700">
|
<div className="sticky top-0 bg-white dark:bg-gray-900 p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@ -168,19 +168,9 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="modal-overlay fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center"
|
className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50"
|
||||||
style={{
|
|
||||||
zIndex: 2147483647,
|
|
||||||
backgroundColor: '#1f2937',
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: '100vw',
|
|
||||||
height: '100vh'
|
|
||||||
}}
|
|
||||||
onClick={handleBackdropClick}
|
onClick={handleBackdropClick}
|
||||||
|
style={{ backgroundColor: '#1f2937' }}
|
||||||
>
|
>
|
||||||
<div className="relative w-full h-full max-w-7xl mx-auto p-4 flex flex-col">
|
<div className="relative w-full h-full max-w-7xl mx-auto p-4 flex flex-col">
|
||||||
{/* Header with close button */}
|
{/* Header with close button */}
|
||||||
@ -275,7 +265,7 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
|
|||||||
className="absolute inset-0 w-full h-full"
|
className="absolute inset-0 w-full h-full"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
onLoad={handleVideoPlay}
|
onLoad={handleVideoPlay}
|
||||||
title={video.title}
|
title={video.title}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { type Video } from "@shared/schema";
|
import { type Video } from "@shared/schema";
|
||||||
import VideoCard from "./video-card";
|
import VideoCard from "./video-card";
|
||||||
import BunnyVideoModal from "./bunny-video-modal";
|
import BunnyVideoModal from "./bunny-video-modal";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import SimpleCarousel from "./simple-carousel";
|
|
||||||
|
|
||||||
interface VideoCategory {
|
interface VideoCategory {
|
||||||
title: string;
|
title: string;
|
||||||
@ -106,9 +105,9 @@ export default function NetflixGrid({ videos, isLoading }: NetflixGridProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-12 relative">
|
<div className="space-y-12">
|
||||||
{categories.map((category, categoryIndex) => (
|
{categories.map((category, categoryIndex) => (
|
||||||
<SimpleCarousel
|
<CategoryRow
|
||||||
key={categoryIndex}
|
key={categoryIndex}
|
||||||
category={category}
|
category={category}
|
||||||
onVideoClick={handleVideoClick}
|
onVideoClick={handleVideoClick}
|
||||||
@ -132,88 +131,23 @@ interface CategoryRowProps {
|
|||||||
onVideoClick: (video: Video) => void;
|
onVideoClick: (video: Video) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CategoryRowOLD_BROKEN({ category, onVideoClick }: CategoryRowProps) {
|
function CategoryRow({ category, onVideoClick }: CategoryRowProps) {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [showLeftButton, setShowLeftButton] = useState(false);
|
|
||||||
const [showRightButton, setShowRightButton] = useState(true);
|
|
||||||
const [isScrolling, setIsScrolling] = useState(false);
|
|
||||||
const [speedMode, setSpeedMode] = useState<'normal' | 'fast'>('normal');
|
|
||||||
|
|
||||||
const scroll = (direction: 'left' | 'right') => {
|
const scroll = (direction: 'left' | 'right') => {
|
||||||
if (!scrollContainerRef.current) return;
|
if (scrollRef.current) {
|
||||||
|
// Responsive scroll amount - wider cards on desktop
|
||||||
|
const isMobile = window.innerWidth < 768;
|
||||||
|
const scrollAmount = isMobile ? 240 : 336; // Mobile: 224px + gap, Desktop: 320px + gap
|
||||||
|
const currentScroll = scrollRef.current.scrollLeft;
|
||||||
|
const targetScroll = direction === 'left'
|
||||||
|
? currentScroll - scrollAmount
|
||||||
|
: currentScroll + scrollAmount;
|
||||||
|
|
||||||
const scrollAmount = direction === 'right' ? 300 : -300;
|
scrollRef.current.scrollTo({
|
||||||
scrollContainerRef.current.scrollBy({
|
left: targetScroll,
|
||||||
left: scrollAmount,
|
behavior: 'smooth'
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSpeed = (direction: 'left' | 'right') => {
|
|
||||||
// Toggle speed mode and restart animation with new speed
|
|
||||||
const newSpeed = speedMode === 'normal' ? 'fast' : 'normal';
|
|
||||||
setSpeedMode(newSpeed);
|
|
||||||
|
|
||||||
// Restart the animation with new speed
|
|
||||||
if (isScrolling && scrollIntervalRef.current) {
|
|
||||||
clearInterval(scrollIntervalRef.current);
|
|
||||||
|
|
||||||
// Start new interval with updated speed immediately
|
|
||||||
const baseSpeed = newSpeed === 'fast' ? 2.5 : 0.8;
|
|
||||||
const interval = 16; // Fixed interval - speed controlled by pixel movement only
|
|
||||||
|
|
||||||
scrollIntervalRef.current = setInterval(() => {
|
|
||||||
if (!scrollContainerRef.current) return;
|
|
||||||
|
|
||||||
const currentSpeed = newSpeed === 'fast' ? 5 : 2;
|
|
||||||
const scrollAmount = direction === 'right' ? currentSpeed : -currentSpeed;
|
|
||||||
scrollContainerRef.current.scrollBy({
|
|
||||||
left: scrollAmount,
|
|
||||||
behavior: 'auto'
|
|
||||||
});
|
|
||||||
}, 16);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const startAutoScroll = (direction: 'left' | 'right') => {
|
|
||||||
// Clear any existing interval
|
|
||||||
if (scrollIntervalRef.current) {
|
|
||||||
clearInterval(scrollIntervalRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsScrolling(true);
|
|
||||||
|
|
||||||
// Start continuous smooth scrolling with variable speed
|
|
||||||
scrollIntervalRef.current = setInterval(() => {
|
|
||||||
if (!scrollContainerRef.current) return;
|
|
||||||
|
|
||||||
const baseSpeed = 3; // Faster speed on hover
|
|
||||||
const scrollAmount = direction === 'right' ? baseSpeed : -baseSpeed;
|
|
||||||
scrollContainerRef.current.scrollBy({
|
|
||||||
left: scrollAmount,
|
|
||||||
behavior: 'auto'
|
|
||||||
});
|
});
|
||||||
}, 16);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize with first video on the left side
|
|
||||||
useEffect(() => {
|
|
||||||
// No need to set initial position for scroll - browser handles it
|
|
||||||
}, [category.videos.length]);
|
|
||||||
|
|
||||||
// Always show both buttons
|
|
||||||
useEffect(() => {
|
|
||||||
setShowLeftButton(true);
|
|
||||||
setShowRightButton(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const stopAutoScroll = () => {
|
|
||||||
setIsScrolling(false);
|
|
||||||
if (scrollIntervalRef.current) {
|
|
||||||
clearInterval(scrollIntervalRef.current);
|
|
||||||
scrollIntervalRef.current = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -223,125 +157,50 @@ function CategoryRowOLD_BROKEN({ category, onVideoClick }: CategoryRowProps) {
|
|||||||
{category.title}
|
{category.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative px-16">
|
||||||
{/* Left scroll button - positioned at video thumbnail center */}
|
{/* Left scroll button */}
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={() => scroll('left')}
|
||||||
e.preventDefault();
|
className="absolute -left-2 top-1/2 -translate-y-1/2 z-30 bg-black/80 hover:bg-black/95 text-white border-none w-12 h-12 rounded-full opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center shadow-xl"
|
||||||
e.stopPropagation();
|
size="sm"
|
||||||
scroll('left');
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
startAutoScroll('left');
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
stopAutoScroll();
|
|
||||||
}}
|
|
||||||
className="flex absolute left-2 top-[45%] -translate-y-1/2 w-12 h-12 z-30 bg-black/80 hover:bg-black/95 rounded-full items-center justify-center transition-all duration-300 cursor-pointer border border-white/30 shadow-lg opacity-80 hover:!opacity-100"
|
|
||||||
data-testid="button-scroll-left"
|
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-6 h-6 text-white" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{/* Right scroll button - positioned at video thumbnail center */}
|
{/* Right scroll button */}
|
||||||
<button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={() => scroll('right')}
|
||||||
e.preventDefault();
|
className="absolute -right-2 top-1/2 -translate-y-1/2 z-30 bg-black/80 hover:bg-black/95 text-white border-none w-12 h-12 rounded-full opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center shadow-xl"
|
||||||
e.stopPropagation();
|
size="sm"
|
||||||
scroll('right');
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
startAutoScroll('right');
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
stopAutoScroll();
|
|
||||||
}}
|
|
||||||
className="flex absolute right-2 top-[45%] -translate-y-1/2 w-12 h-12 z-30 bg-black/80 hover:bg-black/95 rounded-full items-center justify-center transition-all duration-300 cursor-pointer border border-white/30 shadow-lg opacity-80 hover:!opacity-100"
|
|
||||||
data-testid="button-scroll-right"
|
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-6 h-6 text-white" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{/* Mobile scroll buttons */}
|
{/* Scrollable video row */}
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
scroll('left');
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
startAutoScroll('left');
|
|
||||||
}}
|
|
||||||
onTouchEnd={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
stopAutoScroll();
|
|
||||||
}}
|
|
||||||
className="md:hidden absolute left-1 top-[45%] -translate-y-1/2 w-10 h-10 z-40 bg-black/80 rounded-full flex items-center justify-center border border-white/30 shadow-lg"
|
|
||||||
data-testid="button-mobile-scroll-left"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-5 h-5 text-white" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
scroll('right');
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
startAutoScroll('right');
|
|
||||||
}}
|
|
||||||
onTouchEnd={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
stopAutoScroll();
|
|
||||||
}}
|
|
||||||
className="md:hidden absolute right-1 top-[45%] -translate-y-1/2 w-10 h-10 z-40 bg-black/80 rounded-full flex items-center justify-center border border-white/30 shadow-lg"
|
|
||||||
data-testid="button-mobile-scroll-right"
|
|
||||||
>
|
|
||||||
<ChevronRight className="w-5 h-5 text-white" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Simple horizontal scroll carousel */}
|
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollRef}
|
||||||
className="overflow-x-auto scrollbar-hide"
|
className="flex space-x-4 overflow-x-auto scrollbar-hide pb-4"
|
||||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||||
>
|
>
|
||||||
<div className="flex space-x-4 pb-4 w-max">
|
{category.videos.map((video, index) => (
|
||||||
{/* ULTRA SIMPLE - just many copies */}
|
<div key={video.id} className="flex-shrink-0 w-56 md:w-80 relative group">
|
||||||
{Array.from({ length: 50 }).map((_, copyIndex) =>
|
{/* Top 10 Number overlay for first category */}
|
||||||
category.videos.map((video, videoIndex) => (
|
{category.title.includes("Top 10") && index < 10 && (
|
||||||
<div key={`${video.id}-${copyIndex}-${videoIndex}`} className="flex-shrink-0 w-28 md:w-52 relative group">
|
<div className="absolute top-2 left-2 z-20 text-white font-black text-5xl md:text-7xl drop-shadow-2xl"
|
||||||
{/* Top 10 Number overlay for first category */}
|
style={{
|
||||||
{category.title.includes("Top 10") && (
|
textShadow: '4px 4px 8px rgba(0,0,0,0.8), -2px -2px 4px rgba(0,0,0,0.6)',
|
||||||
<div className="absolute top-1 left-1 z-30 text-white font-black text-3xl md:text-5xl drop-shadow-2xl pointer-events-none"
|
WebkitTextStroke: '2px rgba(0,0,0,0.8)'
|
||||||
style={{
|
}}>
|
||||||
textShadow: '4px 4px 8px rgba(0,0,0,0.8), -2px -2px 4px rgba(0,0,0,0.6)',
|
{index + 1}
|
||||||
WebkitTextStroke: '2px rgba(0,0,0,0.8)',
|
|
||||||
transform: 'none',
|
|
||||||
transition: 'none'
|
|
||||||
}}>
|
|
||||||
{videoIndex + 1}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<VideoCard
|
|
||||||
video={video}
|
|
||||||
onClick={onVideoClick}
|
|
||||||
className="w-full hover:scale-105 hover:z-10 transition-all duration-300 group-hover:shadow-xl rounded-md overflow-hidden"
|
|
||||||
/>
|
|
||||||
{/* Simple debug number */}
|
|
||||||
<div className="absolute bottom-0 right-0 bg-green-500 text-white text-xs px-1 rounded">
|
|
||||||
{videoIndex + 1}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
)}
|
||||||
).flat()}
|
<VideoCard
|
||||||
</div>
|
video={video}
|
||||||
|
onClick={onVideoClick}
|
||||||
|
className="w-full hover:scale-110 hover:z-10 transition-all duration-500 group-hover:shadow-2xl rounded-lg overflow-hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -59,7 +59,7 @@ export default function SearchHeader({
|
|||||||
<div className="w-9 h-9 gradient-primary rounded-lg flex items-center justify-center shadow-lg">
|
<div className="w-9 h-9 gradient-primary rounded-lg flex items-center justify-center shadow-lg">
|
||||||
<div className="w-0 h-0 border-l-[10px] border-l-white border-y-[7px] border-y-transparent ml-1"></div>
|
<div className="w-0 h-0 border-l-[10px] border-l-white border-y-[7px] border-y-transparent ml-1"></div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="oswald-text text-2xl text-white">go4.video</h1>
|
<h1 className="text-2xl font-bold text-white tracking-wide">go4.video</h1>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
|
||||||
import { type Video } from "@shared/schema";
|
|
||||||
import VideoCard from "./video-card";
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
||||||
|
|
||||||
interface SimpleCarouselProps {
|
|
||||||
category: {
|
|
||||||
title: string;
|
|
||||||
videos: Video[];
|
|
||||||
};
|
|
||||||
onVideoClick: (video: Video) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SimpleCarousel({ category, onVideoClick }: SimpleCarouselProps) {
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const scrollIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
const [isScrolling, setIsScrolling] = useState(false);
|
|
||||||
const [currentDirection, setCurrentDirection] = useState<'left' | 'right' | null>(null);
|
|
||||||
const [speed, setSpeed] = useState<'normal' | 'fast'>('normal');
|
|
||||||
|
|
||||||
const scroll = (direction: 'left' | 'right') => {
|
|
||||||
// If already scrolling in same direction, toggle speed
|
|
||||||
if (isScrolling && currentDirection === direction) {
|
|
||||||
const newSpeed = speed === 'normal' ? 'fast' : 'normal';
|
|
||||||
setSpeed(newSpeed);
|
|
||||||
|
|
||||||
// Restart with new speed immediately
|
|
||||||
if (scrollIntervalRef.current) {
|
|
||||||
clearInterval(scrollIntervalRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
const speedValue = newSpeed === 'fast' ? 1.8 : 0.8;
|
|
||||||
scrollIntervalRef.current = setInterval(() => {
|
|
||||||
if (!scrollContainerRef.current) return;
|
|
||||||
|
|
||||||
const scrollAmount = direction === 'right' ? speedValue : -speedValue;
|
|
||||||
scrollContainerRef.current.scrollBy({
|
|
||||||
left: scrollAmount,
|
|
||||||
behavior: 'auto'
|
|
||||||
});
|
|
||||||
}, 8);
|
|
||||||
} else {
|
|
||||||
// If not scrolling or different direction, start scrolling
|
|
||||||
startAutoScroll(direction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const startAutoScroll = (direction: 'left' | 'right') => {
|
|
||||||
if (scrollIntervalRef.current) {
|
|
||||||
clearInterval(scrollIntervalRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsScrolling(true);
|
|
||||||
setCurrentDirection(direction);
|
|
||||||
setSpeed('normal'); // Reset to normal speed when starting
|
|
||||||
|
|
||||||
const speedValue = 0.8; // Always start with normal speed
|
|
||||||
scrollIntervalRef.current = setInterval(() => {
|
|
||||||
if (!scrollContainerRef.current) return;
|
|
||||||
|
|
||||||
const scrollAmount = direction === 'right' ? speedValue : -speedValue;
|
|
||||||
scrollContainerRef.current.scrollBy({
|
|
||||||
left: scrollAmount,
|
|
||||||
behavior: 'auto'
|
|
||||||
});
|
|
||||||
}, 8);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopAutoScroll = () => {
|
|
||||||
if (scrollIntervalRef.current) {
|
|
||||||
clearInterval(scrollIntervalRef.current);
|
|
||||||
scrollIntervalRef.current = null;
|
|
||||||
}
|
|
||||||
setIsScrolling(false);
|
|
||||||
setCurrentDirection(null);
|
|
||||||
setSpeed('normal');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize scroll position in the middle so we can scroll both ways
|
|
||||||
useEffect(() => {
|
|
||||||
if (scrollContainerRef.current && category.videos.length > 0) {
|
|
||||||
// Wait for content to load, then scroll to middle
|
|
||||||
setTimeout(() => {
|
|
||||||
if (scrollContainerRef.current) {
|
|
||||||
const containerWidth = scrollContainerRef.current.scrollWidth;
|
|
||||||
const viewportWidth = scrollContainerRef.current.clientWidth;
|
|
||||||
const middlePosition = (containerWidth - viewportWidth) / 2;
|
|
||||||
|
|
||||||
scrollContainerRef.current.scrollTo({
|
|
||||||
left: middlePosition,
|
|
||||||
behavior: 'auto'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [category.videos.length]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative group mb-12">
|
|
||||||
<h2 className="oswald-text text-2xl text-bunny-light mb-6 px-4">
|
|
||||||
{category.title}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{/* Container z flex wrap */}
|
|
||||||
<div className="container flex flex-wrap justify-center gap-2.5 p-2.5 md:flex-row flex-col items-center">
|
|
||||||
{category.videos.map((video, videoIndex) => (
|
|
||||||
<div key={video.id} className="relative">
|
|
||||||
{/* Top 10 Number overlay for first category */}
|
|
||||||
{category.title.includes("Top 10") && (
|
|
||||||
<div className="absolute top-1 left-1 z-30 text-white font-black text-xl md:text-2xl drop-shadow-2xl pointer-events-none"
|
|
||||||
style={{
|
|
||||||
textShadow: '4px 4px 8px rgba(0,0,0,0.8), -2px -2px 4px rgba(0,0,0,0.6)',
|
|
||||||
WebkitTextStroke: '1px rgba(0,0,0,0.8)',
|
|
||||||
}}>
|
|
||||||
{videoIndex + 1}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<VideoCard
|
|
||||||
video={video}
|
|
||||||
onClick={onVideoClick}
|
|
||||||
className="poster w-[150px] md:w-[150px] sm:w-[120px] h-auto hover:scale-105 transition-all duration-300 hover:shadow-2xl rounded-md overflow-hidden relative"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -270,7 +270,7 @@ export default function VASTPlayer({ video, onClose, vastTagUrl, enableAds = tru
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center" style={{ zIndex: 2147483647, position: 'fixed' }}>
|
<div className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50">
|
||||||
<div className="relative w-full max-w-6xl mx-4">
|
<div className="relative w-full max-w-6xl mx-4">
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { Play, Plus, ThumbsUp, ChevronDown } from "lucide-react";
|
import { Play } from "lucide-react";
|
||||||
import { type Video } from "@shared/schema";
|
import { type Video } from "@shared/schema";
|
||||||
import HLSPreviewThumbnail from "./hls-preview-thumbnail";
|
import HLSPreviewThumbnail from "./hls-preview-thumbnail";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
// @ts-ignore
|
|
||||||
import Hls from 'hls.js';
|
|
||||||
|
|
||||||
interface VideoCardProps {
|
interface VideoCardProps {
|
||||||
video: Video;
|
video: Video;
|
||||||
@ -48,43 +46,10 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
|
|||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
const hoverTimeoutRef = useRef<NodeJS.Timeout>();
|
const hoverTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
|
||||||
const hlsRef = useRef<any>(null);
|
|
||||||
const animationFrameRef = useRef<number>();
|
|
||||||
|
|
||||||
// Handle mouse scrubbing for video preview with throttling for smoothness
|
|
||||||
const lastScrubTime = useRef(0);
|
|
||||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
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
|
// Delay preview start to avoid loading on quick mouse passes
|
||||||
// Only enable previews on desktop devices with mouse
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isMobile = window.innerWidth < 768 || 'ontouchstart' in window;
|
if (isHovered) {
|
||||||
|
|
||||||
if (isHovered && !isMobile) {
|
|
||||||
hoverTimeoutRef.current = setTimeout(() => {
|
hoverTimeoutRef.current = setTimeout(() => {
|
||||||
setShowPreview(true);
|
setShowPreview(true);
|
||||||
}, 800); // Start preview after 800ms hover
|
}, 800); // Start preview after 800ms hover
|
||||||
@ -102,89 +67,23 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
|
|||||||
};
|
};
|
||||||
}, [isHovered]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid={`card-video-${video.id}`}
|
data-testid={`card-video-${video.id}`}
|
||||||
className={`video-card transition-all duration-300 ease-out hover:scale-110 ${className}`}
|
className={`video-card transition-transform duration-200 hover:scale-[1.02] p-3 ${className}`}
|
||||||
style={{
|
|
||||||
transformStyle: 'preserve-3d',
|
|
||||||
transition: 'transform 0.3s ease',
|
|
||||||
willChange: 'transform',
|
|
||||||
zIndex: isHovered ? 2147483647 : 1,
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
{/* Video preview container */}
|
{/* Video preview container */}
|
||||||
<div
|
<div
|
||||||
className="relative overflow-hidden cursor-pointer group aspect-[16/9]"
|
className="relative gradient-card rounded-xl overflow-hidden mb-4 aspect-[9/16] md:aspect-[16/9] cursor-pointer group"
|
||||||
onClick={() => onClick?.(video)}
|
onClick={() => onClick?.(video)}
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
>
|
>
|
||||||
{/* Static thumbnail - always visible */}
|
{/* Static thumbnail - always visible */}
|
||||||
<img
|
<img
|
||||||
src={video.thumbnailUrl}
|
src={video.thumbnailUrl}
|
||||||
alt={video.title}
|
alt={video.title}
|
||||||
className={`w-full h-full object-cover transition-all duration-500 ease-out ${showPreview ? 'opacity-0' : 'opacity-100 group-hover:scale-105'}`}
|
className={`w-full h-full object-cover transition-all duration-300 ${showPreview ? 'opacity-0' : 'opacity-100 group-hover:scale-105'}`}
|
||||||
style={{
|
style={{
|
||||||
objectPosition: video.faceCenterPosition || 'center center',
|
objectPosition: video.faceCenterPosition || 'center center',
|
||||||
objectFit: 'cover'
|
objectFit: 'cover'
|
||||||
@ -212,34 +111,63 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
|
|||||||
{showPreview && (
|
{showPreview && (
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
style={{
|
style={{
|
||||||
objectPosition: video.faceCenterPosition || 'center center',
|
objectPosition: video.faceCenterPosition || 'center center',
|
||||||
objectFit: 'cover'
|
objectFit: 'cover'
|
||||||
}}
|
}}
|
||||||
muted={false} // Enable audio for preview
|
autoPlay
|
||||||
|
muted
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
preload="none"
|
preload="none"
|
||||||
/>
|
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 && (
|
||||||
|
<source src={video.mp4Url} type="video/mp4" />
|
||||||
|
)}
|
||||||
|
{/* Fallback to HLS if MP4 fails */}
|
||||||
|
{video.hlsUrl && (
|
||||||
|
<source src={video.hlsUrl} type="application/x-mpegURL" />
|
||||||
|
)}
|
||||||
|
</video>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Title overlay at bottom */}
|
{/* Duration badge */}
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/50 to-transparent p-3">
|
<div className="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded z-10">
|
||||||
<h3
|
{formatDuration(video.duration)}
|
||||||
className="oswald-text text-white text-sm md:text-base line-clamp-2 hover:text-bunny-blue transition-colors duration-300 cursor-pointer"
|
|
||||||
onClick={() => onClick?.(video)}
|
|
||||||
data-testid={`text-title-${video.id}`}
|
|
||||||
>
|
|
||||||
{video.title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Play button overlay - hidden during preview */}
|
||||||
|
{!showPreview && (
|
||||||
|
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-all duration-300 flex items-center justify-center">
|
||||||
|
<div className="bg-white/20 backdrop-blur-sm rounded-full w-16 h-16 flex items-center justify-center group-hover:bg-white/30 group-hover:scale-110 transition-all duration-300">
|
||||||
|
<div className="w-0 h-0 border-l-[12px] border-l-white border-y-[8px] border-y-transparent ml-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3
|
||||||
|
className="font-semibold line-clamp-2 hover:text-bunny-blue transition-colors text-bunny-light cursor-pointer"
|
||||||
|
onClick={() => onClick?.(video)}
|
||||||
|
data-testid={`text-title-${video.id}`}
|
||||||
|
>
|
||||||
|
{video.title}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center space-x-3 text-sm text-bunny-muted">
|
||||||
|
<span data-testid={`text-views-${video.id}`}>
|
||||||
|
{formatViews(video.views)}
|
||||||
|
</span>
|
||||||
|
<span data-testid={`text-date-${video.id}`}>
|
||||||
|
{formatDate(video.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -437,8 +437,7 @@ export default function VideoModal({ video, isOpen, onClose, enableAds = true }:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="modal-overlay fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center p-4"
|
className="fixed inset-0 bg-black/90 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||||
style={{ zIndex: 2147483647, position: 'fixed' }}
|
|
||||||
onClick={handleBackdropClick}
|
onClick={handleBackdropClick}
|
||||||
data-testid="modal-video"
|
data-testid="modal-video"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -2,32 +2,6 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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 {
|
:root {
|
||||||
--background: hsl(222, 84%, 4.9%);
|
--background: hsl(222, 84%, 4.9%);
|
||||||
--foreground: hsl(210, 40%, 98%);
|
--foreground: hsl(210, 40%, 98%);
|
||||||
@ -189,27 +163,6 @@
|
|||||||
.animation-delay-150 {
|
.animation-delay-150 {
|
||||||
animation-delay: 150ms;
|
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 */
|
/* Video edit modal styles */
|
||||||
@ -381,81 +334,3 @@ input[data-testid*="search"]::placeholder {
|
|||||||
opacity: 0.08;
|
opacity: 0.08;
|
||||||
filter: blur(0.5px);
|
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;
|
|
||||||
}
|
|
||||||
@ -252,7 +252,7 @@ export default function VideoPage() {
|
|||||||
className="absolute inset-0 w-full h-full"
|
className="absolute inset-0 w-full h-full"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
onLoad={handleVideoPlay}
|
onLoad={handleVideoPlay}
|
||||||
title={currentVideo.title}
|
title={currentVideo.title}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -78,7 +78,7 @@ export default function Home() {
|
|||||||
currentView={viewMode}
|
currentView={viewMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 relative overflow-visible netflix-grid-container">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 relative">
|
||||||
|
|
||||||
|
|
||||||
{/* Trikotniki na robovih - ne prekrivajo video kartic */}
|
{/* Trikotniki na robovih - ne prekrivajo video kartic */}
|
||||||
|
|||||||
@ -73,11 +73,9 @@ export class BunnyService {
|
|||||||
|
|
||||||
private bunnyVideoToVideo(bunnyVideo: BunnyVideo | BunnyVideoDetails): Video {
|
private bunnyVideoToVideo(bunnyVideo: BunnyVideo | BunnyVideoDetails): Video {
|
||||||
// Generate optimized thumbnail URL from Bunny CDN with WebP format for better performance
|
// 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
|
const thumbnailUrl = bunnyVideo.thumbnailFileName
|
||||||
? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}?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&t=${timestamp}`;
|
: `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg?width=400&height=225&format=webp`;
|
||||||
|
|
||||||
// Generate signed HLS URL for private video access
|
// Generate signed HLS URL for private video access
|
||||||
const hlsUrl = this.generateSignedUrl(bunnyVideo.guid);
|
const hlsUrl = this.generateSignedUrl(bunnyVideo.guid);
|
||||||
|
|||||||