From ef41a6de263dd1b3dccf080ec30234c2af4f6311 Mon Sep 17 00:00:00 2001 From: Thomas Skerbis Date: Wed, 29 Jul 2026 16:45:31 +0200 Subject: [PATCH 1/5] 4.1.0 --- CHANGELOG.md | 18 + README.md | 9 +- assets/css/style.css | 588 +++++++++++++++++++++----------- assets/js/script.js | 188 +++++++--- boot.php | 19 +- lang/de_de.lang | 16 + lang/en_gb.lang | 16 + package.yml | 2 +- pages/mediapool.ffmpeg.main.php | 139 +++++--- 9 files changed, 687 insertions(+), 308 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6eea1ce --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +## 4.1.0 - 2026-07-29 + +### Neu +- Video-Konverter-Oberfläche als tabellarisches Desktop-Layout mit klaren Spalten (Datei, Informationen, Aktionen, Status). +- Konvertierung direkt am jeweiligen Dateieintrag über "Dieses Video konvertieren". +- Vorschau-Modal für Original und Web-Version direkt aus der Liste. +- Dateibezogener Konvertierungsstatus mit Donut-Anzeige und optional aufklappbarem Protokoll. + +### Verbessert +- Deutlich reduzierte visuelle Unruhe durch kompaktere Abstände, konsistente Button-Gruppen und klarere Kartenstruktur. +- Statusdarstellung pro Zeile: Bereit, Konvertiert oder Laufend. +- Globaler Statusbereich als kompakter Technik-Block statt dominanter Fortschrittsleiste. + +### Technisch +- Asset-Cache-Busting fuer CSS/JS in `boot.php`. +- JavaScript auf Donut-Fortschritt und zeilenbezogene Statussteuerung umgestellt. diff --git a/README.md b/README.md index 96b4d0d..5529e70 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# REDAXO-AddOn: FFmpeg Video Tools v4.0 +# REDAXO-AddOn: FFmpeg Video Tools v4.1 Vollständige Video-Management-Lösung für REDAXO CMS – Konvertierung, Trimming und detaillierte Video-Analyse, alles in einem Addon! @@ -51,10 +51,11 @@ Vollständige Video-Management-Lösung für REDAXO CMS – Konvertierung, Trimmi ### Video-Konverter 1. **Medienpool → Video-Tools → Video-Konverter** öffnen -2. Video aus der Liste auswählen -3. "Video konvertieren" klicken +2. Gewünschtes Video in der tabellarischen Übersicht finden +3. Direkt am Eintrag auf **"Dieses Video konvertieren"** klicken 4. **Browser schließen möglich** – Konvertierung läuft weiter -5. Später "Status prüfen" → Fertig optimiertes Video im Medienpool +5. Fortschritt direkt am Eintrag verfolgen (Donut + optionales Protokoll) +6. Später "Status prüfen" → Fertig optimiertes Video im Medienpool ### Video-Trimmer 1. **Medienpool → Video-Tools → Video-Trimmer** aufrufen diff --git a/assets/css/style.css b/assets/css/style.css index edf3245..d0cade6 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1,309 +1,487 @@ -.video-item { - padding: 10px; - border-bottom: 1px solid rgba(var(--rex-color-border-text-rgb, 238, 238, 238), .2); - display: flex; - flex-direction: column; +.ffmpeg-converter-ui { + --ffmpeg-surface: #ffffff; + --ffmpeg-surface-soft: #f3f7fb; + --ffmpeg-border: #d9e2ec; + --ffmpeg-border-strong: #c5d0db; + --ffmpeg-text: #1f2d3d; + --ffmpeg-muted: #617287; + --ffmpeg-brand: #2563eb; + --ffmpeg-brand-rgb: 37, 99, 235; + --ffmpeg-success: #2f9a4e; + --ffmpeg-success-rgb: 47, 154, 78; + --ffmpeg-shadow: 0 8px 24px rgba(12, 22, 34, 0.08); + --ffmpeg-track-dark: #10161d; + --ffmpeg-log-bg: #0f1722; + --ffmpeg-log-text: #dce8f6; +} + +body.rex-theme-dark .ffmpeg-converter-ui { + --ffmpeg-surface: #1f2933; + --ffmpeg-surface-soft: #161f28; + --ffmpeg-border: #344353; + --ffmpeg-border-strong: #3f4f61; + --ffmpeg-text: #e7f0f8; + --ffmpeg-muted: #a7b8c8; + --ffmpeg-brand: #5ea3ff; + --ffmpeg-brand-rgb: 94, 163, 255; + --ffmpeg-success: #4fbe73; + --ffmpeg-success-rgb: 79, 190, 115; + --ffmpeg-shadow: 0 8px 24px rgba(0, 0, 0, 0.32); + --ffmpeg-track-dark: #0a0d12; + --ffmpeg-log-bg: #0b1118; + --ffmpeg-log-text: #d7e4f2; +} + +@media (prefers-color-scheme: dark) { + body.rex-has-theme:not(.rex-theme-light) .ffmpeg-converter-ui { + --ffmpeg-surface: #1f2933; + --ffmpeg-surface-soft: #161f28; + --ffmpeg-border: #344353; + --ffmpeg-border-strong: #3f4f61; + --ffmpeg-text: #e7f0f8; + --ffmpeg-muted: #a7b8c8; + --ffmpeg-brand: #5ea3ff; + --ffmpeg-brand-rgb: 94, 163, 255; + --ffmpeg-success: #4fbe73; + --ffmpeg-success-rgb: 79, 190, 115; + --ffmpeg-shadow: 0 8px 24px rgba(0, 0, 0, 0.32); + --ffmpeg-track-dark: #0a0d12; + --ffmpeg-log-bg: #0b1118; + --ffmpeg-log-text: #d7e4f2; + } +} + +.ffmpeg-converter-ui .ffmpeg-intro-card { + padding: 14px 16px; + margin: 0 0 14px; + border: 1px solid var(--ffmpeg-border); + border-radius: 12px; + background: linear-gradient(120deg, var(--ffmpeg-surface-soft), var(--ffmpeg-surface)); + color: var(--ffmpeg-text); + font-size: 14px; + line-height: 1.45; +} + +.ffmpeg-converter-ui .ffmpeg-video-list { + margin: 0; +} + +.ffmpeg-converter-ui .ffmpeg-video-items { + width: 100%; +} + +.ffmpeg-converter-ui .ffmpeg-list-head { + display: none; +} + +.ffmpeg-converter-ui .ffmpeg-video-list > legend { + margin-bottom: 14px; + border-bottom: 0; + color: var(--ffmpeg-text); + font-size: 18px; + font-weight: 700; +} + +.ffmpeg-converter-ui .video-item { margin-bottom: 10px; - transition: background-color 0.2s ease; + border: 1px solid var(--ffmpeg-border); + border-radius: 12px; + background: var(--ffmpeg-surface); + box-shadow: 0 4px 14px rgba(12, 22, 34, 0.06); + overflow: hidden; + transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease; +} + +.ffmpeg-converter-ui .video-item:hover { + transform: translateY(-1px); + border-color: var(--ffmpeg-border-strong); } -.video-meta { +.ffmpeg-converter-ui .video-head { display: flex; - color: var(--rex-color-text-muted, #888); - font-size: 0.85em; - margin-top: 5px; - margin-left: 20px; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 12px 14px 8px; +} + +.ffmpeg-converter-ui .video-filename { + color: var(--ffmpeg-text); + font-size: 15px; + line-height: 1.3; + letter-spacing: 0.1px; } -.video-size, .video-date { - margin-right: 15px; +.ffmpeg-converter-ui .video-title { + padding: 0 14px; + color: var(--ffmpeg-muted); + font-size: 12px; + margin-top: -2px; } -.optimized { - background-color: rgba(var(--rex-color-background-rgb, 248, 248, 248), .1); - border-radius: 5px; - padding: 12px; +.ffmpeg-converter-ui .video-meta { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + padding: 7px 14px 10px; + color: var(--ffmpeg-muted); + font-size: 12px; } -.processing { - background-color: rgba(var(--rex-color-background-rgb, 240, 247, 255), .15); - border-radius: 5px; +.ffmpeg-converter-ui .video-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 0 14px 12px; } -.already-converted { - /* Previously used opacity to indicate converted items — this reduced text contrast. - Use a subtle background color instead for clarity and accessibility. */ - background-color: rgba(var(--rex-color-success-rgb, 92, 184, 92), .08); - border-radius: 5px; +.ffmpeg-converter-ui .video-actions-group { + display: inline-flex; + flex-wrap: wrap; + gap: 6px; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; } -.compression-rate { +.ffmpeg-converter-ui .video-actions-group-primary { + margin-right: 6px; +} + +.ffmpeg-converter-ui .video-actions .btn { + margin: 0; + border-radius: 8px; + font-weight: 600; +} + +.ffmpeg-converter-ui .video-actions .btn.btn-xs { + padding: 6px 10px; +} + +.ffmpeg-converter-ui .ffmpeg-start-conversion { + min-width: 200px; +} + +.ffmpeg-converter-ui .conversion-badge { + margin-left: 0; +} + +.ffmpeg-converter-ui .compression-rate { color: #fff; - background-color: #5cb85c; - margin-left: 10px; + background-color: var(--ffmpeg-success); } -.conversion-badge { - margin-left: 10px; - display: inline-block; +.ffmpeg-converter-ui .video-item.processing { + border-color: rgba(var(--ffmpeg-brand-rgb), 0.45); + box-shadow: 0 0 0 2px rgba(var(--ffmpeg-brand-rgb), 0.18); } -.video-actions { - margin-top: 10px; - margin-left: 20px; +.ffmpeg-converter-ui .video-item.already-converted { + border-color: rgba(var(--ffmpeg-success-rgb), 0.45); + background: linear-gradient(180deg, rgba(var(--ffmpeg-success-rgb), 0.06), var(--ffmpeg-surface)); } -/* Coole Statusanzeige */ -.conversion-status { - padding: 15px; - position: relative; +.ffmpeg-converter-ui .video-inline-status { + margin: 0 14px 12px; + padding: 10px; + border: 1px solid var(--ffmpeg-border); + border-radius: 10px; + background: linear-gradient(120deg, rgba(var(--ffmpeg-brand-rgb), 0.08), rgba(var(--ffmpeg-success-rgb), 0.08)); } -.progress { - height: 20px; - margin-bottom: 0; - background-color: rgba(var(--rex-color-border-text-rgb, 233, 236, 239), .2); +.ffmpeg-converter-ui .video-inline-status-static { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 700; + padding: 5px 10px; + border-radius: 999px; + border: 1px solid transparent; +} + +.ffmpeg-converter-ui .video-inline-status-static.is-ready { + color: #6b7280; + background: rgba(107, 114, 128, 0.12); + border-color: rgba(107, 114, 128, 0.2); } -.progress-bar { - background-color: var(--rex-color-brand, #3498db); +.ffmpeg-converter-ui .video-inline-status-static.is-converted { + color: var(--ffmpeg-success); + background: rgba(var(--ffmpeg-success-rgb), 0.12); + border-color: rgba(var(--ffmpeg-success-rgb), 0.25); } -.conversion-details { +.ffmpeg-converter-ui .video-inline-main { display: flex; - justify-content: center; align-items: center; - margin-top: 10px; + gap: 12px; } -#progress-text { - font-size: 16px; - font-weight: bold; - margin-left: 10px; - color: var(--rex-color-text, inherit); +.ffmpeg-converter-ui .video-inline-donut { + --progress: 0; + width: 72px; + height: 72px; + border-radius: 50%; + background: conic-gradient(var(--ffmpeg-success) calc(var(--progress) * 1%), var(--ffmpeg-track-dark) 0); + position: relative; + display: grid; + place-items: center; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); + flex: 0 0 auto; } -/* Spinner Animation */ -.spinner { - margin-right: 10px; - width: 70px; - text-align: center; +.ffmpeg-converter-ui .video-inline-donut::before { + content: ''; + position: absolute; + inset: 8px; + border-radius: 50%; + background: var(--ffmpeg-surface); } -.spinner > div { - width: 12px; - height: 12px; - background-color: var(--rex-color-brand, #3498db); - border-radius: 100%; - display: inline-block; - animation: sk-bouncedelay 1.4s infinite ease-in-out both; +.ffmpeg-converter-ui .video-inline-donut-value { + position: relative; + z-index: 1; + font-size: 13px; + font-weight: 700; + color: var(--ffmpeg-text); } -.spinner .bounce1 { - animation-delay: -0.32s; +.ffmpeg-converter-ui .video-inline-status-meta { + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; } -.spinner .bounce2 { - animation-delay: -0.16s; +.ffmpeg-converter-ui .video-inline-status-text { + font-size: 14px; + font-weight: 600; + color: var(--ffmpeg-text); } -@keyframes sk-bouncedelay { - 0%, 80%, 100% { - transform: scale(0); - } 40% { - transform: scale(1.0); - } +.ffmpeg-converter-ui .ffmpeg-toggle-inline-log { + padding: 0; + font-size: 12px; } -/* Hervorhebung aktiver Videos */ -.active-video { - background-color: rgba(var(--rex-color-brand-rgb, 52, 152, 219), .1); - border-left: 4px solid var(--rex-color-brand, #3498db); - padding-left: 8px; +.ffmpeg-converter-ui .video-inline-log { + margin-top: 10px; } -/* Log-Bereich */ -#log pre { - background-color: var(--rex-color-panel-bg, #fff); - color: var(--rex-color-text, #212529); - border: 1px solid rgba(var(--rex-color-border-text-rgb, 221, 221, 221), .2); - height: 200px; +.ffmpeg-converter-ui .video-inline-log pre { + margin: 0; + max-height: 220px; overflow-y: auto; + border: 1px solid rgba(var(--ffmpeg-brand-rgb), 0.25); + border-radius: 8px; padding: 10px; - font-family: monospace; - font-size: 0.9em; + background: var(--ffmpeg-log-bg); + color: var(--ffmpeg-log-text); + font-family: Menlo, Monaco, Consolas, 'Liberation Mono', monospace; + font-size: 12px; white-space: pre-wrap; } -/* Speziell für den REDAXO Dark Mode */ -.rex-theme-dark .video-item { - border-bottom-color: rgba(255, 255, 255, 0.1); +.ffmpeg-converter-ui .ffmpeg-global-status-panel { + border-color: var(--ffmpeg-border); } -.rex-theme-dark .optimized { - background-color: rgba(255, 255, 255, 0.05); +.ffmpeg-converter-ui .ffmpeg-global-status { + display: flex; + align-items: center; + gap: 14px; } -.rex-theme-dark .processing { - background-color: rgba(53, 152, 219, 0.15); +.ffmpeg-converter-ui .video-inline-donut-global { + width: 58px; + height: 58px; } -.rex-theme-dark .already-converted { - background-color: rgba(92, 184, 92, 0.1); +.ffmpeg-converter-ui .video-inline-donut-global::before { + inset: 7px; } -.rex-theme-dark .active-video { - background-color: rgba(53, 152, 219, 0.15); +.ffmpeg-converter-ui .video-inline-donut-global .video-inline-donut-value { + font-size: 11px; } -.rex-theme-dark #log pre { - background-color: rgba(0, 0, 0, 0.2); - border-color: rgba(255, 255, 255, 0.1); +.ffmpeg-converter-ui .conversion-status { + padding: 12px 14px; } -.rex-theme-dark .progress { - background-color: rgba(255, 255, 255, 0.1); +.ffmpeg-converter-ui .conversion-details { + display: flex; + align-items: center; + gap: 10px; + margin-top: 0; } -/* Bessere Kontrastfarben für Text */ -.text-success { - color: var(--rex-color-success, #2ecc71) !important; +.ffmpeg-converter-ui #progress-text { + margin-left: 0; + color: var(--ffmpeg-text); + font-size: 14px; + font-weight: 600; } -/* Hover-Effekte */ -.video-item:hover { - background-color: rgba(var(--rex-color-brand-rgb, 52, 152, 219), .05); +.ffmpeg-converter-ui .global-spinner { + margin-right: 0; + width: auto; } -.rex-theme-dark .video-item:hover { - background-color: rgba(255, 255, 255, 0.05); +.ffmpeg-converter-ui .spinner > div { + width: 9px; + height: 9px; + border-radius: 100%; + background-color: var(--ffmpeg-brand); + display: inline-block; + animation: sk-bouncedelay 1.4s infinite ease-in-out both; } -/* Verbesserte Buttons im Dark Mode */ - - -/* Smooth Transitions */ -.video-item, .progress-bar, .spinner > div, .btn { - transition: all 0.3s ease; +.ffmpeg-converter-ui .spinner .bounce1 { + animation-delay: -0.32s; } -/* CSS-Variablen für Light- und Dark-Mode */ -:root { - --color-text: #333; - --color-text-muted: #666; - --color-background: #f8f8f8; - --color-background-rgb: 248, 248, 248; - --color-border: #ddd; - --color-border-rgb: 221, 221, 221; - --color-brand: #3498db; - --color-brand-rgb: 52, 152, 219; - --color-success: #5cb85c; - --color-success-rgb: 92, 184, 92; - --color-panel-bg: #fff; +.ffmpeg-converter-ui .spinner .bounce2 { + animation-delay: -0.16s; } -/* Prefers Dark Mode - System Einstellung */ -@media (prefers-color-scheme: dark) { - :root { - --color-text: #e9ecef; - --color-text-muted: #adb5bd; - --color-background: #212529; - --color-background-rgb: 33, 37, 41; - --color-border: #495057; - --color-border-rgb: 73, 80, 87; - --color-brand: #3498db; - --color-brand-rgb: 52, 152, 219; - --color-success: #2ecc71; - --color-success-rgb: 46, 204, 113; - --color-panel-bg: #343a40; +@keyframes sk-bouncedelay { + 0%, + 80%, + 100% { + transform: scale(0); } - /* Nur anwenden, wenn kein expliziter REDAXO Dark-Mode aktiviert ist */ - body:not(.rex-theme-dark) .video-item { - border-bottom-color: rgba(255, 255, 255, 0.1); + 40% { + transform: scale(1); } +} - body:not(.rex-theme-dark) .optimized { - background-color: rgba(255, 255, 255, 0.05); - } +.ffmpeg-preview-video { + width: 100%; + max-height: 70vh; + background: #000; + border-radius: 8px; +} + +.ffmpeg-preview-player-wrapper { + display: flex; + justify-content: center; + align-items: center; + min-height: 240px; +} + +.ffmpeg-converter-ui .text-success { + color: var(--ffmpeg-success) !important; +} - body:not(.rex-theme-dark) .processing { - background-color: rgba(53, 152, 219, 0.15); +@media (min-width: 992px) { + .ffmpeg-converter-ui .ffmpeg-list-head { + display: grid; + grid-template-columns: 2fr 1.2fr 2fr 1.2fr; + gap: 12px; + margin: 0 0 8px; + padding: 0 14px; + color: var(--ffmpeg-muted); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; } - body:not(.rex-theme-dark) .already-converted { - background-color: rgba(92, 184, 92, 0.1); + .ffmpeg-converter-ui .video-item { + display: grid; + grid-template-columns: 2fr 1.2fr 2fr 1.2fr; + gap: 12px; + align-items: center; + padding: 12px 14px; } - body:not(.rex-theme-dark) .active-video { - background-color: rgba(53, 152, 219, 0.15); + .ffmpeg-converter-ui .video-head { + grid-column: 1; + padding: 0; } - body:not(.rex-theme-dark) #log pre { - background-color: rgba(0, 0, 0, 0.2); - border-color: rgba(255, 255, 255, 0.1); + .ffmpeg-converter-ui .video-title { + grid-column: 1; + padding: 0; + margin-top: 4px; } - body:not(.rex-theme-dark) .progress { - background-color: rgba(255, 255, 255, 0.1); + .ffmpeg-converter-ui .video-meta { + grid-column: 2; + padding: 0; + display: grid; + gap: 6px; + align-content: center; } - body:not(.rex-theme-dark) .video-item:hover { - background-color: rgba(255, 255, 255, 0.05); + .ffmpeg-converter-ui .video-actions { + grid-column: 3; + padding: 0; } -} -/* Klassen, die die CSS-Variablen nutzen */ -.video-item { - border-bottom-color: rgba(var(--color-border-rgb, 238, 238, 238), .2); -} + .ffmpeg-converter-ui .video-inline-status { + grid-column: 4; + margin: 0; + padding: 8px; + min-height: 60px; + display: flex; + align-items: center; + } -.video-meta { - color: var(--color-text-muted, #888); -} + .ffmpeg-converter-ui .video-inline-main { + width: 100%; + } -.optimized { - background-color: rgba(var(--color-background-rgb, 248, 248, 248), .3); - border: 1px solid rgba(var(--color-border-rgb, 221, 221, 221), .3); -} + .ffmpeg-converter-ui .video-inline-donut { + width: 54px; + height: 54px; + } -.processing { - background-color: rgba(var(--color-brand-rgb, 52, 152, 219), .1); - border: 1px solid rgba(var(--color-brand-rgb, 52, 152, 219), .2); -} + .ffmpeg-converter-ui .video-inline-donut::before { + inset: 7px; + } -.already-converted { - background-color: rgba(var(--color-success-rgb, 92, 184, 92), .1); + .ffmpeg-converter-ui .video-inline-donut-value { + font-size: 11px; + } } -.progress { - background-color: rgba(var(--color-border-rgb, 233, 236, 239), .2); -} +@media (max-width: 767px) { + .ffmpeg-converter-ui .video-head { + padding: 10px 10px 7px; + } -.progress-bar, .spinner > div { - background-color: var(--color-brand, #3498db); -} + .ffmpeg-converter-ui .video-title, + .ffmpeg-converter-ui .video-meta, + .ffmpeg-converter-ui .video-actions { + padding-left: 10px; + padding-right: 10px; + } -#progress-text { - color: var(--color-text, inherit); -} + .ffmpeg-converter-ui .video-inline-status { + margin-left: 10px; + margin-right: 10px; + } -.active-video { - background-color: rgba(var(--color-brand-rgb, 52, 152, 219), .1); - border-left-color: var(--color-brand, #3498db); -} + .ffmpeg-converter-ui .video-inline-main { + align-items: flex-start; + } -#log pre { - background-color: var(--color-panel-bg, #fff); - color: var(--color-text, #212529); - border-color: rgba(var(--color-border-rgb, 221, 221, 221), .2); -} + .ffmpeg-converter-ui .video-actions-group { + width: 100%; + } -.text-success { - color: var(--color-success, #2ecc71) !important; -} + .ffmpeg-converter-ui .video-actions .btn { + flex: 1 1 auto; + } -.video-item:hover { - background-color: rgba(var(--color-brand-rgb, 52, 152, 219), .08); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + .ffmpeg-converter-ui .video-inline-donut { + width: 64px; + height: 64px; + } } diff --git a/assets/js/script.js b/assets/js/script.js index 951fd3e..f69999f 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -16,6 +16,76 @@ $('#log pre').scrollTop($('#log pre')[0].scrollHeight); } + function getCurrentVideoItem() { + return $('.video-item[data-video-item="' + currentVideoName + '"]').first(); + } + + function activateInlineStatus(videoName) { + if (!videoName) { + return; + } + + currentVideoName = videoName; + $('.video-inline-status').hide(); + $('.video-item').removeClass('has-inline-status'); + + var $item = getCurrentVideoItem(); + if ($item.length) { + $item.addClass('has-inline-status'); + $item.find('.video-inline-status').show(); + $item.find('.video-inline-main').css('display', 'flex'); + $item.find('.video-inline-status-static').hide(); + $item.find('.video-inline-status-text').text($item.find('.video-inline-status').data('runningLabel') || 'Läuft'); + } + } + + function updateInlineProgress(percent, label) { + var $item = getCurrentVideoItem(); + if (!$item.length) { + return; + } + + var valueText = Math.max(0, Math.min(100, parseInt(percent, 10) || 0)) + '%'; + $item.find('.video-inline-donut').css('--progress', String(percent)); + $item.find('.video-inline-donut-value').text(valueText); + $item.find('.video-inline-status-text').removeClass('text-success').text(label || valueText); + } + + function updateInlineLog(logText) { + var $item = getCurrentVideoItem(); + if (!$item.length) { + return; + } + + $item.find('.video-inline-log pre').text(logText || ''); + } + + function markInlineCompleted() { + var $item = getCurrentVideoItem(); + if (!$item.length) { + return; + } + + $item.find('.video-inline-donut').css('--progress', '100'); + $item.find('.video-inline-donut-value').text('100%'); + $item.find('.video-inline-status-text') + .addClass('text-success') + .html(' Fertig!'); + $item.find('.video-inline-status-static').hide(); + } + + function updateGlobalProgress(percent, label) { + var value = Math.max(0, Math.min(100, parseInt(percent, 10) || 0)); + $('#global-progress-donut').css('--progress', String(value)); + $('#global-progress-value').text(value + '%'); + + if (label) { + $('#progress-text').html(label); + } else { + $('#progress-text').html(value + '%'); + } + } + function showProgress() { $.ajax({ type: 'get', @@ -25,7 +95,7 @@ .fail(function (jqXHR, textStatus) { console.log("Request failed: " + textStatus); $('#log pre').html("Request failed: " + textStatus); - $('#start').removeClass('disabled').prop('disabled', false); + $('.ffmpeg-start-conversion').removeClass('disabled').prop('disabled', false); clearInterval(doProgress); conversionActive = false; updateUIForConversion(false); @@ -33,7 +103,7 @@ .done(function (data) { if (data.error) { $('#log pre').html("Error: " + data.error); - $('#start').removeClass('disabled').prop('disabled', false); + $('.ffmpeg-start-conversion').removeClass('disabled').prop('disabled', false); clearInterval(doProgress); conversionActive = false; updateUIForConversion(false); @@ -43,6 +113,7 @@ // Log aktualisieren $('#log pre').html(data.log); scrolllog(); + updateInlineLog(data.log || ''); // Prüfen, ob das Log auf einen Import-Abschluss hinweist var logText = data.log || ''; @@ -58,8 +129,8 @@ showCompleted(); } else if (data.status === 'importing') { // Import läuft noch - $('#prog').css('width', '99%'); - $('#progress-text').html('Importiere...'); + updateGlobalProgress(99, 'Importiere...'); + updateInlineProgress(99, 'Importiere...'); // Wenn noch nicht gestartet, Import-Prozess starten if (!importStarted) { @@ -75,6 +146,7 @@ if (newProgress > progressValue || progressValue === 0) { progressValue = newProgress; updateProgress(progressValue); + updateInlineProgress(progressValue, progressValue + '%'); } // Prüfen auf Abschlusszeichen im Log @@ -112,13 +184,12 @@ // Fortschrittsanzeige aktualisieren function updateProgress(percent) { - $('#prog').css('width', percent + '%'); - $('#progress-text').html(percent + '%'); + updateGlobalProgress(percent); } // Konvertierung beenden (bei Fehler oder Abbruch) function stopConversion() { - $('#start').removeClass('disabled').prop('disabled', false); + $('.ffmpeg-start-conversion').removeClass('disabled').prop('disabled', false); clearInterval(doProgress); if (completionTimeout) { @@ -133,13 +204,13 @@ // Abschluss anzeigen function showCompleted() { // UI auf 100% setzen - $('#prog').css('width', '100%'); - $('#progress-text').html('100%'); - $('#start').removeClass('disabled').prop('disabled', false); + updateGlobalProgress(100); + $('.ffmpeg-start-conversion').removeClass('disabled').prop('disabled', false); // Erfolgsanimation anzeigen $('.spinner').hide(); $('#progress-text').addClass('text-success').html(' Fertig!'); + markInlineCompleted(); // Timer stoppen clearInterval(doProgress); @@ -163,16 +234,17 @@ if (active) { // UI für aktive Konvertierung $('.progress-section').show(); - $('input[name=video]').prop('disabled', true); - $('#start').addClass('disabled').prop('disabled', true); + $('.ffmpeg-start-conversion').addClass('disabled').prop('disabled', true); $('.spinner').show(); $('#progress-text').removeClass('text-success').html('0%'); + updateGlobalProgress(0); progressValue = 0; importStarted = false; + activateInlineStatus(currentVideoName); + updateInlineProgress(0, '0%'); } else { // UI für inaktive Konvertierung - $('input[name=video]').prop('disabled', false); - $('#start').removeClass('disabled').prop('disabled', false); + $('.ffmpeg-start-conversion').removeClass('disabled').prop('disabled', false); } } @@ -181,8 +253,8 @@ console.log("Starting media import process..."); // Fortschrittsanzeige auf 99% - $('#prog').css('width', '99%'); - $('#progress-text').html('Importiere...'); + updateGlobalProgress(99, 'Importiere...'); + updateInlineProgress(99, 'Importiere...'); $.ajax({ type: 'get', @@ -206,6 +278,7 @@ // Log aktualisieren $('#log pre').html(data.log); scrolllog(); + updateInlineLog(data.log || ''); // Prüfen, ob der Import erfolgreich war if (data.status === 'success' || data.log.indexOf('was successfully added to rex_mediapool') !== -1) { @@ -240,19 +313,15 @@ // UI aktualisieren $('.progress-section').show(); - $('input[name=video]').prop('disabled', true); - $('#start').addClass('disabled').prop('disabled', true); + $('.ffmpeg-start-conversion').addClass('disabled').prop('disabled', true); // Video-Name speichern if (data.info && data.info.video) { currentVideoName = data.info.video; + activateInlineStatus(currentVideoName); // Markiere das aktive Video in der Liste - $('input[name=video]').each(function() { - if ($(this).data('video') === currentVideoName) { - $(this).closest('.video-item').addClass('processing'); - } - }); + $('.video-item[data-video-item="' + currentVideoName + '"]').addClass('processing'); } if (data.status === 'converting') { @@ -260,8 +329,8 @@ SetProgressStart(); } else if (data.status === 'importing') { // Import läuft - $('#prog').css('width', '99%'); - $('#progress-text').html('Importiere...'); + updateGlobalProgress(99, 'Importiere...'); + updateInlineProgress(99, 'Importiere...'); if (!importStarted) { importStarted = true; @@ -296,18 +365,16 @@ if (data.info && data.info.log) { $('#log pre').html(data.info.log); scrolllog(); + updateInlineLog(data.info.log); } // Video-Name speichern if (data.info && data.info.video) { currentVideoName = data.info.video; + activateInlineStatus(currentVideoName); // Markiere das aktive Video in der Liste - $('input[name=video]').each(function() { - if ($(this).data('video') === currentVideoName) { - $(this).closest('.video-item').addClass('processing'); - } - }); + $('.video-item[data-video-item="' + currentVideoName + '"]').addClass('processing'); } // Starte Fortschrittsanzeige @@ -329,12 +396,10 @@ // Bei Seitenladung den Status prüfen checkStatus(); - // Start-Button - $('#start').on('click', function () { - let video = $('input[name=video]:checked').val(); - - if (video === undefined) { - alert($('.rex-addon-output').data('i18n-select-video') || 'Bitte wählen Sie ein Video zur Konvertierung aus!'); + // Konvertierung direkt am Video starten + $(document).on('click', '.ffmpeg-start-conversion', function () { + var video = $(this).data('video'); + if (!video) { return false; } @@ -361,13 +426,44 @@ return false; }); - // Video-Auswahl - $('input[name=video]').change(function() { - $('.video-item').removeClass('active-video'); - $(this).closest('.video-item').addClass('active-video'); - - // Aktuell ausgewähltes Video speichern - currentVideoName = $(this).val(); + // Video-Vorschau im Modal öffnen + $(document).on('click', '.ffmpeg-preview-link', function (event) { + event.preventDefault(); + + var mediaUrl = $(this).data('mediaUrl'); + var previewLabel = $(this).data('previewLabel') || 'Vorschau'; + var videoTitle = $(this).data('videoTitle') || ''; + + if (!mediaUrl) { + return; + } + + var title = previewLabel + (videoTitle ? ' - ' + videoTitle : ''); + $('#ffmpeg-video-preview-title').text(title); + + var videoElement = document.getElementById('ffmpeg-preview-video'); + if (videoElement) { + videoElement.pause(); + videoElement.removeAttribute('src'); + videoElement.load(); + videoElement.setAttribute('src', mediaUrl); + videoElement.load(); + } + + $('#ffmpeg-video-preview-modal').modal('show'); + }); + + // Inline-Protokoll ein-/ausklappen + $(document).on('click', '.ffmpeg-toggle-inline-log', function () { + var $button = $(this); + var $log = $button.closest('.video-inline-status').find('.video-inline-log'); + var isVisible = $log.is(':visible'); + var showLabel = $button.data('showLabel') || 'Protokoll anzeigen'; + var hideLabel = $button.data('hideLabel') || 'Protokoll ausblenden'; + + $log.toggle(!isVisible); + $button.attr('aria-expanded', !isVisible ? 'true' : 'false'); + $button.text(!isVisible ? hideLabel : showLabel); }); }); @@ -410,9 +506,11 @@ // Konvertierung gestartet $('.progress-section').show(); - $('#prog').css('width', '0%'); - $('#progress-text').html('0%'); + updateGlobalProgress(0); $('#log pre').html("Konvertierung gestartet..."); + activateInlineStatus(video); + updateInlineProgress(0, '0%'); + updateInlineLog('Konvertierung gestartet...'); // Fortschrittsüberwachung starten SetProgressStart(); diff --git a/boot.php b/boot.php index dc20e21..3fad56c 100644 --- a/boot.php +++ b/boot.php @@ -16,8 +16,23 @@ if (rex::isBackend() && rex::getUser()) { // Add JavaScript to ffmpeg page if (rex_be_controller::getCurrentPagePart(2) == 'ffmpeg') { - rex_view::addJsFile(rex_addon::get('ffmpeg')->getAssetsUrl('js/script.js')); - rex_view::addCssFile(rex_addon::get('ffmpeg')->getAssetsUrl('css/style.css')); + $addon = rex_addon::get('ffmpeg'); + + $jsUrl = $addon->getAssetsUrl('js/script.js'); + $cssUrl = $addon->getAssetsUrl('css/style.css'); + + $jsPath = $addon->getAssetsPath('js/script.js'); + if (is_file($jsPath)) { + $jsUrl .= '?v=' . (string) filemtime($jsPath); + } + + $cssPath = $addon->getAssetsPath('css/style.css'); + if (is_file($cssPath)) { + $cssUrl .= '?v=' . (string) filemtime($cssPath); + } + + rex_view::addJsFile($jsUrl); + rex_view::addCssFile($cssUrl); } // Create session variables if needed diff --git a/lang/de_de.lang b/lang/de_de.lang index c9954b9..a4b6719 100644 --- a/lang/de_de.lang +++ b/lang/de_de.lang @@ -51,6 +51,22 @@ ffmpeg_conversion_in_progress = Es läuft bereits eine Konvertierung. Bitte wart ffmpeg_view_in_mediapool = Im Medienpool anzeigen ffmpeg_smaller = kleiner ffmpeg_already_converted = Bereits konvertiert +ffmpeg_preview = Vorschau +ffmpeg_preview_original = Original-Vorschau +ffmpeg_preview_optimized = Optimierte Vorschau +ffmpeg_preview_optimized_button = Vorschau (Web) +ffmpeg_preview_modal_title = Video-Vorschau +ffmpeg_preview_modal_close = Schließen +ffmpeg_show_log = Protokoll anzeigen +ffmpeg_hide_log = Protokoll ausblenden +ffmpeg_convert_this_video = Dieses Video konvertieren +ffmpeg_col_file = Datei +ffmpeg_col_info = Informationen +ffmpeg_col_actions = Aktionen +ffmpeg_col_status = Status +ffmpeg_status_processing = Läuft +ffmpeg_status_converted = Konvertiert +ffmpeg_status_ready = Bereit ffmpeg_config_save = Einstellungen speichern ffmpeg_config_saved = Einstellungen wurden gespeichert! diff --git a/lang/en_gb.lang b/lang/en_gb.lang index 874805b..332b954 100644 --- a/lang/en_gb.lang +++ b/lang/en_gb.lang @@ -51,6 +51,22 @@ ffmpeg_conversion_in_progress = A conversion is already running. Please wait unt ffmpeg_view_in_mediapool = View in media pool ffmpeg_smaller = smaller ffmpeg_already_converted = Already converted +ffmpeg_preview = Preview +ffmpeg_preview_original = Original preview +ffmpeg_preview_optimized = Optimized preview +ffmpeg_preview_optimized_button = Preview (Web) +ffmpeg_preview_modal_title = Video preview +ffmpeg_preview_modal_close = Close +ffmpeg_show_log = Show log +ffmpeg_hide_log = Hide log +ffmpeg_convert_this_video = Convert this video +ffmpeg_col_file = File +ffmpeg_col_info = Information +ffmpeg_col_actions = Actions +ffmpeg_col_status = Status +ffmpeg_status_processing = Running +ffmpeg_status_converted = Converted +ffmpeg_status_ready = Ready ffmpeg_config_save = Save settings ffmpeg_config_saved = Settings have been saved! diff --git a/package.yml b/package.yml index ab84c31..36af47b 100644 --- a/package.yml +++ b/package.yml @@ -1,5 +1,5 @@ package: ffmpeg -version: '4.0.0' +version: '4.1.0' author: Friends Of REDAXO supportpage: https://github.com/FriendsOfREDAXO/ diff --git a/pages/mediapool.ffmpeg.main.php b/pages/mediapool.ffmpeg.main.php index 585999b..a094f9d 100644 --- a/pages/mediapool.ffmpeg.main.php +++ b/pages/mediapool.ffmpeg.main.php @@ -7,13 +7,6 @@ $csrfToken = rex_csrf_token::factory('ffmpeg'); -// Video-Parameter aus URL auslesen (für Direktlinks aus Vidstack) -$preselectedVideo = rex_request('video', 'string'); -// HTML-Entity-Dekodierung für URLs mit & -if (empty($preselectedVideo)) { - $preselectedVideo = html_entity_decode(rex_request('video', 'string')); -} - // Prüfen, ob eine aktive Konvertierung läuft $conversionActive = false; $conversionInfo = []; @@ -79,13 +72,13 @@ echo rex_view::info($this->i18n('ffmpeg_no_videos_found')); } else { // Videos in der konsolidierten Liste anzeigen - $content .= '

