This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/h5p-service/editors/interactive-video-editor.html
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
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>
2026-02-09 09:51:32 +01:00

442 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Video Editor - 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;
}
.container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 30px;
max-width: 1000px;
margin: 0 auto;
}
h1 {
color: #333;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 12px;
}
.info-box {
background: #f0f9ff;
border-left: 4px solid #3b82f6;
padding: 16px;
margin-bottom: 24px;
border-radius: 4px;
}
.form-group {
margin-bottom: 24px;
}
label {
display: block;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
input[type="text"], input[type="url"], textarea, select {
width: 100%;
padding: 10px 14px;
border: 2px solid #e5e7eb;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
input[type="text"]:focus, input[type="url"]:focus, textarea:focus, select:focus {
outline: none;
border-color: #667eea;
}
textarea {
resize: vertical;
min-height: 80px;
}
.video-preview {
background: #000;
border-radius: 8px;
overflow: hidden;
margin-bottom: 24px;
aspect-ratio: 16/9;
display: none;
}
.video-preview iframe {
width: 100%;
height: 100%;
}
.interactions-section {
background: #fef3c7;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.interaction-card {
background: white;
border: 2px solid #f59e0b;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.interaction-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.interaction-time {
font-weight: 700;
color: #92400e;
font-size: 16px;
}
.form-row {
display: grid;
grid-template-columns: 120px 1fr;
gap: 12px;
align-items: start;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #e5e7eb;
color: #374151;
}
.btn-secondary:hover {
background: #d1d5db;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-group {
display: flex;
gap: 12px;
margin-top: 24px;
}
.add-btn {
width: 100%;
padding: 12px;
border: 2px dashed #9ca3af;
background: transparent;
color: #6b7280;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s;
margin-top: 12px;
}
.add-btn:hover {
border-color: #f59e0b;
color: #f59e0b;
background: #fef3c7;
}
.success-message {
background: #d1fae5;
border: 2px solid #10b981;
color: #065f46;
padding: 16px;
border-radius: 6px;
margin-bottom: 20px;
display: none;
}
.time-input {
width: 120px !important;
font-family: monospace;
}
.hint {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>
<span>🎬</span>
Interactive Video Editor
</h1>
<div class="success-message" id="successMessage">
✅ Interactive Video erfolgreich gespeichert!
</div>
<div class="info-box">
<h3 style="color: #1e40af; margin-bottom: 8px; font-size: 14px;">💡 Wie funktioniert Interactive Video?</h3>
<p style="color: #475569; font-size: 14px; line-height: 1.6;">
Füge einem Video interaktive Elemente wie Fragen, Infotexte oder Links hinzu.
Das Video pausiert automatisch an den definierten Zeitpunkten und zeigt die Interaktionen an.
</p>
</div>
<div class="form-group">
<label for="title">Titel des Videos</label>
<input type="text" id="title" placeholder="z.B. Einführung in die Photosynthese">
</div>
<div class="form-group">
<label for="videoUrl">Video-URL</label>
<input type="url" id="videoUrl" placeholder="https://www.youtube.com/watch?v=... oder https://player.vimeo.com/video/...">
<div class="hint">Unterstützt: YouTube, Vimeo oder direkte MP4-Links</div>
</div>
<div class="video-preview" id="videoPreview"></div>
<div class="form-group">
<label for="description">Beschreibung (optional)</label>
<textarea id="description" placeholder="Kurze Beschreibung des Video-Inhalts..."></textarea>
</div>
<div class="interactions-section">
<h3 style="margin-bottom: 16px; color: #78350f;">⏱️ Interaktive Elemente</h3>
<div id="interactionsContainer"></div>
<button class="add-btn" onclick="addInteraction()">+ Neue Interaktion hinzufügen</button>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="saveVideo()">💾 Speichern</button>
<button class="btn btn-secondary" onclick="previewVideo()">▶️ Vorschau</button>
<button class="btn btn-secondary" onclick="window.history.back()">← Zurück</button>
</div>
</div>
<script>
let interactions = [];
function addInteraction() {
const interactionId = Date.now();
interactions.push({
id: interactionId,
time: '00:00',
type: 'question',
title: '',
content: ''
});
renderInteractions();
}
function removeInteraction(interactionId) {
if (confirm('Interaktion wirklich löschen?')) {
interactions = interactions.filter(i => i.id !== interactionId);
renderInteractions();
}
}
function updateInteraction(interactionId, field, value) {
const interaction = interactions.find(i => i.id === interactionId);
if (interaction) {
interaction[field] = value;
}
}
function renderInteractions() {
const container = document.getElementById('interactionsContainer');
container.innerHTML = '';
// Sort by time
const sortedInteractions = [...interactions].sort((a, b) => {
const aSeconds = timeToSeconds(a.time);
const bSeconds = timeToSeconds(b.time);
return aSeconds - bSeconds;
});
sortedInteractions.forEach((interaction, index) => {
const interactionEl = document.createElement('div');
interactionEl.className = 'interaction-card';
interactionEl.innerHTML = `
<div class="interaction-header">
<span class="interaction-time">⏱️ ${interaction.time}</span>
<button class="btn btn-danger" style="padding: 6px 12px; font-size: 12px;" onclick="removeInteraction(${interaction.id})">🗑️</button>
</div>
<div class="form-row" style="margin-bottom: 12px;">
<input type="text"
class="time-input"
value="${interaction.time}"
onchange="updateInteraction(${interaction.id}, 'time', this.value); renderInteractions();"
placeholder="mm:ss">
<select onchange="updateInteraction(${interaction.id}, 'type', this.value)">
<option value="question" ${interaction.type === 'question' ? 'selected' : ''}>❓ Frage</option>
<option value="info" ${interaction.type === 'info' ? 'selected' : ''}> Information</option>
<option value="link" ${interaction.type === 'link' ? 'selected' : ''}>🔗 Link</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 12px;">
<input type="text"
value="${interaction.title}"
onchange="updateInteraction(${interaction.id}, 'title', this.value)"
placeholder="Titel der Interaktion">
</div>
<div class="form-group">
<textarea
onchange="updateInteraction(${interaction.id}, 'content', this.value)"
placeholder="${getPlaceholderForType(interaction.type)}">${interaction.content}</textarea>
</div>
`;
container.appendChild(interactionEl);
});
}
function getPlaceholderForType(type) {
switch (type) {
case 'question':
return 'Frage eingeben (z.B. Was passiert bei der Photosynthese?)';
case 'info':
return 'Informationstext eingeben';
case 'link':
return 'URL eingeben (z.B. https://example.com)';
default:
return 'Inhalt eingeben';
}
}
function timeToSeconds(timeStr) {
const parts = timeStr.split(':');
const minutes = parseInt(parts[0]) || 0;
const seconds = parseInt(parts[1]) || 0;
return minutes * 60 + seconds;
}
function saveVideo() {
const title = document.getElementById('title').value;
const videoUrl = document.getElementById('videoUrl').value;
const description = document.getElementById('description').value;
if (!title) {
alert('Bitte gib einen Titel ein!');
return;
}
if (!videoUrl) {
alert('Bitte gib eine Video-URL ein!');
return;
}
if (interactions.length === 0) {
alert('Bitte füge mindestens eine Interaktion hinzu!');
return;
}
for (let i = 0; i < interactions.length; i++) {
if (!interactions[i].title) {
alert(`Interaktion ${i + 1} hat keinen Titel!`);
return;
}
if (!interactions[i].content) {
alert(`Interaktion ${i + 1} hat keinen Inhalt!`);
return;
}
}
const videoData = {
type: 'interactive-video',
title,
videoUrl,
description,
interactions: interactions.sort((a, b) => timeToSeconds(a.time) - timeToSeconds(b.time)),
created: new Date().toISOString()
};
const contentId = 'video_' + Date.now();
localStorage.setItem(contentId, JSON.stringify(videoData));
const successMsg = document.getElementById('successMessage');
successMsg.style.display = 'block';
setTimeout(() => {
successMsg.style.display = 'none';
}, 3000);
console.log('Interactive Video gespeichert:', videoData);
}
function previewVideo() {
const title = document.getElementById('title').value;
const videoUrl = document.getElementById('videoUrl').value;
if (!title || !videoUrl || interactions.length === 0) {
alert('Bitte fülle erst alle Felder aus!');
return;
}
const previewData = encodeURIComponent(JSON.stringify({
title,
videoUrl,
description: document.getElementById('description').value,
interactions: interactions.sort((a, b) => timeToSeconds(a.time) - timeToSeconds(b.time))
}));
window.open(`/h5p/player/interactive-video?data=${previewData}`, '_blank');
}
// Video preview on URL change
document.getElementById('videoUrl').addEventListener('change', function() {
const url = this.value;
const preview = document.getElementById('videoPreview');
if (!url) {
preview.style.display = 'none';
return;
}
let embedUrl = '';
// YouTube
if (url.includes('youtube.com') || url.includes('youtu.be')) {
const videoId = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&]+)/)?.[1];
if (videoId) {
embedUrl = `https://www.youtube.com/embed/${videoId}`;
}
}
// Vimeo
else if (url.includes('vimeo.com')) {
const videoId = url.match(/vimeo\.com\/(\d+)/)?.[1];
if (videoId) {
embedUrl = `https://player.vimeo.com/video/${videoId}`;
}
}
// Direct MP4
else if (url.endsWith('.mp4')) {
preview.innerHTML = `<video controls style="width: 100%; height: 100%;"><source src="${url}" type="video/mp4"></video>`;
preview.style.display = 'block';
return;
}
if (embedUrl) {
preview.innerHTML = `<iframe src="${embedUrl}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
preview.style.display = 'block';
}
});
// Initialize with 2 interactions
addInteraction();
addInteraction();
</script>
</body>
</html>