A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
426 lines
12 KiB
HTML
426 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Interactive Video - BreakPilot H5P</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
background: #f5f5f5;
|
||
padding: 20px;
|
||
min-height: 100vh;
|
||
}
|
||
.container {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
padding: 30px;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
h1 {
|
||
color: #333;
|
||
margin-bottom: 12px;
|
||
font-size: 28px;
|
||
}
|
||
.description {
|
||
color: #6b7280;
|
||
margin-bottom: 24px;
|
||
font-size: 16px;
|
||
line-height: 1.6;
|
||
}
|
||
.video-wrapper {
|
||
position: relative;
|
||
background: #000;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
aspect-ratio: 16/9;
|
||
margin-bottom: 24px;
|
||
}
|
||
.video-wrapper iframe,
|
||
.video-wrapper video {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: none;
|
||
}
|
||
.interaction-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.85);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
z-index: 100;
|
||
}
|
||
.interaction-overlay.show {
|
||
display: flex;
|
||
}
|
||
.interaction-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 32px;
|
||
max-width: 600px;
|
||
width: 100%;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
}
|
||
.interaction-icon {
|
||
font-size: 48px;
|
||
text-align: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
.interaction-title {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
margin-bottom: 16px;
|
||
text-align: center;
|
||
}
|
||
.interaction-text {
|
||
color: #4b5563;
|
||
font-size: 16px;
|
||
line-height: 1.6;
|
||
margin-bottom: 24px;
|
||
}
|
||
.interaction-link {
|
||
display: block;
|
||
padding: 12px 24px;
|
||
background: #667eea;
|
||
color: white;
|
||
text-decoration: none;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
margin-bottom: 12px;
|
||
}
|
||
.interaction-link:hover {
|
||
background: #5568d3;
|
||
}
|
||
.btn {
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: all 0.2s;
|
||
width: 100%;
|
||
}
|
||
.btn-primary {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
.btn-primary:hover {
|
||
background: #5568d3;
|
||
}
|
||
.timeline {
|
||
background: #f9fafb;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin-bottom: 24px;
|
||
}
|
||
.timeline-title {
|
||
font-weight: 700;
|
||
color: #374151;
|
||
margin-bottom: 12px;
|
||
font-size: 14px;
|
||
}
|
||
.timeline-markers {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
.timeline-marker {
|
||
background: #fef3c7;
|
||
border: 2px solid #f59e0b;
|
||
border-radius: 6px;
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #92400e;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.timeline-marker:hover {
|
||
background: #fde68a;
|
||
transform: translateY(-2px);
|
||
}
|
||
.timeline-marker.completed {
|
||
background: #d1fae5;
|
||
border-color: #10b981;
|
||
color: #065f46;
|
||
}
|
||
.controls {
|
||
text-align: center;
|
||
color: #6b7280;
|
||
font-size: 14px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1 id="title"></h1>
|
||
<p class="description" id="description"></p>
|
||
|
||
<div class="video-wrapper">
|
||
<div id="videoContainer"></div>
|
||
|
||
<div class="interaction-overlay" id="interactionOverlay">
|
||
<div class="interaction-content">
|
||
<div class="interaction-icon" id="interactionIcon"></div>
|
||
<div class="interaction-title" id="interactionTitle"></div>
|
||
<div class="interaction-text" id="interactionText"></div>
|
||
<div id="interactionExtra"></div>
|
||
<button class="btn btn-primary" onclick="closeInteraction()">Weiter</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="timeline">
|
||
<div class="timeline-title">📌 Interaktive Elemente im Video:</div>
|
||
<div class="timeline-markers" id="timelineMarkers"></div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
ℹ️ Das Video pausiert automatisch bei interaktiven Elementen
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://www.youtube.com/iframe_api"></script>
|
||
<script src="https://player.vimeo.com/api/player.js"></script>
|
||
|
||
<script>
|
||
let data = null;
|
||
let player = null;
|
||
let playerType = null;
|
||
let currentTime = 0;
|
||
let completedInteractions = new Set();
|
||
let checkInterval = null;
|
||
|
||
function loadContent() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const dataParam = urlParams.get('data');
|
||
|
||
if (dataParam) {
|
||
try {
|
||
data = JSON.parse(decodeURIComponent(dataParam));
|
||
} catch (e) {
|
||
alert('Fehler beim Laden!');
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!data) {
|
||
alert('Keine Daten gefunden!');
|
||
return;
|
||
}
|
||
|
||
document.getElementById('title').textContent = data.title;
|
||
document.getElementById('description').textContent = data.description || '';
|
||
|
||
renderTimeline();
|
||
embedVideo();
|
||
}
|
||
|
||
function embedVideo() {
|
||
const container = document.getElementById('videoContainer');
|
||
const url = data.videoUrl;
|
||
|
||
// YouTube
|
||
if (url.includes('youtube.com') || url.includes('youtu.be')) {
|
||
const videoId = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&]+)/)?.[1];
|
||
if (videoId) {
|
||
playerType = 'youtube';
|
||
container.innerHTML = `<div id="ytPlayer"></div>`;
|
||
|
||
// YouTube API will be loaded via script tag
|
||
window.onYouTubeIframeAPIReady = function() {
|
||
player = new YT.Player('ytPlayer', {
|
||
videoId: videoId,
|
||
width: '100%',
|
||
height: '100%',
|
||
playerVars: {
|
||
autoplay: 0,
|
||
modestbranding: 1,
|
||
rel: 0
|
||
},
|
||
events: {
|
||
onReady: function() {
|
||
startTimeTracking();
|
||
},
|
||
onStateChange: function(event) {
|
||
if (event.data === YT.PlayerState.PLAYING) {
|
||
startTimeTracking();
|
||
}
|
||
}
|
||
}
|
||
});
|
||
};
|
||
}
|
||
}
|
||
// Vimeo
|
||
else if (url.includes('vimeo.com')) {
|
||
const videoId = url.match(/vimeo\.com\/(\d+)/)?.[1];
|
||
if (videoId) {
|
||
playerType = 'vimeo';
|
||
container.innerHTML = `<iframe id="vimeoPlayer" src="https://player.vimeo.com/video/${videoId}" allowfullscreen></iframe>`;
|
||
|
||
setTimeout(() => {
|
||
player = new Vimeo.Player('vimeoPlayer');
|
||
player.on('timeupdate', function(data) {
|
||
currentTime = data.seconds;
|
||
checkInteractions();
|
||
});
|
||
}, 500);
|
||
}
|
||
}
|
||
// Direct MP4
|
||
else if (url.endsWith('.mp4')) {
|
||
playerType = 'html5';
|
||
container.innerHTML = `<video id="html5Player" controls style="width: 100%; height: 100%;"><source src="${url}" type="video/mp4"></video>`;
|
||
|
||
const video = document.getElementById('html5Player');
|
||
video.addEventListener('timeupdate', function() {
|
||
currentTime = video.currentTime;
|
||
checkInteractions();
|
||
});
|
||
player = video;
|
||
}
|
||
}
|
||
|
||
function startTimeTracking() {
|
||
if (checkInterval) clearInterval(checkInterval);
|
||
|
||
checkInterval = setInterval(() => {
|
||
if (playerType === 'youtube' && player && player.getCurrentTime) {
|
||
currentTime = player.getCurrentTime();
|
||
checkInteractions();
|
||
}
|
||
}, 500);
|
||
}
|
||
|
||
function checkInteractions() {
|
||
data.interactions.forEach((interaction, index) => {
|
||
const interactionTime = timeToSeconds(interaction.time);
|
||
|
||
// Check if we're at this interaction time (within 1 second window)
|
||
if (Math.abs(currentTime - interactionTime) < 0.5 && !completedInteractions.has(index)) {
|
||
showInteraction(interaction, index);
|
||
}
|
||
});
|
||
}
|
||
|
||
function showInteraction(interaction, index) {
|
||
// Pause video
|
||
if (playerType === 'youtube' && player && player.pauseVideo) {
|
||
player.pauseVideo();
|
||
} else if (playerType === 'vimeo' && player && player.pause) {
|
||
player.pause();
|
||
} else if (playerType === 'html5' && player && player.pause) {
|
||
player.pause();
|
||
}
|
||
|
||
// Show overlay
|
||
const overlay = document.getElementById('interactionOverlay');
|
||
const icon = document.getElementById('interactionIcon');
|
||
const title = document.getElementById('interactionTitle');
|
||
const text = document.getElementById('interactionText');
|
||
const extra = document.getElementById('interactionExtra');
|
||
|
||
// Set icon based on type
|
||
switch (interaction.type) {
|
||
case 'question':
|
||
icon.textContent = '❓';
|
||
break;
|
||
case 'info':
|
||
icon.textContent = 'ℹ️';
|
||
break;
|
||
case 'link':
|
||
icon.textContent = '🔗';
|
||
break;
|
||
}
|
||
|
||
title.textContent = interaction.title;
|
||
text.textContent = interaction.content;
|
||
|
||
// Add link if type is link
|
||
if (interaction.type === 'link') {
|
||
extra.innerHTML = `<a href="${interaction.content}" target="_blank" class="interaction-link">🔗 Link öffnen</a>`;
|
||
} else {
|
||
extra.innerHTML = '';
|
||
}
|
||
|
||
overlay.classList.add('show');
|
||
completedInteractions.add(index);
|
||
|
||
// Update timeline
|
||
renderTimeline();
|
||
}
|
||
|
||
function closeInteraction() {
|
||
const overlay = document.getElementById('interactionOverlay');
|
||
overlay.classList.remove('show');
|
||
|
||
// Resume video
|
||
if (playerType === 'youtube' && player && player.playVideo) {
|
||
player.playVideo();
|
||
} else if (playerType === 'vimeo' && player && player.play) {
|
||
player.play();
|
||
} else if (playerType === 'html5' && player && player.play) {
|
||
player.play();
|
||
}
|
||
}
|
||
|
||
function renderTimeline() {
|
||
const container = document.getElementById('timelineMarkers');
|
||
container.innerHTML = '';
|
||
|
||
data.interactions.forEach((interaction, index) => {
|
||
const marker = document.createElement('div');
|
||
marker.className = 'timeline-marker';
|
||
if (completedInteractions.has(index)) {
|
||
marker.classList.add('completed');
|
||
}
|
||
|
||
const iconMap = {
|
||
question: '❓',
|
||
info: 'ℹ️',
|
||
link: '🔗'
|
||
};
|
||
|
||
marker.textContent = `${iconMap[interaction.type]} ${interaction.time} - ${interaction.title}`;
|
||
marker.onclick = () => seekTo(interaction.time);
|
||
|
||
container.appendChild(marker);
|
||
});
|
||
}
|
||
|
||
function seekTo(timeStr) {
|
||
const seconds = timeToSeconds(timeStr);
|
||
|
||
if (playerType === 'youtube' && player && player.seekTo) {
|
||
player.seekTo(seconds, true);
|
||
} else if (playerType === 'vimeo' && player && player.setCurrentTime) {
|
||
player.setCurrentTime(seconds);
|
||
} else if (playerType === 'html5' && player) {
|
||
player.currentTime = seconds;
|
||
}
|
||
}
|
||
|
||
function timeToSeconds(timeStr) {
|
||
const parts = timeStr.split(':');
|
||
const minutes = parseInt(parts[0]) || 0;
|
||
const seconds = parseInt(parts[1]) || 0;
|
||
return minutes * 60 + seconds;
|
||
}
|
||
|
||
loadContent();
|
||
</script>
|
||
</body>
|
||
</html>
|