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>
442 lines
13 KiB
HTML
442 lines
13 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 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>
|