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:
@@ -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"
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
62
frontend/src/utils/viewportUtils.js
Normal file
62
frontend/src/utils/viewportUtils.js
Normal 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 };
|
||||
};
|
||||
Reference in New Issue
Block a user