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
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +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>