' . $this->i18n('ffmpeg_convert_info') . '

'; + $content .= '
' . $this->i18n('ffmpeg_convert_info') . '
'; if ($conversionActive) { $content .= rex_view::warning($this->i18n('ffmpeg_conversion_in_progress')); } - $content .= '
' . $this->i18n('legend_video') . ''; + $content .= '
' . $this->i18n('legend_video') . ''; $videoItems = []; foreach ($allVideos as $key => $video) { @@ -105,16 +98,23 @@ if (!empty($video['title'])) { $videoTitle = '
' . $video['title'] . '
'; } + + $inlineStatusDisplay = 'block'; + $inlineLog = ''; + if ($video['isProcessing'] && isset($conversionInfo['log']) && is_string($conversionInfo['log'])) { + $inlineLog = rex_escape($conversionInfo['log']); + } + + $statusText = $video['isProcessing'] + ? $this->i18n('ffmpeg_status_processing') + : ($video['isAlreadyConverted'] ? $this->i18n('ffmpeg_status_converted') : $this->i18n('ffmpeg_status_ready')); $item = ' -
-
'; - // Action Buttons - $formElements = []; - - // Convert Button - $n = []; - $n['field'] = ''; - $formElements[] = $n; - - // Status Button - $n = []; - $n['field'] = ''; - $formElements[] = $n; - - $fragment = new rex_fragment(); - $fragment->setVar('elements', $formElements, false); - $buttons = $fragment->parse('core/form/submit.php'); - $buttons = '
' . $buttons . '
'; + $buttons = ''; // Ausgabe Formular $fragment = new rex_fragment(); @@ -188,27 +206,27 @@ $output = $fragment->parse('core/page/section.php'); $output = ' -
+ ' . $csrfToken->getHiddenField() . ' ' . $output . '
-
-
-
-
+
+
+
+ ' . ($conversionActive ? '99%' : '0%') . '
-
+
- ' . ($conversionActive ? 'Läuft...' : '0%') . ' + ' . ($conversionActive ? 'Konvertierung läuft…' : '0%') . '
-
' . 
+                
             
