feat: Viewport lazy loading and video refresh support

- Add intersection observer hooks for viewport detection
- Lazy load iframes and control video autoplay based on visibility
- Extend refresh button to restart videos from beginning
- Show refresh button for both HTML animations and video content
- Add intelligent feedback messages for mixed media types
- Optimize performance by loading media only when in viewport
This commit is contained in:
Johannes
2025-09-14 16:27:36 +02:00
parent e37bc9e381
commit d36b9de4a6
4 changed files with 130 additions and 19 deletions

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Spin } from 'antd';
import { useVideoAutoPlay, useInViewport } from '../utils/viewportUtils';
// Hilfsfunktion für Dateigröße
export function formatFileSize(bytes) {
@@ -24,6 +25,10 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
const width = file.width || 300;
const height = file.height || 200;
// Viewport-Detection für Videos und iFrames
const { containerRef: videoContainerRef, videoRef, isInViewport: videoInViewport } = useVideoAutoPlay();
const { ref: iframeRef, isInViewport: iframeInViewport } = useInViewport();
// Wrapper für saubere Skalierung
const wrapperStyle = {
display: 'inline-block',
@@ -45,7 +50,7 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
const renderFileContent = () => {
if (file.type === 'html') {
return (
<div style={wrapperStyle}>
<div style={wrapperStyle} ref={iframeRef}>
<div style={innerStyle}>
{!loaded && (
<div style={{
@@ -64,14 +69,34 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
<Spin size="large" />
</div>
)}
<iframe
src={file.url}
title={file.name}
width={width}
height={height}
style={{ border: 'none', opacity: loaded ? 1 : 0 }}
onLoad={() => setLoaded(true)}
/>
{/* iFrame nur laden wenn im Viewport oder bereits geladen */}
{iframeInViewport && (
<iframe
src={file.url}
title={file.name}
width={width}
height={height}
style={{ border: 'none', opacity: loaded ? 1 : 0 }}
onLoad={() => setLoaded(true)}
/>
)}
{/* Placeholder wenn nicht im Viewport */}
{!iframeInViewport && (
<div style={{
width: width,
height: height,
backgroundColor: darkMode ? '#1f1f1f' : '#f5f5f5',
border: `1px solid ${darkMode ? '#404040' : '#d9d9d9'}`,
borderRadius: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: darkMode ? '#888' : '#666',
fontSize: 14
}}>
HTML Animation
</div>
)}
</div>
</div>
);
@@ -112,7 +137,7 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
}
if (file.type === 'video') {
return (
<div style={wrapperStyle}>
<div style={wrapperStyle} ref={videoContainerRef}>
<div style={innerStyle}>
{!loaded && (
<div style={{
@@ -132,11 +157,11 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
</div>
)}
<video
ref={videoRef}
src={file.url}
width={width}
height={height}
controls
autoPlay
loop
muted
preload="metadata"

View File

@@ -32,14 +32,17 @@ export const createCopyLinkHandler = (onSuccess) => async (adId, adName) => {
}
};
// iFrame Refresh Funktionalität
// iFrame und Video Refresh Funktionalität
export const createRefreshAdHandler = (onSuccess, onInfo) => (adId, adName) => {
// Finde alle iFrames innerhalb des Ad-Containers
// Finde alle iFrames und Videos innerhalb des Ad-Containers
const adContainer = document.getElementById(adId);
if (adContainer) {
const iframes = adContainer.querySelectorAll('iframe');
const videos = adContainer.querySelectorAll('video');
let refreshedCount = 0;
let restartedCount = 0;
// iFrames refreshen (mit Cache-Busting)
iframes.forEach((iframe) => {
if (iframe.src) {
// Füge einen Timestamp als URL-Parameter hinzu, um den Cache zu umgehen
@@ -50,10 +53,31 @@ export const createRefreshAdHandler = (onSuccess, onInfo) => (adId, adName) => {
}
});
if (refreshedCount > 0) {
onSuccess(`Animation${refreshedCount > 1 ? 'en' : ''} "${adName}" ${refreshedCount > 1 ? 'wurden' : 'wurde'} neu geladen!`);
// Videos neu starten
videos.forEach((video) => {
if (video.src || video.currentSrc) {
video.currentTime = 0; // Zurück zum Start
video.play().catch(err => {
// Autoplay kann durch Browser-Richtlinien blockiert werden
console.log('Video restart prevented:', err);
});
restartedCount++;
}
});
const totalCount = refreshedCount + restartedCount;
if (totalCount > 0) {
let message = '';
if (refreshedCount > 0 && restartedCount > 0) {
message = `${refreshedCount} Animation${refreshedCount > 1 ? 'en' : ''} und ${restartedCount} Video${restartedCount > 1 ? 's' : ''} in "${adName}" ${totalCount > 1 ? 'wurden' : 'wurde'} neu gestartet!`;
} else if (refreshedCount > 0) {
message = `${refreshedCount} Animation${refreshedCount > 1 ? 'en' : ''} in "${adName}" ${refreshedCount > 1 ? 'wurden' : 'wurde'} neu geladen!`;
} else if (restartedCount > 0) {
message = `${restartedCount} Video${restartedCount > 1 ? 's' : ''} in "${adName}" ${restartedCount > 1 ? 'wurden' : 'wurde'} neu gestartet!`;
}
onSuccess(message);
} else {
onInfo(`Keine Animationen in "${adName}" gefunden.`);
onInfo(`Keine Animationen oder Videos in "${adName}" gefunden.`);
}
}
};

View File

@@ -38,9 +38,9 @@ export const buildTabsFromCategories = (data, zoom, darkMode, handleCopyLink, ha
onClick={() => handleCopyLink(adId, ad.name)}
/>
</Tooltip>
{/* Refresh-Button nur bei HTML-Dateien (iFrames) anzeigen */}
{Array.isArray(ad.files) && ad.files.some(file => file.type === 'html') && (
<Tooltip title="Animationen neu laden">
{/* Refresh-Button bei HTML-Dateien (iFrames) und Videos anzeigen */}
{Array.isArray(ad.files) && ad.files.some(file => file.type === 'html' || file.type === 'video') && (
<Tooltip title="Animationen und Videos neu starten">
<Button
type="text"
shape="circle"

View File

@@ -0,0 +1,62 @@
import { useState, useEffect, useRef } from 'react';
// Custom Hook für Intersection Observer
export const useInViewport = (options = {}) => {
const [isInViewport, setIsInViewport] = useState(false);
const [hasBeenInViewport, setHasBeenInViewport] = useState(false);
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
const inViewport = entry.isIntersecting;
setIsInViewport(inViewport);
// Einmal im Viewport gewesen = für immer markiert (für Videos die einmal geladen werden sollen)
if (inViewport && !hasBeenInViewport) {
setHasBeenInViewport(true);
}
},
{
threshold: 0.5, // 50% des Elements müssen sichtbar sein
rootMargin: '50px', // 50px Vorlaufbereich
...options
}
);
observer.observe(element);
return () => {
observer.unobserve(element);
};
}, [hasBeenInViewport, options]);
return { ref, isInViewport, hasBeenInViewport };
};
// Hook speziell für Videos mit Auto-Play Kontrolle
export const useVideoAutoPlay = (options = {}) => {
const { ref, isInViewport, hasBeenInViewport } = useInViewport(options);
const videoRef = useRef(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (isInViewport) {
// Video abspielen wenn im Viewport
video.play().catch(err => {
// Autoplay kann durch Browser-Richtlinien blockiert werden
console.log('Autoplay prevented:', err);
});
} else {
// Video pausieren wenn aus dem Viewport
video.pause();
}
}, [isInViewport]);
return { containerRef: ref, videoRef, isInViewport, hasBeenInViewport };
};