diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..391209b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# 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, 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 96b4d0d..941ec07 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.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) @@ -51,10 +53,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 @@ -63,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 @@ -230,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/css/style.css b/assets/css/style.css index edf3245..e4f18ff 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1,309 +1,543 @@ -.video-item { - padding: 10px; - border-bottom: 1px solid rgba(var(--rex-color-border-text-rgb, 238, 238, 238), .2); +.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; + 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); +} + +.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; + 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; +} + +.ffmpeg-converter-ui .video-file-cell { display: flex; flex-direction: column; - margin-bottom: 10px; - transition: background-color 0.2s ease; } -.video-meta { +.ffmpeg-converter-ui .video-title { + padding: 0 14px; + color: var(--ffmpeg-muted); + font-size: 12px; + margin-top: -2px; +} + +.ffmpeg-converter-ui .video-meta { display: flex; - color: var(--rex-color-text-muted, #888); - font-size: 0.85em; - margin-top: 5px; - margin-left: 20px; + flex-wrap: wrap; + gap: 8px 14px; + padding: 7px 14px 10px; + color: var(--ffmpeg-muted); + font-size: 12px; +} + +.ffmpeg-converter-ui .video-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 0 14px 12px; +} + +.ffmpeg-converter-ui .video-actions-group { + display: inline-flex; + flex-wrap: wrap; + gap: 6px; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; } -.video-size, .video-date { - margin-right: 15px; +.ffmpeg-converter-ui .video-actions-group-primary { + margin-right: 6px; } -.optimized { - background-color: rgba(var(--rex-color-background-rgb, 248, 248, 248), .1); - border-radius: 5px; - padding: 12px; +.ffmpeg-converter-ui .video-actions .btn { + margin: 0; + border-radius: 8px; + font-weight: 600; } -.processing { - background-color: rgba(var(--rex-color-background-rgb, 240, 247, 255), .15); - border-radius: 5px; +.ffmpeg-converter-ui .video-actions .btn.btn-xs { + padding: 6px 10px; } -.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 .ffmpeg-start-conversion { + min-width: 200px; } -.compression-rate { +.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-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; } -.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 { + 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-bar { - background-color: var(--rex-color-brand, #3498db); +.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); } -.conversion-details { +.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); +} + +.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 */ +.ffmpeg-converter-ui .spinner .bounce1 { + animation-delay: -0.32s; +} +.ffmpeg-converter-ui .spinner .bounce2 { + animation-delay: -0.16s; +} -/* Smooth Transitions */ -.video-item, .progress-bar, .spinner > div, .btn { - transition: all 0.3s ease; +@keyframes sk-bouncedelay { + 0%, + 80%, + 100% { + transform: scale(0); + } + + 40% { + transform: scale(1); + } } -/* 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-preview-video { + width: 100%; + max-height: 70vh; + background: #000; + border-radius: 8px; } -/* 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; - } +.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; +} - /* 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); +@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) .optimized { - background-color: rgba(255, 255, 255, 0.05); + .ffmpeg-converter-ui .video-item { + display: grid; + grid-template-columns: 2fr 1.2fr 2fr 1.2fr; + gap: 12px; + align-items: start; + padding: 12px 14px; } - body:not(.rex-theme-dark) .processing { - background-color: rgba(53, 152, 219, 0.15); + .ffmpeg-converter-ui .video-file-cell { + grid-column: 1; + grid-row: 1; + align-self: start; } - body:not(.rex-theme-dark) .already-converted { - background-color: rgba(92, 184, 92, 0.1); + .ffmpeg-converter-ui .video-head { + grid-column: 1; + grid-row: 1; + padding: 0; } - body:not(.rex-theme-dark) .active-video { - background-color: rgba(53, 152, 219, 0.15); + .ffmpeg-converter-ui .video-title { + grid-column: 1; + grid-row: 1; + padding: 0; + margin-top: 28px; + align-self: start; } - 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-file-date { + grid-column: 1; + grid-row: 1; + padding: 0; + margin-top: 18px; + align-self: start; } - body:not(.rex-theme-dark) .progress { - background-color: rgba(255, 255, 255, 0.1); + .ffmpeg-converter-ui .video-meta { + grid-column: 2; + grid-row: 1; + padding: 0; + display: grid; + gap: 6px; + align-content: start; + align-self: start; + margin-top: 4px; } - body:not(.rex-theme-dark) .video-item:hover { - background-color: rgba(255, 255, 255, 0.05); + .ffmpeg-converter-ui .video-actions { + grid-column: 3; + grid-row: 1; + padding: 0; + align-self: start; + margin-top: 4px; } -} -/* 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; + grid-row: 1; + margin: 0; + padding: 8px; + min-height: 0; + display: flex; + align-items: flex-start; + align-self: start; + margin-top: 2px; + } -.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-file-date, + .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/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/script.js b/assets/js/script.js index 951fd3e..cf9e786 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 @@ -325,16 +392,14 @@ }); } - $(document).ready(function () { + function initFfmpegConverter() { // 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,16 +426,64 @@ 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.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); + 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); }); }); + $(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); @@ -410,9 +523,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/assets/js/trimmer.js b/assets/js/trimmer.js new file mode 100644 index 0000000..eea3653 --- /dev/null +++ b/assets/js/trimmer.js @@ -0,0 +1,343 @@ +(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.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; + } + + 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(); + }); + + $(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(); + + if (!selectionPlaybackActive) { + return; + } + + var end = toNumber($('#end_time').val()); + if (video.currentTime >= end) { + restartSelectionPlayback(); + } + }); + + 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]; + 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(); + }); + } + + function initFfmpegTrimmer() { + setupEditor(); + setupListPreview(); + } + + $(document).on('rex:ready', function () { + initFfmpegTrimmer(); + }); + + $(document).ready(function () { + initFfmpegTrimmer(); + }); +})(jQuery); diff --git a/boot.php b/boot.php index dc20e21..905c2b4 100644 --- a/boot.php +++ b/boot.php @@ -16,8 +16,37 @@ 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); + } + + $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/lang/de_de.lang b/lang/de_de.lang index c9954b9..293b851 100644 --- a/lang/de_de.lang +++ b/lang/de_de.lang @@ -51,6 +51,26 @@ 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_preview_loop = Schleife +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_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 874805b..95f4be8 100644 --- a/lang/en_gb.lang +++ b/lang/en_gb.lang @@ -51,6 +51,26 @@ 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_preview_loop = Loop +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_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/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..dd20f45 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 .= '