@@ -216,6 +234,25 @@ '; echo $output; + + echo ''; // Füge Data-Attribute für JavaScript hinzu echo '
'; From 314b72eeeb24569e4d781663833836f213613b16 Mon Sep 17 00:00:00 2001 From: Thomas Skerbis Date: Wed, 29 Jul 2026 16:58:44 +0200 Subject: [PATCH 2/5] better ux --- assets/css/style.css | 66 ++++++++++++++++++++++++++++++--- lang/de_de.lang | 3 ++ lang/en_gb.lang | 3 ++ pages/mediapool.ffmpeg.main.php | 36 ++++++++++++------ 4 files changed, 91 insertions(+), 17 deletions(-) diff --git a/assets/css/style.css b/assets/css/style.css index d0cade6..e4f18ff 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -97,6 +97,18 @@ body.rex-theme-dark .ffmpeg-converter-ui { border-color: var(--ffmpeg-border-strong); } +.ffmpeg-converter-ui .video-file-date { + margin: 2px 0 0; + padding: 0 14px; + color: var(--ffmpeg-muted); + font-size: 11px; + line-height: 1.2; + opacity: 0.78; + display: inline-flex; + align-items: center; + gap: 6px; +} + .ffmpeg-converter-ui .video-head { display: flex; align-items: center; @@ -112,6 +124,11 @@ body.rex-theme-dark .ffmpeg-converter-ui { letter-spacing: 0.1px; } +.ffmpeg-converter-ui .video-file-cell { + display: flex; + flex-direction: column; +} + .ffmpeg-converter-ui .video-title { padding: 0 14px; color: var(--ffmpeg-muted); @@ -182,6 +199,18 @@ body.rex-theme-dark .ffmpeg-converter-ui { background: linear-gradient(180deg, rgba(var(--ffmpeg-success-rgb), 0.06), var(--ffmpeg-surface)); } +.ffmpeg-converter-ui .video-file-date { + margin: 2px 0 0; + padding: 0 14px; + color: var(--ffmpeg-muted); + font-size: 11px; + line-height: 1.2; + opacity: 0.78; + display: inline-flex; + align-items: center; + gap: 6px; +} + .ffmpeg-converter-ui .video-inline-status { margin: 0 14px 12px; padding: 10px; @@ -396,41 +425,67 @@ body.rex-theme-dark .ffmpeg-converter-ui { display: grid; grid-template-columns: 2fr 1.2fr 2fr 1.2fr; gap: 12px; - align-items: center; + align-items: start; padding: 12px 14px; } + .ffmpeg-converter-ui .video-file-cell { + grid-column: 1; + grid-row: 1; + align-self: start; + } + .ffmpeg-converter-ui .video-head { grid-column: 1; + grid-row: 1; padding: 0; } .ffmpeg-converter-ui .video-title { grid-column: 1; + grid-row: 1; padding: 0; - margin-top: 4px; + margin-top: 28px; + align-self: start; + } + + .ffmpeg-converter-ui .video-file-date { + grid-column: 1; + grid-row: 1; + padding: 0; + margin-top: 18px; + align-self: start; } .ffmpeg-converter-ui .video-meta { grid-column: 2; + grid-row: 1; padding: 0; display: grid; gap: 6px; - align-content: center; + align-content: start; + align-self: start; + margin-top: 4px; } .ffmpeg-converter-ui .video-actions { grid-column: 3; + grid-row: 1; padding: 0; + align-self: start; + margin-top: 4px; } .ffmpeg-converter-ui .video-inline-status { grid-column: 4; + grid-row: 1; margin: 0; padding: 8px; - min-height: 60px; + min-height: 0; display: flex; - align-items: center; + align-items: flex-start; + align-self: start; + margin-top: 2px; } .ffmpeg-converter-ui .video-inline-main { @@ -457,6 +512,7 @@ body.rex-theme-dark .ffmpeg-converter-ui { } .ffmpeg-converter-ui .video-title, + .ffmpeg-converter-ui .video-file-date, .ffmpeg-converter-ui .video-meta, .ffmpeg-converter-ui .video-actions { padding-left: 10px; diff --git a/lang/de_de.lang b/lang/de_de.lang index a4b6719..6fbde2e 100644 --- a/lang/de_de.lang +++ b/lang/de_de.lang @@ -67,6 +67,9 @@ ffmpeg_col_status = Status ffmpeg_status_processing = Läuft ffmpeg_status_converted = Konvertiert ffmpeg_status_ready = Bereit +ffmpeg_info_original_size = Original +ffmpeg_info_web_size = Web +ffmpeg_not_available = - ffmpeg_config_save = Einstellungen speichern ffmpeg_config_saved = Einstellungen wurden gespeichert! diff --git a/lang/en_gb.lang b/lang/en_gb.lang index 332b954..021332d 100644 --- a/lang/en_gb.lang +++ b/lang/en_gb.lang @@ -67,6 +67,9 @@ ffmpeg_col_status = Status ffmpeg_status_processing = Running ffmpeg_status_converted = Converted ffmpeg_status_ready = Ready +ffmpeg_info_original_size = Original +ffmpeg_info_web_size = Web +ffmpeg_not_available = - ffmpeg_config_save = Save settings ffmpeg_config_saved = Settings have been saved! diff --git a/pages/mediapool.ffmpeg.main.php b/pages/mediapool.ffmpeg.main.php index a094f9d..b2ddf8b 100644 --- a/pages/mediapool.ffmpeg.main.php +++ b/pages/mediapool.ffmpeg.main.php @@ -90,7 +90,7 @@ $statusBadge = ' Wird konvertiert...'; } elseif ($video['isAlreadyConverted']) { $statusClass = ' already-converted'; - $statusBadge = ' ' . $this->i18n('ffmpeg_already_converted') . ''; + $statusBadge = ''; } // Titel des Videos anzeigen, falls vorhanden @@ -99,6 +99,8 @@ $videoTitle = '
' . $video['title'] . '
'; } + $fileMetaLine = '
' . rex_formatter::strftime(strtotime($video['updatedate']), 'datetime') . '
'; + $inlineStatusDisplay = 'block'; $inlineLog = ''; if ($video['isProcessing'] && isset($conversionInfo['log']) && is_string($conversionInfo['log'])) { @@ -107,24 +109,34 @@ $statusText = $video['isProcessing'] ? $this->i18n('ffmpeg_status_processing') - : ($video['isAlreadyConverted'] ? $this->i18n('ffmpeg_status_converted') : $this->i18n('ffmpeg_status_ready')); + : ($video['isAlreadyConverted'] + ? (($video['compressionRate'] > 0) + ? ($video['compressionRate'] . '% ' . $this->i18n('ffmpeg_smaller')) + : $this->i18n('ffmpeg_status_converted')) + : $this->i18n('ffmpeg_status_ready')); $item = '
-
- ' . $video['filename'] . ' - ' . $statusBadge . ' +
+
+ ' . $video['filename'] . ' + ' . $statusBadge . ' +
+ ' . $fileMetaLine . ' + ' . $videoTitle . '
- ' . $videoTitle . '
- ' . rex_formatter::bytes($video['filesize']) . ' - ' . rex_formatter::strftime(strtotime($video['updatedate']), 'datetime') . ''; - - // Wenn konvertierte Version existiert, Details anzeigen - if ($video['isAlreadyConverted'] && $video['compressionRate'] > 0) { - $item .= ' ' . $video['compressionRate'] . '% ' . $this->i18n('ffmpeg_smaller') . ''; + '; + + $item .= ' ' . $this->i18n('ffmpeg_info_original_size') . ': ' . rex_formatter::bytes($video['filesize']) . ''; + if ($video['isAlreadyConverted'] && isset($video['optimizedData']['filesize'])) { + $item .= ' ' . $this->i18n('ffmpeg_info_web_size') . ': ' . rex_formatter::bytes((int) $video['optimizedData']['filesize']) . ''; + } else { + $item .= ' ' . $this->i18n('ffmpeg_info_web_size') . ': ' . $this->i18n('ffmpeg_not_available') . ''; } + // Kompressionsangabe wird in der Status-Spalte dargestellt + $item .= '
'; // Aktionsbereich für Konvertierung und Links From 15efa0a1f9e5b1a94e15c6e4bfa25d89a2861945 Mon Sep 17 00:00:00 2001 From: Thomas Skerbis Date: Wed, 29 Jul 2026 17:06:54 +0200 Subject: [PATCH 3/5] Update mediapool.ffmpeg.main.php --- pages/mediapool.ffmpeg.main.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pages/mediapool.ffmpeg.main.php b/pages/mediapool.ffmpeg.main.php index b2ddf8b..a3ab41a 100644 --- a/pages/mediapool.ffmpeg.main.php +++ b/pages/mediapool.ffmpeg.main.php @@ -159,14 +159,15 @@ // Wenn konvertierte Version existiert, Link zum optimierten Video und dessen Titel anzeigen if ($video['isAlreadyConverted']) { - // Titel der optimierten Version anzeigen, falls vorhanden - $optimizedTitle = ''; - if (!empty($video['optimizedData']['title'])) { - $optimizedTitle = ' (' . $video['optimizedData']['title'] . ')'; + $optimizedFormat = ''; + if (isset($video['optimizedData']['filename']) && is_string($video['optimizedData']['filename'])) { + $optimizedFormat = strtoupper((string) pathinfo($video['optimizedData']['filename'], PATHINFO_EXTENSION)); } + + $webVariantLabel = 'Web' . ('' !== $optimizedFormat ? ' (' . $optimizedFormat . ')' : ''); $item .= '
'; - $item .= ' Web-Version' . $optimizedTitle . ' '; + $item .= ' ' . rex_escape($webVariantLabel) . ' '; $item .= ''; $item .= '
'; } From 87385946d081e71d32531ed04695c8fea1af4c97 Mon Sep 17 00:00:00 2001 From: Thomas Skerbis Date: Wed, 29 Jul 2026 17:38:09 +0200 Subject: [PATCH 4/5] better trimmer --- assets/css/trimmer.css | 441 +++++++++++++++++++++++++++++ assets/js/trimmer.js | 285 +++++++++++++++++++ boot.php | 14 + pages/mediapool.ffmpeg.trimmer.php | 234 ++++----------- 4 files changed, 793 insertions(+), 181 deletions(-) create mode 100644 assets/css/trimmer.css create mode 100644 assets/js/trimmer.js diff --git a/assets/css/trimmer.css b/assets/css/trimmer.css new file mode 100644 index 0000000..d209783 --- /dev/null +++ b/assets/css/trimmer.css @@ -0,0 +1,441 @@ +.ffmpeg-trimmer-panel { + --ffmpeg-trimmer-table-head-bg: #f4f8fc; + --ffmpeg-trimmer-table-head-border: #d6e1ec; + --ffmpeg-trimmer-table-head-text: #3b556e; + --ffmpeg-trimmer-editor-bg: #ffffff; + --ffmpeg-trimmer-editor-text: #23384d; + --ffmpeg-trimmer-editor-muted: #5b6f83; + --ffmpeg-trimmer-control-bg: #f8fbff; + --ffmpeg-trimmer-control-border: #dce5ef; + --ffmpeg-trimmer-field-bg: #ffffff; + --ffmpeg-trimmer-field-border: #c9d7e6; + --ffmpeg-trimmer-field-text: #23384d; + --ffmpeg-trimmer-video-border: #d3dde8; + --ffmpeg-trimmer-default-btn-bg: #f7faff; + --ffmpeg-trimmer-default-btn-border: #d3dde8; + --ffmpeg-trimmer-default-btn-text: #23384d; + --ffmpeg-trimmer-info-btn-bg: #2f7fd2; + --ffmpeg-trimmer-info-btn-border: #276eaf; + --ffmpeg-trimmer-info-btn-text: #ffffff; + --ffmpeg-trimmer-hud-bg: rgba(17, 25, 34, 0.82); + --ffmpeg-trimmer-hud-border: rgba(210, 224, 238, 0.35); + --ffmpeg-trimmer-hud-chip-bg: rgba(255, 255, 255, 0.16); + --ffmpeg-trimmer-hud-chip-text: #ecf3fa; + --ffmpeg-trimmer-hud-text: #ecf3fa; + --ffmpeg-trimmer-scrubber-accent: #4ca9ff; + border: 1px solid #d6e1ec; + border-radius: 12px; + box-shadow: 0 10px 28px rgba(21, 32, 43, 0.08); + overflow: hidden; +} + +#videoPreviewModal { + --ffmpeg-trimmer-modal-bg: #ffffff; + --ffmpeg-trimmer-modal-header-bg: #f7faff; + --ffmpeg-trimmer-modal-border: #d3dde8; + --ffmpeg-trimmer-modal-text: #23384d; + --ffmpeg-trimmer-modal-muted: #556b80; + --ffmpeg-trimmer-modal-btn-bg: #f7faff; + --ffmpeg-trimmer-modal-btn-border: #d3dde8; + --ffmpeg-trimmer-modal-btn-text: #23384d; +} + +body.rex-theme-dark .ffmpeg-trimmer-panel { + --ffmpeg-trimmer-table-head-bg: #24303b; + --ffmpeg-trimmer-table-head-border: #3b4754; + --ffmpeg-trimmer-table-head-text: #d6e4f1; + --ffmpeg-trimmer-editor-bg: #1f2933; + --ffmpeg-trimmer-editor-text: #d6e4f1; + --ffmpeg-trimmer-editor-muted: #aebfd0; + --ffmpeg-trimmer-control-bg: #24303b; + --ffmpeg-trimmer-control-border: #3b4754; + --ffmpeg-trimmer-field-bg: #1b242e; + --ffmpeg-trimmer-field-border: #425160; + --ffmpeg-trimmer-field-text: #d6e4f1; + --ffmpeg-trimmer-video-border: #3b4754; + --ffmpeg-trimmer-default-btn-bg: #24303b; + --ffmpeg-trimmer-default-btn-border: #3b4754; + --ffmpeg-trimmer-default-btn-text: #d6e4f1; + --ffmpeg-trimmer-info-btn-bg: #2e6faa; + --ffmpeg-trimmer-info-btn-border: #265f8f; + --ffmpeg-trimmer-info-btn-text: #eaf4ff; + --ffmpeg-trimmer-hud-bg: rgba(9, 14, 20, 0.86); + --ffmpeg-trimmer-hud-border: rgba(92, 113, 133, 0.45); + --ffmpeg-trimmer-hud-chip-bg: rgba(174, 191, 208, 0.2); + --ffmpeg-trimmer-hud-chip-text: #e5eff9; + --ffmpeg-trimmer-hud-text: #d8e7f6; + --ffmpeg-trimmer-scrubber-accent: #65b5ff; + border-color: #3b4754; + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35); +} + +body.rex-theme-dark #videoPreviewModal { + --ffmpeg-trimmer-modal-bg: #1f2933; + --ffmpeg-trimmer-modal-header-bg: #24303b; + --ffmpeg-trimmer-modal-border: #3b4754; + --ffmpeg-trimmer-modal-text: #d6e4f1; + --ffmpeg-trimmer-modal-muted: #aebfd0; + --ffmpeg-trimmer-modal-btn-bg: #24303b; + --ffmpeg-trimmer-modal-btn-border: #3b4754; + --ffmpeg-trimmer-modal-btn-text: #d6e4f1; +} + +@media (prefers-color-scheme: dark) { + body.rex-has-theme:not(.rex-theme-light) .ffmpeg-trimmer-panel { + --ffmpeg-trimmer-table-head-bg: #24303b; + --ffmpeg-trimmer-table-head-border: #3b4754; + --ffmpeg-trimmer-table-head-text: #d6e4f1; + --ffmpeg-trimmer-editor-bg: #1f2933; + --ffmpeg-trimmer-editor-text: #d6e4f1; + --ffmpeg-trimmer-editor-muted: #aebfd0; + --ffmpeg-trimmer-control-bg: #24303b; + --ffmpeg-trimmer-control-border: #3b4754; + --ffmpeg-trimmer-field-bg: #1b242e; + --ffmpeg-trimmer-field-border: #425160; + --ffmpeg-trimmer-field-text: #d6e4f1; + --ffmpeg-trimmer-video-border: #3b4754; + --ffmpeg-trimmer-default-btn-bg: #24303b; + --ffmpeg-trimmer-default-btn-border: #3b4754; + --ffmpeg-trimmer-default-btn-text: #d6e4f1; + --ffmpeg-trimmer-info-btn-bg: #2e6faa; + --ffmpeg-trimmer-info-btn-border: #265f8f; + --ffmpeg-trimmer-info-btn-text: #eaf4ff; + --ffmpeg-trimmer-hud-bg: rgba(9, 14, 20, 0.86); + --ffmpeg-trimmer-hud-border: rgba(92, 113, 133, 0.45); + --ffmpeg-trimmer-hud-chip-bg: rgba(174, 191, 208, 0.2); + --ffmpeg-trimmer-hud-chip-text: #e5eff9; + --ffmpeg-trimmer-hud-text: #d8e7f6; + --ffmpeg-trimmer-scrubber-accent: #65b5ff; + border-color: #3b4754; + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35); + } + + body.rex-has-theme:not(.rex-theme-light) #videoPreviewModal { + --ffmpeg-trimmer-modal-bg: #1f2933; + --ffmpeg-trimmer-modal-header-bg: #24303b; + --ffmpeg-trimmer-modal-border: #3b4754; + --ffmpeg-trimmer-modal-text: #d6e4f1; + --ffmpeg-trimmer-modal-muted: #aebfd0; + --ffmpeg-trimmer-modal-btn-bg: #24303b; + --ffmpeg-trimmer-modal-btn-border: #3b4754; + --ffmpeg-trimmer-modal-btn-text: #d6e4f1; + } +} + +.ffmpeg-trimmer-panel .panel-heading { + background: linear-gradient(120deg, #1f5ea8, #2f7fd2); + border-bottom: 0; + color: #fff; +} + +.ffmpeg-trimmer-panel .panel-title { + font-weight: 600; +} + +.ffmpeg-trimmer-editor-panel .panel-body { + background: var(--ffmpeg-trimmer-editor-bg); + color: var(--ffmpeg-trimmer-editor-text); + padding: 20px; +} + +.video-trimmer-container { + max-width: 980px; + margin: 0 auto; +} + +.trimmer-video-stage { + position: relative; + margin-bottom: 12px; +} + +.trimmer-video { + width: 100%; + border: 1px solid var(--ffmpeg-trimmer-video-border); + border-radius: 12px; + background: #0a1017; + margin-bottom: 0; +} + +.trimmer-video-hud { + position: absolute; + left: 12px; + right: 12px; + bottom: 12px; + border: 1px solid var(--ffmpeg-trimmer-hud-border); + background: var(--ffmpeg-trimmer-hud-bg); + border-radius: 10px; + padding: 10px; + color: var(--ffmpeg-trimmer-hud-text); + backdrop-filter: blur(6px); +} + +.trimmer-video-hud-top { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 8px; +} + +.trimmer-time-chip { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 8px; + border-radius: 999px; + background: var(--ffmpeg-trimmer-hud-chip-bg); + color: var(--ffmpeg-trimmer-hud-chip-text); + font-size: 11px; + line-height: 1.3; +} + +.trimmer-time-chip strong { + font-weight: 700; +} + +.trimmer-scrubber { + width: 100%; + margin-bottom: 8px; + accent-color: var(--ffmpeg-trimmer-scrubber-accent); +} + +.trimmer-video-hud-controls { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.trimmer-video-hud-controls .btn { + border-radius: 6px; +} + +.trimmer-form { + margin-top: 8px; +} + +.video-controls-wrapper { + border: 1px solid var(--ffmpeg-trimmer-control-border); + border-radius: 12px; + background: var(--ffmpeg-trimmer-control-bg); + padding: 14px; +} + +.trimmer-range-label { + font-size: 14px; + font-weight: 700; + margin-bottom: 6px; + display: block; +} + +.trimmer-duration-hint { + margin: 0 0 12px; + color: var(--ffmpeg-trimmer-editor-muted); + font-size: 12px; +} + +.video-controls-wrapper .control-label { + color: var(--ffmpeg-trimmer-editor-text); + font-weight: 600; + margin-bottom: 5px; + display: block; +} + +.video-controls-wrapper .form-control { + background: var(--ffmpeg-trimmer-field-bg); + border-color: var(--ffmpeg-trimmer-field-border); + color: var(--ffmpeg-trimmer-field-text); +} + +.video-controls-wrapper .form-control:focus { + border-color: var(--ffmpeg-trimmer-info-btn-border); + box-shadow: 0 0 0 2px rgba(47, 127, 210, 0.18); +} + +.video-controls-wrapper .input-group-btn .btn { + border-color: var(--ffmpeg-trimmer-info-btn-border); +} + +.video-controls-wrapper .input-group-btn .btn-info, +.video-controls-wrapper .input-group-btn .trimmer-set-current { + background: var(--ffmpeg-trimmer-info-btn-bg); + border-color: var(--ffmpeg-trimmer-info-btn-border); + color: var(--ffmpeg-trimmer-info-btn-text); +} + +.video-controls-wrapper .input-group-btn .btn-info:hover, +.video-controls-wrapper .input-group-btn .btn-info:focus, +.video-controls-wrapper .input-group-btn .trimmer-set-current:hover, +.video-controls-wrapper .input-group-btn .trimmer-set-current:focus { + background: var(--ffmpeg-trimmer-info-btn-border); + border-color: var(--ffmpeg-trimmer-info-btn-border); + color: var(--ffmpeg-trimmer-info-btn-text); +} + +.video-controls-wrapper .input-group { + margin-bottom: 10px; +} + +.trimmer-actions { + margin-top: 18px; +} + +.trimmer-actions .btn + .btn { + margin-left: 6px; +} + +.trimmer-actions .btn-default { + background: var(--ffmpeg-trimmer-default-btn-bg); + border-color: var(--ffmpeg-trimmer-default-btn-border); + color: var(--ffmpeg-trimmer-default-btn-text); +} + +.trimmer-actions .btn-default:hover, +.trimmer-actions .btn-default:focus { + background: var(--ffmpeg-trimmer-control-bg); + border-color: var(--ffmpeg-trimmer-default-btn-border); + color: var(--ffmpeg-trimmer-default-btn-text); +} + +.ffmpeg-trimmer-table { + margin-bottom: 0; +} + +.ffmpeg-trimmer-table th { + background: var(--ffmpeg-trimmer-table-head-bg); + border-bottom: 2px solid var(--ffmpeg-trimmer-table-head-border); + color: var(--ffmpeg-trimmer-table-head-text); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.ffmpeg-trimmer-col-file { + width: 250px; +} + +.ffmpeg-trimmer-col-size { + width: 100px; +} + +.ffmpeg-trimmer-col-date { + width: 120px; +} + +.ffmpeg-trimmer-col-actions { + width: 180px; +} + +.ffmpeg-trimmer-table .video-filename { + max-width: 250px; + word-wrap: break-word; + word-break: break-all; +} + +.ffmpeg-trimmer-table .filename-truncate { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ffmpeg-trimmer-table .video-actions .btn { + margin-right: 4px; +} + +#videoPreviewModal .modal-content { + background: var(--ffmpeg-trimmer-modal-bg); + border: 1px solid var(--ffmpeg-trimmer-modal-border); + color: var(--ffmpeg-trimmer-modal-text); + border-radius: 12px; + overflow: hidden; +} + +#videoPreviewModal .modal-header { + background: var(--ffmpeg-trimmer-modal-header-bg); + border-bottom: 1px solid var(--ffmpeg-trimmer-modal-border); +} + +#videoPreviewModal .modal-title { + color: var(--ffmpeg-trimmer-modal-text); +} + +#videoPreviewModal .modal-body { + background: var(--ffmpeg-trimmer-modal-bg); + color: var(--ffmpeg-trimmer-modal-text); + padding: 15px; +} + +#videoPreviewModal .modal-footer { + background: var(--ffmpeg-trimmer-modal-bg); + border-top: 1px solid var(--ffmpeg-trimmer-modal-border); +} + +#videoPreviewModal .close { + color: var(--ffmpeg-trimmer-modal-text); + opacity: 0.75; +} + +#videoPreviewModal .close:hover, +#videoPreviewModal .close:focus { + color: var(--ffmpeg-trimmer-modal-text); + opacity: 1; +} + +#videoPreviewModal .btn-default { + background: var(--ffmpeg-trimmer-modal-btn-bg); + border-color: var(--ffmpeg-trimmer-modal-btn-border); + color: var(--ffmpeg-trimmer-modal-btn-text); +} + +#videoPreviewModal .btn-default:hover, +#videoPreviewModal .btn-default:focus { + background: var(--ffmpeg-trimmer-modal-header-bg); + border-color: var(--ffmpeg-trimmer-modal-btn-border); + color: var(--ffmpeg-trimmer-modal-btn-text); +} + +#videoPreviewModal video { + width: 100%; + max-height: 500px; + border: 1px solid #d3dde8; + border-radius: 8px; + background: #000; +} + +.video-filename-display { + margin-top: 10px; + font-weight: 600; + word-wrap: break-word; + font-size: 12px; + color: var(--ffmpeg-trimmer-modal-muted); +} + +@media (max-width: 768px) { + .ffmpeg-trimmer-editor-panel .panel-body { + padding: 14px; + } + + .video-trimmer-container { + max-width: none; + margin: 0; + } + + .video-controls-wrapper .row .col-sm-6 { + margin-bottom: 14px; + } + + .ffmpeg-trimmer-table .video-filename { + max-width: 150px; + } + + .ffmpeg-trimmer-table .video-actions .btn { + margin-bottom: 3px; + } + + .trimmer-video-hud { + position: static; + margin-top: 10px; + left: auto; + right: auto; + bottom: auto; + } +} diff --git a/assets/js/trimmer.js b/assets/js/trimmer.js new file mode 100644 index 0000000..d33fc48 --- /dev/null +++ b/assets/js/trimmer.js @@ -0,0 +1,285 @@ +(function ($) { + var selectionPlaybackActive = false; + + function toNumber(value) { + var parsed = parseFloat(value); + if (Number.isNaN(parsed)) { + return 0; + } + return parsed; + } + + function formatTime(seconds) { + var safeSeconds = Math.max(0, toNumber(seconds)); + return safeSeconds.toFixed(1) + 's'; + } + + function updateDurationHint() { + var context = document.getElementById('ffmpeg-trimmer-context'); + if (!context) { + return; + } + + var template = context.getAttribute('data-preview-template') || 'Das geschnittene Video wird {0} Sekunden lang sein'; + var hint = document.getElementById('trimmer-duration-hint'); + if (!hint) { + return; + } + + var start = toNumber($('#start_time').val()); + var end = toNumber($('#end_time').val()); + var duration = Math.max(0, end - start); + + hint.textContent = template.replace('{0}', duration.toFixed(1)); + } + + function setCurrentTime(target) { + var video = document.getElementById('trimmer-video'); + if (!video) { + return; + } + + var currentTime = video.currentTime.toFixed(1); + if (target === 'start') { + $('#start_time').val(currentTime); + } else if (target === 'end') { + $('#end_time').val(currentTime); + } + + updateDurationHint(); + updateVideoHud(); + } + + function seekBy(seconds) { + var video = document.getElementById('trimmer-video'); + if (!video) { + return; + } + + var duration = Number.isFinite(video.duration) ? video.duration : 0; + var next = Math.max(0, Math.min(duration, video.currentTime + seconds)); + video.currentTime = next; + updateVideoHud(); + } + + function updateVideoHud() { + var video = document.getElementById('trimmer-video'); + if (!video) { + return; + } + + var currentNode = document.getElementById('trimmer-chip-current'); + var startNode = document.getElementById('trimmer-chip-start'); + var endNode = document.getElementById('trimmer-chip-end'); + var scrubber = document.getElementById('trimmer-scrubber'); + + var start = toNumber($('#start_time').val()); + var end = toNumber($('#end_time').val()); + var current = toNumber(video.currentTime); + var duration = Number.isFinite(video.duration) ? video.duration : 0; + + if (currentNode) { + currentNode.textContent = formatTime(current); + } + if (startNode) { + startNode.textContent = formatTime(start); + } + if (endNode) { + endNode.textContent = formatTime(end); + } + + if (scrubber) { + scrubber.max = String(duration); + scrubber.value = String(current); + } + } + + function playSelection() { + var video = document.getElementById('trimmer-video'); + if (!video) { + return; + } + + var start = toNumber($('#start_time').val()); + var end = toNumber($('#end_time').val()); + + if (end <= start) { + return; + } + + selectionPlaybackActive = true; + video.currentTime = start; + video.play(); + } + + function setupEditor() { + var video = document.getElementById('trimmer-video'); + if (!video) { + return; + } + + function applyVideoMetadata() { + if (!Number.isFinite(video.duration) || video.duration <= 0) { + return; + } + + if (!$('#end_time').val()) { + $('#end_time').val(video.duration.toFixed(1)); + } + + var scrubber = document.getElementById('trimmer-scrubber'); + if (scrubber) { + scrubber.max = String(video.duration); + if (!scrubber.value || scrubber.value === '0') { + scrubber.value = String(video.currentTime || 0); + } + } + + updateDurationHint(); + updateVideoHud(); + } + + $(document).on('click', '.trimmer-set-current', function () { + var target = $(this).data('target'); + setCurrentTime(String(target)); + }); + + $('#start_time, #end_time').on('input change', function () { + updateDurationHint(); + updateVideoHud(); + }); + + $(document).on('click', '.trimmer-video-control', function () { + var action = String($(this).data('action') || ''); + if (action === 'seek') { + seekBy(toNumber($(this).data('seconds'))); + return; + } + + if (action === 'toggle-play') { + if (video.paused) { + video.play(); + } else { + video.pause(); + selectionPlaybackActive = false; + } + return; + } + + if (action === 'mark-start') { + setCurrentTime('start'); + return; + } + + if (action === 'mark-end') { + setCurrentTime('end'); + return; + } + + if (action === 'play-selection') { + playSelection(); + } + }); + + $('#trimmer-scrubber').on('input change', function () { + video.currentTime = toNumber($(this).val()); + updateVideoHud(); + }); + + video.addEventListener('timeupdate', function () { + updateVideoHud(); + + if (!selectionPlaybackActive) { + return; + } + + var end = toNumber($('#end_time').val()); + if (video.currentTime >= end) { + video.pause(); + selectionPlaybackActive = false; + } + }); + + video.addEventListener('pause', function () { + selectionPlaybackActive = false; + }); + + // Prefill and sync metadata-dependent controls. + video.addEventListener('loadedmetadata', applyVideoMetadata); + video.addEventListener('durationchange', applyVideoMetadata); + video.addEventListener('loadeddata', applyVideoMetadata); + + document.addEventListener('keydown', function (event) { + var activeTag = (document.activeElement && document.activeElement.tagName) ? document.activeElement.tagName.toLowerCase() : ''; + if (activeTag === 'input' || activeTag === 'textarea') { + return; + } + + if (event.ctrlKey && event.key.toLowerCase() === 's') { + event.preventDefault(); + setCurrentTime('start'); + return; + } + + if (event.ctrlKey && event.key.toLowerCase() === 'e') { + event.preventDefault(); + setCurrentTime('end'); + return; + } + + if (event.key === ' ') { + event.preventDefault(); + if (video.paused) { + video.play(); + } else { + video.pause(); + selectionPlaybackActive = false; + } + } + }); + + applyVideoMetadata(); + updateVideoHud(); + } + + function setupListPreview() { + var context = document.getElementById('ffmpeg-trimmer-context'); + if (!context) { + return; + } + + var mediaBase = context.getAttribute('data-media-base') || ''; + + $(document).on('click', '.video-preview-btn', function () { + var filename = $(this).data('filename'); + if (!filename) { + return; + } + + var modal = $('#videoPreviewModal'); + if (!modal.length) { + return; + } + + var video = modal.find('#modalVideo')[0]; + var source = modal.find('#modalVideoSource')[0]; + + video.pause(); + source.src = mediaBase + encodeURIComponent(String(filename)); + modal.find('.video-filename-display').text(String(filename)); + + video.load(); + modal.modal('show'); + }); + + $('#videoPreviewModal').on('hidden.bs.modal', function () { + var video = $(this).find('#modalVideo')[0]; + video.pause(); + }); + } + + $(document).ready(function () { + setupEditor(); + setupListPreview(); + }); +})(jQuery); diff --git a/boot.php b/boot.php index 3fad56c..905c2b4 100644 --- a/boot.php +++ b/boot.php @@ -31,8 +31,22 @@ $cssUrl .= '?v=' . (string) filemtime($cssPath); } + $trimmerJsUrl = $addon->getAssetsUrl('js/trimmer.js'); + $trimmerJsPath = $addon->getAssetsPath('js/trimmer.js'); + if (is_file($trimmerJsPath)) { + $trimmerJsUrl .= '?v=' . (string) filemtime($trimmerJsPath); + } + + $trimmerCssUrl = $addon->getAssetsUrl('css/trimmer.css'); + $trimmerCssPath = $addon->getAssetsPath('css/trimmer.css'); + if (is_file($trimmerCssPath)) { + $trimmerCssUrl .= '?v=' . (string) filemtime($trimmerCssPath); + } + rex_view::addJsFile($jsUrl); rex_view::addCssFile($cssUrl); + rex_view::addJsFile($trimmerJsUrl); + rex_view::addCssFile($trimmerCssUrl); } // Create session variables if needed diff --git a/pages/mediapool.ffmpeg.trimmer.php b/pages/mediapool.ffmpeg.trimmer.php index e4383e6..7b41960 100644 --- a/pages/mediapool.ffmpeg.trimmer.php +++ b/pages/mediapool.ffmpeg.trimmer.php @@ -145,33 +145,54 @@ if ($videoFile && $videoInfo) { // Video-Editor anzeigen $content .= ' -
+

