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 React from 'react';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
|
import { useVideoAutoPlay, useInViewport } from '../utils/viewportUtils';
|
||||||
|
|
||||||
// Hilfsfunktion für Dateigröße
|
// Hilfsfunktion für Dateigröße
|
||||||
export function formatFileSize(bytes) {
|
export function formatFileSize(bytes) {
|
||||||
@@ -24,6 +25,10 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
|
|||||||
const width = file.width || 300;
|
const width = file.width || 300;
|
||||||
const height = file.height || 200;
|
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
|
// Wrapper für saubere Skalierung
|
||||||
const wrapperStyle = {
|
const wrapperStyle = {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
@@ -45,7 +50,7 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
|
|||||||
const renderFileContent = () => {
|
const renderFileContent = () => {
|
||||||
if (file.type === 'html') {
|
if (file.type === 'html') {
|
||||||
return (
|
return (
|
||||||
<div style={wrapperStyle}>
|
<div style={wrapperStyle} ref={iframeRef}>
|
||||||
<div style={innerStyle}>
|
<div style={innerStyle}>
|
||||||
{!loaded && (
|
{!loaded && (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -64,14 +69,34 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
|
|||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<iframe
|
{/* iFrame nur laden wenn im Viewport oder bereits geladen */}
|
||||||
src={file.url}
|
{iframeInViewport && (
|
||||||
title={file.name}
|
<iframe
|
||||||
width={width}
|
src={file.url}
|
||||||
height={height}
|
title={file.name}
|
||||||
style={{ border: 'none', opacity: loaded ? 1 : 0 }}
|
width={width}
|
||||||
onLoad={() => setLoaded(true)}
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -112,7 +137,7 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
|
|||||||
}
|
}
|
||||||
if (file.type === 'video') {
|
if (file.type === 'video') {
|
||||||
return (
|
return (
|
||||||
<div style={wrapperStyle}>
|
<div style={wrapperStyle} ref={videoContainerRef}>
|
||||||
<div style={innerStyle}>
|
<div style={innerStyle}>
|
||||||
{!loaded && (
|
{!loaded && (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -132,11 +157,11 @@ export default function FilePreview({ file, zoom = 1, darkMode = false }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<video
|
<video
|
||||||
|
ref={videoRef}
|
||||||
src={file.url}
|
src={file.url}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
controls
|
controls
|
||||||
autoPlay
|
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
preload="metadata"
|
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) => {
|
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);
|
const adContainer = document.getElementById(adId);
|
||||||
if (adContainer) {
|
if (adContainer) {
|
||||||
const iframes = adContainer.querySelectorAll('iframe');
|
const iframes = adContainer.querySelectorAll('iframe');
|
||||||
|
const videos = adContainer.querySelectorAll('video');
|
||||||
let refreshedCount = 0;
|
let refreshedCount = 0;
|
||||||
|
let restartedCount = 0;
|
||||||
|
|
||||||
|
// iFrames refreshen (mit Cache-Busting)
|
||||||
iframes.forEach((iframe) => {
|
iframes.forEach((iframe) => {
|
||||||
if (iframe.src) {
|
if (iframe.src) {
|
||||||
// Füge einen Timestamp als URL-Parameter hinzu, um den Cache zu umgehen
|
// 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) {
|
// Videos neu starten
|
||||||
onSuccess(`Animation${refreshedCount > 1 ? 'en' : ''} "${adName}" ${refreshedCount > 1 ? 'wurden' : 'wurde'} neu geladen!`);
|
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 {
|
} 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)}
|
onClick={() => handleCopyLink(adId, ad.name)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/* Refresh-Button nur bei HTML-Dateien (iFrames) anzeigen */}
|
{/* Refresh-Button bei HTML-Dateien (iFrames) und Videos anzeigen */}
|
||||||
{Array.isArray(ad.files) && ad.files.some(file => file.type === 'html') && (
|
{Array.isArray(ad.files) && ad.files.some(file => file.type === 'html' || file.type === 'video') && (
|
||||||
<Tooltip title="Animationen neu laden">
|
<Tooltip title="Animationen und Videos neu starten">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
shape="circle"
|
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