' . $this->i18n('ffmpeg_trimmer_cut_video') . ': ' . rex_escape($videoInfo['filename']) . '

-
- - -
+
+
+ +
+
+ Start 0.0s + Jetzt 0.0s + Ende 0.0s +
+ +
+ + + + + + + + +
+
+
+ + ' . $csrf->getHiddenField() . ' - +
- + +

' . str_replace('{0}', '0', $this->i18n('ffmpeg_trimmer_preview')) . '

- @@ -182,7 +203,7 @@
- @@ -190,8 +211,8 @@
- -
+ +
@@ -202,45 +223,11 @@
-
- - '; - +
'; } else { // Video-Liste anzeigen $content .= ' -
+

' . $this->i18n('ffmpeg_trimmer') . ' @@ -248,28 +235,27 @@ function setCurrentTime(type) {

' . $this->i18n('ffmpeg_trimmer_select_video') . '

'; - + if (count($videos) > 0) { $content .= '
- +
- + - - - + + + '; - + foreach ($videos as $video) { $filesize = rex_formatter::bytes($video['filesize']); $date = rex_formatter::strftime($video['updatedate'], 'date'); - // Prepare safe data attributes $escapedFilename = rex_escape($video['filename']); - + $content .= ' '; } - + $content .= '
' . $this->i18n('ffmpeg_trimmer_table_filename') . '' . $this->i18n('ffmpeg_trimmer_table_filename') . ' ' . $this->i18n('ffmpeg_trimmer_table_title') . '' . $this->i18n('ffmpeg_trimmer_table_size') . '' . $this->i18n('ffmpeg_trimmer_table_date') . '' . $this->i18n('ffmpeg_trimmer_table_actions') . '' . $this->i18n('ffmpeg_trimmer_table_size') . '' . $this->i18n('ffmpeg_trimmer_table_date') . '' . $this->i18n('ffmpeg_trimmer_table_actions') . '
@@ -288,15 +274,13 @@ function setCurrentTime(type) {
'; - - // Add modal for video preview + $content .= ' - -
- - '; +
'; } else { $content .= '
@@ -356,96 +310,14 @@ function setCurrentTime(type) {

' . str_replace('{0}', rex_url::backendPage('media'), $this->i18n('ffmpeg_trimmer_upload_videos')) . '

'; } - + $content .= '
'; } -// Zusätzliche Styles -$content .= ' -'; +// Kontext-Daten für Trimmer-JS +$content .= '
'; // Fragment erstellen $fragment = new rex_fragment(); From e83989f7d47073312e32d3b7a6fedff0c343caf8 Mon Sep 17 00:00:00 2001 From: Thomas Skerbis Date: Wed, 29 Jul 2026 17:50:58 +0200 Subject: [PATCH 5/5] trimmer cool --- CHANGELOG.md | 7 +++- README.md | 16 ++++++-- assets/js/script.js | 19 ++++++++- assets/js/trimmer.js | 64 ++++++++++++++++++++++++++++-- lang/de_de.lang | 1 + lang/en_gb.lang | 1 + pages/mediapool.ffmpeg.main.php | 3 ++ pages/mediapool.ffmpeg.trimmer.php | 6 +++ 8 files changed, 109 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eea1ce..391209b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,19 @@ ### Neu - Video-Konverter-Oberfläche als tabellarisches Desktop-Layout mit klaren Spalten (Datei, Informationen, Aktionen, Status). - Konvertierung direkt am jeweiligen Dateieintrag über "Dieses Video konvertieren". -- Vorschau-Modal für Original und Web-Version direkt aus der Liste. +- Vorschau-Modal für Original und Web-Version direkt aus der Liste, inklusive Loop-Schalter. - Dateibezogener Konvertierungsstatus mit Donut-Anzeige und optional aufklappbarem Protokoll. +- Video-Trimmer mit direkt am Video eingebetteter Steuerleiste, Scrubber, Schnellbuttons, Start-/Ende-Markern und Bereichstest. ### Verbessert - Deutlich reduzierte visuelle Unruhe durch kompaktere Abstände, konsistente Button-Gruppen und klarere Kartenstruktur. - Statusdarstellung pro Zeile: Bereit, Konvertiert oder Laufend. - Globaler Statusbereich als kompakter Technik-Block statt dominanter Fortschrittsleiste. +- Trimmer-, Tabellen- und Modal-Oberfläche für Light-, Dark- und Auto-Theme optimiert. +- Trimmer-Editor, Eingabefelder und Buttons erhalten nun konsistente Theme-Farben und bessere Lesbarkeit im Dark Mode. +- Vorschau- und Trimmer-Interaktionen laufen zuverlässig über REDAXO-Backend-Navigationen ohne kompletten Reload. ### Technisch - Asset-Cache-Busting fuer CSS/JS in `boot.php`. - JavaScript auf Donut-Fortschritt und zeilenbezogene Statussteuerung umgestellt. +- Trimmer-Logik für Scrubber, HUD-Buttons, Loop-Schalter und Auswahl-Wiedergabe erweitert. diff --git a/README.md b/README.md index 5529e70..941ec07 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# REDAXO-AddOn: FFmpeg Video Tools v4.1 +# REDAXO-AddOn: FFmpeg Video Tools v4.1.0 Vollständige Video-Management-Lösung für REDAXO CMS – Konvertierung, Trimming und detaillierte Video-Analyse, alles in einem Addon! @@ -25,7 +25,9 @@ Vollständige Video-Management-Lösung für REDAXO CMS – Konvertierung, Trimmi ### 🆕 Video-Trimmer - **Präzises Schneiden** direkt im Browser - **Verlustfreies Trimming** mit FFmpeg Stream-Copy -- **Intuitive Bedienung** mit Video-Player-Integration +- **Intuitive Bedienung** mit Video-Player-Integration, Scrubber und HUD-Steuerung +- **Bereichstest** für definierte Start-/Endzeit direkt im Player +- **Loop-Schalter** für einfache Vorschau-Tests - **Keyboard-Shortcuts** für professionellen Workflow - **Alle Video-Typen** unterstützt (Original + web-optimiert) @@ -64,7 +66,9 @@ Vollständige Video-Management-Lösung für REDAXO CMS – Konvertierung, Trimmi 4. **Start-/Endzeit setzen**: - Position im Player pausieren - "Aktuelle Zeit setzen" klicken (oder Strg+S/E) -5. "Video schneiden" → Geschnittenes Video als `web_trimmed_*` gespeichert +5. **Bereich testen** nutzen, um den markierten Abschnitt wiederholt abzuspielen +6. Optional **Loop-Schalter** aktivieren, damit die Vorschau direkt in Schleife läuft +7. "Video schneiden" → Geschnittenes Video als `web_trimmed_*` gespeichert ### Video-Informationen 1. **Medienpool → Video-Tools → Video-Informationen** öffnen @@ -231,6 +235,12 @@ echo '
'; - `getOptimizationStatus($filename)` – Web-Optimierung mit Score - `isMobileOptimized($filename)` – Mobile-Kompatibilität prüfen +## ✨ Neueste Verbesserungen +- Moderne Vorschau-Modal mit Loop-Option für schnelle Prüfungen +- Trimmer-UI mit direkt eingebetteten Controls, Scrubber und Bereichstest +- Theme-optimierte Darstellung für Light-, Dark- und Auto-Theme +- Zuverlässige Initialisierung im REDAXO-Backend auch nach Navigation ohne kompletten Reload + ## ⚙️ Installation & Konfiguration ### FFmpeg Installation diff --git a/assets/js/script.js b/assets/js/script.js index f69999f..cf9e786 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -392,7 +392,7 @@ }); } - $(document).ready(function () { + function initFfmpegConverter() { // Bei Seitenladung den Status prüfen checkStatus(); @@ -447,12 +447,21 @@ videoElement.removeAttribute('src'); videoElement.load(); videoElement.setAttribute('src', mediaUrl); + videoElement.loop = $('.ffmpeg-preview-loop-toggle').is(':checked'); videoElement.load(); } $('#ffmpeg-video-preview-modal').modal('show'); }); + $(document).on('change', '.ffmpeg-preview-loop-toggle', function () { + var targetId = $(this).data('previewTarget'); + var videoElement = document.getElementById(targetId); + if (videoElement) { + videoElement.loop = $(this).is(':checked'); + } + }); + // Inline-Protokoll ein-/ausklappen $(document).on('click', '.ffmpeg-toggle-inline-log', function () { var $button = $(this); @@ -467,6 +476,14 @@ }); }); + $(document).on('rex:ready', function () { + initFfmpegConverter(); + }); + + $(document).ready(function () { + initFfmpegConverter(); + }); + // Konvertierung starten function startConversion(video, confirmOverwrite) { let url = 'index.php?rex-api-call=ffmpeg_convert&func=start&video=' + encodeURIComponent(video); diff --git a/assets/js/trimmer.js b/assets/js/trimmer.js index d33fc48..eea3653 100644 --- a/assets/js/trimmer.js +++ b/assets/js/trimmer.js @@ -108,16 +108,49 @@ } selectionPlaybackActive = true; + video.loop = false; video.currentTime = start; video.play(); } + function restartSelectionPlayback() { + var video = document.getElementById('trimmer-video'); + if (!video || !selectionPlaybackActive) { + return; + } + + var start = toNumber($('#start_time').val()); + var end = toNumber($('#end_time').val()); + if (end <= start) { + return; + } + + if (video.currentTime < end) { + return; + } + + video.pause(); + video.currentTime = start; + window.setTimeout(function () { + if (selectionPlaybackActive && video.paused) { + video.play(); + } + }, 50); + } + function setupEditor() { var video = document.getElementById('trimmer-video'); if (!video) { return; } + if (video.dataset.ffmpegReady === '1') { + applyVideoMetadata(); + updateVideoHud(); + return; + } + video.dataset.ffmpegReady = '1'; + function applyVideoMetadata() { if (!Number.isFinite(video.duration) || video.duration <= 0) { return; @@ -186,6 +219,14 @@ updateVideoHud(); }); + $(document).on('change', '.trimmer-loop-toggle', function () { + var targetId = $(this).data('target'); + var targetVideo = document.getElementById(targetId); + if (targetVideo) { + targetVideo.loop = $(this).is(':checked'); + } + }); + video.addEventListener('timeupdate', function () { updateVideoHud(); @@ -195,8 +236,7 @@ var end = toNumber($('#end_time').val()); if (video.currentTime >= end) { - video.pause(); - selectionPlaybackActive = false; + restartSelectionPlayback(); } }); @@ -263,23 +303,41 @@ var video = modal.find('#modalVideo')[0]; var source = modal.find('#modalVideoSource')[0]; + var loopToggle = modal.find('.video-preview-loop-toggle'); video.pause(); source.src = mediaBase + encodeURIComponent(String(filename)); modal.find('.video-filename-display').text(String(filename)); + video.loop = loopToggle.is(':checked'); video.load(); modal.modal('show'); }); + $('#videoPreviewModal').on('change', '.video-preview-loop-toggle', function () { + var targetId = $(this).data('previewTarget'); + var video = document.getElementById(targetId); + if (video) { + video.loop = $(this).is(':checked'); + } + }); + $('#videoPreviewModal').on('hidden.bs.modal', function () { var video = $(this).find('#modalVideo')[0]; video.pause(); }); } - $(document).ready(function () { + function initFfmpegTrimmer() { setupEditor(); setupListPreview(); + } + + $(document).on('rex:ready', function () { + initFfmpegTrimmer(); + }); + + $(document).ready(function () { + initFfmpegTrimmer(); }); })(jQuery); diff --git a/lang/de_de.lang b/lang/de_de.lang index 6fbde2e..293b851 100644 --- a/lang/de_de.lang +++ b/lang/de_de.lang @@ -57,6 +57,7 @@ ffmpeg_preview_optimized = Optimierte Vorschau ffmpeg_preview_optimized_button = Vorschau (Web) ffmpeg_preview_modal_title = Video-Vorschau ffmpeg_preview_modal_close = Schließen +ffmpeg_preview_loop = Schleife ffmpeg_show_log = Protokoll anzeigen ffmpeg_hide_log = Protokoll ausblenden ffmpeg_convert_this_video = Dieses Video konvertieren diff --git a/lang/en_gb.lang b/lang/en_gb.lang index 021332d..95f4be8 100644 --- a/lang/en_gb.lang +++ b/lang/en_gb.lang @@ -57,6 +57,7 @@ ffmpeg_preview_optimized = Optimized preview ffmpeg_preview_optimized_button = Preview (Web) ffmpeg_preview_modal_title = Video preview ffmpeg_preview_modal_close = Close +ffmpeg_preview_loop = Loop ffmpeg_show_log = Show log ffmpeg_hide_log = Hide log ffmpeg_convert_this_video = Convert this video diff --git a/pages/mediapool.ffmpeg.main.php b/pages/mediapool.ffmpeg.main.php index a3ab41a..dd20f45 100644 --- a/pages/mediapool.ffmpeg.main.php +++ b/pages/mediapool.ffmpeg.main.php @@ -261,6 +261,9 @@
diff --git a/pages/mediapool.ffmpeg.trimmer.php b/pages/mediapool.ffmpeg.trimmer.php index 7b41960..d276c28 100644 --- a/pages/mediapool.ffmpeg.trimmer.php +++ b/pages/mediapool.ffmpeg.trimmer.php @@ -174,6 +174,9 @@ +
@@ -298,6 +301,9 @@