ghassan c160242dbc WIP: storage-fix-local-nas work before playlist controls feature
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:15:20 +03:00

1523 lines
56 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- Edit Video Modal - Cute Staged Pop-up -->
<div class="modal fade" id="editVideoModal" tabindex="-1" aria-labelledby="editVideoModalLabel" aria-hidden="true"
data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content edit-modal-content">
<!-- Header -->
<div class="modal-header edit-modal-header">
<div class="d-flex align-items-center gap-3">
<div class="edit-icon-wrapper">
<i class="bi bi-pencil-fill"></i>
</div>
<div>
<h5 class="modal-title" id="editVideoModalLabel">Edit Video</h5>
<span class="edit-subtitle">Update your video</span>
</div>
</div>
<button type="button" class="btn-close btn-close-white" onclick="closeEditVideoModal()"
aria-label="Close"></button>
</div>
<!-- Body -->
<div class="modal-body edit-modal-body">
<!-- Progress Steps -->
<div class="edit-steps">
<div class="edit-step active" data-step="1">
<div class="step-circle">
<span>1</span>
<i class="bi bi-check-lg"></i>
</div>
<span class="step-label">Details</span>
</div>
<div class="step-line"></div>
<div class="edit-step" data-step="2">
<div class="step-circle">
<span>2</span>
<i class="bi bi-check-lg"></i>
</div>
<span class="step-label">Thumbnail</span>
</div>
<div class="step-line"></div>
<div class="edit-step" data-step="3">
<div class="step-circle">
<span>3</span>
<i class="bi bi-check-lg"></i>
</div>
<span class="step-label">Privacy</span>
</div>
</div>
<!-- Form -->
<form id="edit-video-form-modal" enctype="multipart/form-data">
@csrf
@method('PUT')
<!-- Step 1: Details -->
<div class="edit-step-content active" data-step="1">
<!-- Current Thumbnail Preview -->
<div class="current-thumbnail-section">
<label class="form-label">
<i class="bi bi-image"></i> Current Thumbnail
</label>
<div class="current-thumbnail-wrapper" id="current-thumbnail-wrapper">
<img id="current-thumbnail-preview" src="" alt="Current thumbnail"
class="current-thumbnail-img">
<div class="thumbnail-placeholder" id="thumbnail-placeholder">
<i class="bi bi-card-image"></i>
<span>No thumbnail</span>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-card-heading"></i> Title *
</label>
<input type="text" name="title" id="edit-video-title" required class="form-input"
placeholder="Give your video a catchy title">
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-text-paragraph"></i> Description
</label>
<textarea name="description" id="edit-video-description" rows="3" class="form-textarea"
placeholder="Tell viewers about your video"></textarea>
</div>
<div class="form-group">
<label class="form-label">
<i class="bi bi-collection-play"></i> Video Type
</label>
<div class="video-type-options">
<label class="video-type-option" id="type-generic">
<input type="radio" name="type" value="generic" checked>
<div class="video-type-content">
<i class="bi bi-film"></i>
<span>Generic</span>
</div>
</label>
<label class="video-type-option" id="type-music">
<input type="radio" name="type" value="music">
<div class="video-type-content">
<i class="bi bi-music-note"></i>
<span>Music</span>
</div>
</label>
<label class="video-type-option" id="type-match">
<input type="radio" name="type" value="match">
<div class="video-type-content">
<i class="bi bi-trophy"></i>
<span>Match</span>
</div>
</label>
</div>
</div>
<div class="step-navigation">
<div></div>
<button type="button" class="action-btn action-btn-primary btn-next" onclick="editNextStep(2)">
<span>Next Step</span> <i class="bi bi-arrow-right"></i>
</button>
</div>
</div>
<!-- Step 2: Thumbnail / Slides -->
<div class="edit-step-content" data-step="2">
<!-- Standard thumbnail (non-audio) -->
<div id="edit-thumb-section" class="form-group">
<label class="form-label">
<i class="bi bi-camera"></i> Change Thumbnail
</label>
<div id="edit-thumbnail-dropzone" class="dropzone-modal">
<input type="file" name="thumbnail" id="edit-thumbnail-input" accept="image/*">
<div id="edit-thumbnail-default">
<div class="dropzone-icon">
<i class="bi bi-card-image"></i>
</div>
<p class="dropzone-title">Click to select or drag thumbnail</p>
<p class="dropzone-hint">JPG, PNG, WebP up to 20MB</p>
</div>
<div id="edit-thumbnail-info" class="file-info-modal">
<div class="file-preview thumbnail-preview">
<img id="edit-thumbnail-preview-img" src="" alt="Thumbnail preview">
</div>
<div class="file-details">
<p class="filename" id="edit-thumbnail-filename"></p>
<p id="edit-thumbnail-filesize"></p>
</div>
<button type="button" class="btn-remove-file" onclick="removeEditThumbnail(event)">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
<!-- Slides manager (audio tracks) -->
<div id="edit-slides-section" style="display:none;">
<label class="form-label"><i class="bi bi-images"></i> Cover Slides
<span style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-left:6px;">Drag to reorder · click × to remove</span>
</label>
<!-- Hidden inputs carrying order + new files -->
<input type="hidden" name="slides_order" id="edit-slides-order" value="[]">
<input type="file" name="slides_add[]" id="edit-slides-add-input" accept="image/*" multiple style="display:none;">
<div id="edit-slides-strip" style="display:flex;flex-wrap:wrap;gap:8px;min-height:72px;padding:10px;background:rgba(255,255,255,.03);border:2px dashed rgba(255,255,255,.1);border-radius:10px;margin-bottom:8px;">
<!-- Slide items injected by JS -->
<div id="edit-slides-empty" style="display:flex;align-items:center;justify-content:center;width:100%;color:var(--text-secondary);font-size:13px;">No slides yet</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<button type="button" class="action-btn" onclick="document.getElementById('edit-slides-add-input').click()">
<i class="bi bi-plus-lg"></i> <span>Add Images</span>
</button>
<span id="edit-slides-hint" style="font-size:12px;color:var(--text-secondary);"></span>
</div>
</div>
<div class="step-navigation">
<button type="button" class="action-btn btn-prev" onclick="editPrevStep(1)">
<i class="bi bi-arrow-left"></i> <span>Back</span>
</button>
<button type="button" class="action-btn action-btn-primary btn-next" onclick="editNextStep(3)">
<span>Next Step</span> <i class="bi bi-arrow-right"></i>
</button>
</div>
</div>
<!-- Step 3: Privacy -->
<div class="edit-step-content" data-step="3">
<div class="form-group">
<label class="form-label">
<i class="bi bi-shield-lock"></i> Privacy Setting
</label>
<div class="visibility-options-modal">
<label class="visibility-option-modal active" id="visibility-public">
<input type="radio" name="visibility" value="public" checked>
<div class="visibility-content-modal">
<i class="bi bi-globe"></i>
<div class="visibility-text">
<span class="visibility-title">Public</span>
<span class="visibility-desc">Everyone can see this video</span>
</div>
</div>
</label>
<label class="visibility-option-modal" id="visibility-unlisted">
<input type="radio" name="visibility" value="unlisted">
<div class="visibility-content-modal">
<i class="bi bi-link-45deg"></i>
<div class="visibility-text">
<span class="visibility-title">Unlisted</span>
<span class="visibility-desc">Only people with the link</span>
</div>
</div>
</label>
<label class="visibility-option-modal" id="visibility-private">
<input type="radio" name="visibility" value="private">
<div class="visibility-content-modal">
<i class="bi bi-lock"></i>
<div class="visibility-text">
<span class="visibility-title">Private</span>
<span class="visibility-desc">Only you can see</span>
</div>
</div>
</label>
</div>
</div>
<!-- Download Access -->
<div class="form-group" style="margin: 16px 0 0;">
<label class="form-label">
<i class="bi bi-download"></i> Who can download?
</label>
<div class="options-grid" id="edit-download-options-modal">
<label class="option-item" data-download="disabled">
<input type="radio" name="download_access" value="disabled">
<div class="option-content">
<i class="bi bi-slash-circle"></i>
<span>Disabled</span>
</div>
</label>
<label class="option-item" data-download="everyone">
<input type="radio" name="download_access" value="everyone">
<div class="option-content">
<i class="bi bi-globe"></i>
<span>Everyone</span>
</div>
</label>
<label class="option-item" data-download="registered">
<input type="radio" name="download_access" value="registered">
<div class="option-content">
<i class="bi bi-person-check"></i>
<span>Members</span>
</div>
</label>
<label class="option-item" data-download="subscribers">
<input type="radio" name="download_access" value="subscribers">
<div class="option-content">
<i class="bi bi-star"></i>
<span>Subscribers</span>
</div>
</label>
</div>
</div>
<!-- Status Message -->
<div id="edit-status-message" class="status-message-modal"></div>
<div class="step-navigation">
<button type="button" class="action-btn btn-prev" onclick="editPrevStep(2)">
<i class="bi bi-arrow-left"></i> <span>Back</span>
</button>
<button type="submit" class="action-btn action-btn-primary btn-save-changes" id="edit-submit-btn">
<i class="bi bi-check-lg"></i> <span>Save Changes</span>
</button>
</div>
</div>
</form>
{{-- ── Replace File panel (outside main form, separate POST) ── --}}
<div id="replace-file-panel" style="margin-top:24px;padding-top:20px;border-top:1px solid #3a3a3a;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<i class="bi bi-arrow-repeat" style="color:#f87171;font-size:16px;"></i>
<span style="font-size:13px;font-weight:700;color:#f87171;text-transform:uppercase;letter-spacing:.05em;">Replace Media File</span>
</div>
<p style="font-size:12px;color:#888;margin:0 0 14px;line-height:1.5;">
Upload a new file to fix a corrupted or missing video/audio. All views, likes, and comments are preserved.
</p>
<div id="rfl-dropzone" onclick="document.getElementById('rfl-input').click()"
style="border:2px dashed #444;border-radius:10px;padding:20px;text-align:center;cursor:pointer;transition:border-color .15s,background .15s;"
onmouseenter="this.style.borderColor='#ef4444';this.style.background='rgba(239,68,68,.05)'"
onmouseleave="this.style.borderColor='#444';this.style.background='transparent'">
<i class="bi bi-cloud-upload" style="font-size:28px;color:#666;display:block;margin-bottom:8px;"></i>
<span id="rfl-drop-label" style="font-size:13px;color:#888;">Click to choose replacement file</span>
<input type="file" id="rfl-input" accept="video/*,audio/*,.mp4,.webm,.ogg,.mov,.avi,.wmv,.flv,.mkv,.mp3,.m4a,.aac,.wav,.flac,.opus" style="display:none;" onchange="rflFileSelected(this)">
</div>
<div id="rfl-file-info" style="display:none;margin-top:10px;background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:10px 14px;display:none;align-items:center;gap:10px;">
<i class="bi bi-file-earmark-play" style="font-size:22px;color:#ef4444;flex-shrink:0;"></i>
<div style="flex:1;min-width:0;">
<div id="rfl-file-name" style="font-size:13px;font-weight:600;color:#e5e5e5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></div>
<div id="rfl-file-size" style="font-size:11px;color:#888;margin-top:2px;"></div>
</div>
<button type="button" onclick="rflClearFile()" style="background:none;border:none;color:#888;cursor:pointer;font-size:16px;flex-shrink:0;padding:0;"><i class="bi bi-x-lg"></i></button>
</div>
<div id="rfl-status" style="display:none;margin-top:10px;font-size:12px;padding:8px 12px;border-radius:6px;"></div>
<button type="button" id="rfl-submit-btn" onclick="rflSubmit()" disabled
class="action-btn action-btn-danger" style="width:100%;justify-content:center;margin-top:12px;">
<i class="bi bi-arrow-repeat"></i> <span>Replace File</span>
</button>
</div>
</div>
</div>
</div>
</div>
<style>
/* Modal Base Styles */
.edit-modal-content {
background: linear-gradient(145deg, #1e1e1e 0%, #252525 100%);
border: 1px solid #3a3a3a;
border-radius: 24px;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6), 0 0 40px rgba(230, 30, 30, 0.1);
overflow: hidden;
}
/* Cute Staged Animation */
#editVideoModal .modal-dialog {
max-width: 600px;
transform: scale(0.8) translateY(20px);
opacity: 0;
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#editVideoModal.show .modal-dialog {
transform: scale(1) translateY(0);
opacity: 1;
}
#editVideoModal.show .modal-dialog {
animation: editModalBounce 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s both;
}
@keyframes editModalBounce {
0% {
transform: scale(0.9) translateY(10px);
opacity: 0;
}
60% {
transform: scale(1.02) translateY(-5px);
}
100% {
transform: scale(1) translateY(0);
opacity: 1;
}
}
/* Header */
.edit-modal-header {
background: linear-gradient(135deg, #e61e1e 0%, #ff4757 100%);
border-bottom: none;
padding: 20px 24px;
position: relative;
overflow: hidden;
}
.edit-modal-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 100%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
pointer-events: none;
}
.edit-modal-header .btn-close {
position: relative;
z-index: 1;
}
.edit-icon-wrapper {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: white;
backdrop-filter: blur(10px);
}
.edit-modal-header .modal-title {
font-size: 20px;
font-weight: 600;
color: white;
margin: 0;
}
.edit-subtitle {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
}
/* Body */
.edit-modal-body {
padding: 24px;
background: #1a1a1a;
}
/* Progress Steps */
.edit-steps {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 28px;
padding: 0 10px;
}
.edit-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.edit-step .step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: #2a2a2a;
border: 2px solid #404040;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
color: #888;
transition: all 0.3s;
position: relative;
}
.edit-step .step-circle i {
display: none;
font-size: 18px;
color: #4ade80;
}
.edit-step.active .step-circle {
background: linear-gradient(135deg, #e61e1e 0%, #ff4757 100%);
border-color: #e61e1e;
color: white;
box-shadow: 0 4px 15px rgba(230, 30, 30, 0.4);
}
.edit-step.completed .step-circle {
background: #22c55e;
border-color: #22c55e;
color: white;
}
.edit-step.completed .step-circle span {
display: none;
}
.edit-step.completed .step-circle i {
display: block;
}
.step-label {
font-size: 12px;
color: #888;
font-weight: 500;
}
.edit-step.active .step-label {
color: #e61e1e;
}
.step-line {
width: 60px;
height: 2px;
background: #333;
margin: 0 8px;
margin-bottom: 24px;
transition: background 0.3s;
}
.step-line.active {
background: linear-gradient(90deg, #e61e1e, #22c55e);
}
/* Step Content */
.edit-step-content {
display: none;
animation: fadeSlideIn 0.4s ease;
}
.edit-step-content.active {
display: block;
}
@keyframes fadeSlideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Current Thumbnail */
.current-thumbnail-section {
margin-bottom: 20px;
}
.current-thumbnail-wrapper {
width: 100%;
height: 140px;
border-radius: 12px;
overflow: hidden;
background: #151515;
border: 2px dashed #333;
position: relative;
}
.current-thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumbnail-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #555;
}
.thumbnail-placeholder i {
font-size: 32px;
margin-bottom: 8px;
}
.thumbnail-placeholder span {
font-size: 13px;
}
.current-thumbnail-wrapper.has-thumbnail .thumbnail-placeholder {
display: none;
}
.current-thumbnail-wrapper.has-thumbnail .current-thumbnail-img {
display: block;
}
.current-thumbnail-wrapper:not(.has-thumbnail) .current-thumbnail-img {
display: none;
}
/* Form Elements */
.form-group {
margin-bottom: 20px;
}
.form-label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
font-weight: 500;
font-size: 14px;
color: #e5e5e5;
}
.form-label i {
color: #e61e1e;
font-size: 16px;
}
.form-input,
.form-textarea {
width: 100%;
background: #121212;
border: 1px solid #333;
border-radius: 12px;
padding: 14px 16px;
color: #f1f1f1;
font-size: 14px;
transition: all 0.2s;
}
.form-input:focus,
.form-textarea:focus {
outline: none;
border-color: #e61e1e;
box-shadow: 0 0 0 3px rgba(230, 30, 30, 0.15);
}
.form-input::placeholder,
.form-textarea::placeholder {
color: #666;
}
/* Video Type Options */
.video-type-options {
display: flex;
gap: 12px;
}
.video-type-option {
flex: 1;
cursor: pointer;
}
.video-type-option input {
display: none;
}
.video-type-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 12px;
background: #1a1a1a;
border: 2px solid #333;
border-radius: 12px;
transition: all 0.2s;
}
.video-type-option:hover .video-type-content {
border-color: #555;
background: #1f1f1f;
}
.video-type-option.active .video-type-content {
border-color: #e61e1e;
background: rgba(230, 30, 30, 0.1);
}
.video-type-content i {
font-size: 24px;
color: #666;
transition: color 0.2s;
}
.video-type-option.active .video-type-content i {
color: #e61e1e;
}
.video-type-content span {
font-size: 13px;
font-weight: 500;
color: #aaa;
}
.video-type-option.active .video-type-content span {
color: #e5e5e5;
}
/* Dropzone */
.dropzone-modal {
border: 2px dashed #404040;
border-radius: 16px;
padding: 32px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
position: relative;
background: #151515;
}
.dropzone-modal:hover {
border-color: #e61e1e;
background: rgba(230, 30, 30, 0.05);
}
.dropzone-modal.dragover {
border-color: #e61e1e;
background: rgba(230, 30, 30, 0.1);
transform: scale(1.01);
}
.dropzone-modal input[type="file"] {
display: none;
}
.dropzone-icon {
font-size: 48px;
color: #e61e1e;
margin-bottom: 12px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
.dropzone-title {
color: #e5e5e5;
font-size: 16px;
font-weight: 500;
margin: 8px 0;
}
.dropzone-hint {
color: #666;
font-size: 13px;
margin: 0;
}
/* File Info */
.file-info-modal {
display: none;
align-items: center;
gap: 16px;
padding: 16px;
background: #1f1f1f;
border-radius: 12px;
border: 1px solid #333;
}
.file-info-modal.active {
display: flex;
}
.file-preview {
width: 56px;
height: 56px;
background: linear-gradient(135deg, #e61e1e 0%, #ff6b6b 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
overflow: hidden;
}
.file-preview.thumbnail-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-details {
flex: 1;
text-align: left;
}
.file-details .filename {
color: #e5e5e5;
font-weight: 500;
font-size: 14px;
margin: 0 0 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 250px;
}
.file-details p {
color: #888;
font-size: 13px;
margin: 0;
}
.btn-remove-file {
width: 32px;
height: 32px;
border-radius: 50%;
background: #333;
border: none;
color: #888;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.btn-remove-file:hover {
background: #e61e1e;
color: white;
}
/* Visibility Options */
.visibility-options-modal {
display: flex;
flex-direction: column;
gap: 12px;
}
.visibility-option-modal {
cursor: pointer;
}
.visibility-option-modal input {
display: none;
}
.visibility-content-modal {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: #1a1a1a;
border: 2px solid #333;
border-radius: 14px;
transition: all 0.2s;
}
.visibility-option-modal:hover .visibility-content-modal {
border-color: #555;
background: #1f1f1f;
}
.visibility-option-modal.active .visibility-content-modal {
border-color: #e61e1e;
background: rgba(230, 30, 30, 0.1);
}
.visibility-content-modal i {
font-size: 22px;
color: #666;
width: 32px;
text-align: center;
transition: color 0.2s;
}
.visibility-option-modal.active .visibility-content-modal i {
color: #e61e1e;
}
.visibility-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.visibility-title {
font-weight: 500;
font-size: 15px;
color: #e5e5e5;
}
.visibility-desc {
color: #777;
font-size: 13px;
}
/* Step Navigation */
.step-navigation {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid #2a2a2a;
}
.btn-next:disabled,
.btn-save-changes:disabled {
background: #444 !important;
border-color: #444 !important;
cursor: not-allowed;
transform: none;
box-shadow: none;
opacity: 1;
}
/* Status Message */
.status-message-modal {
display: none;
padding: 14px 18px;
border-radius: 10px;
margin-top: 16px;
font-size: 14px;
}
.status-message-modal.success {
display: flex;
align-items: center;
gap: 10px;
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
border: 1px solid rgba(34, 197, 94, 0.3);
}
.status-message-modal.error {
display: flex;
align-items: center;
gap: 10px;
background: rgba(239, 68, 68, 0.15);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.3);
}
/* Slide items in edit modal */
.es-item {
position: relative;
width: 72px;
height: 72px;
border-radius: 8px;
overflow: hidden;
border: 2px solid rgba(255,255,255,.12);
cursor: grab;
flex-shrink: 0;
transition: border-color .2s, transform .15s;
}
.es-item:hover { border-color: #e61e1e; }
.es-item img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
.es-drag-handle {
position: absolute;
top: 2px; left: 2px;
width: 20px; height: 20px;
background: rgba(0,0,0,.55);
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 11px; color: #ccc;
cursor: grab;
}
.es-remove {
position: absolute;
top: 2px; right: 2px;
width: 20px; height: 20px;
background: rgba(0,0,0,.55);
border: none;
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 10px; color: #ccc;
cursor: pointer;
padding: 0;
transition: background .15s, color .15s;
}
.es-remove:hover { background: #e61e1e; color: #fff; }
.es-item.es-dragging { opacity: .35; outline: 2px dashed #e61e1e; }
/* Responsive */
@media (max-width: 576px) {
.edit-modal-body {
padding: 20px 16px;
}
.edit-steps {
transform: scale(0.85);
}
.step-line {
width: 30px;
}
.video-type-options {
flex-direction: column;
}
.step-navigation {
flex-direction: column-reverse;
gap: 12px;
}
.btn-prev,
.btn-next,
.btn-save-changes {
width: 100%;
justify-content: center;
}
}
</style>
<script>
window.currentEditStep = window.currentEditStep ?? 1;
window.totalEditSteps = window.totalEditSteps ?? 3;
window.currentVideoId = window.currentVideoId ?? null;
// Open modal and load video data
function openEditVideoModal(videoId) {
// On mobile redirect to the full edit page
if (window.innerWidth < 992) {
window.location.href = `/videos/${videoId}/edit`;
return;
}
window.currentVideoId = videoId;
// Fetch video data
fetch(`/videos/${videoId}/edit`, {
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const video = data.video;
// Set form values
document.getElementById('edit-video-title').value = video.title || '';
document.getElementById('edit-video-description').value = video.description || '';
// Set thumbnail preview
const thumbnailWrapper = document.getElementById('current-thumbnail-wrapper');
const thumbnailImg = document.getElementById('current-thumbnail-preview');
if (video.thumbnail) {
thumbnailImg.src = video.thumbnail_url;
thumbnailWrapper.classList.add('has-thumbnail');
} else {
thumbnailWrapper.classList.remove('has-thumbnail');
}
// Set video type
const videoType = video.type || 'generic';
document.querySelectorAll('.video-type-option').forEach(opt => {
opt.classList.remove('active');
const radio = opt.querySelector('input[type="radio"]');
if (radio.value === videoType) {
radio.checked = true;
opt.classList.add('active');
}
});
// Set visibility
const visibility = video.visibility || 'public';
document.querySelectorAll('.visibility-option-modal').forEach(opt => {
opt.classList.remove('active');
const radio = opt.querySelector('input[type="radio"]');
if (radio.value === visibility) {
radio.checked = true;
opt.classList.add('active');
}
});
// Set download_access radio cards
const dlValue = video.download_access || 'disabled';
document.querySelectorAll('#edit-download-options-modal .option-item').forEach(opt => {
opt.classList.remove('active');
const radio = opt.querySelector('input[type="radio"]');
if (radio.value === dlValue) {
radio.checked = true;
opt.classList.add('active');
} else {
radio.checked = false;
}
});
// Reset file inputs
document.getElementById('edit-thumbnail-default').style.display = 'block';
document.getElementById('edit-thumbnail-info').classList.remove('active');
document.getElementById('edit-status-message').className = 'status-message-modal';
// Populate slides or thumbnail section
populateEditSlides(video.slides || [], video.is_audio || false);
// Update step 2 label dynamically
const step2Label = document.querySelector('.edit-step[data-step="2"] .step-label');
if (step2Label) step2Label.textContent = video.is_audio ? 'Slides' : 'Thumbnail';
// Reset step to 1
window.currentEditStep = 1;
updateEditStepDisplay();
// Update form action
document.getElementById('edit-video-form-modal').action = `/videos/${videoId}`;
// Show modal
const modal = new bootstrap.Modal(document.getElementById('editVideoModal'));
modal.show();
setTimeout(() => {
document.getElementById('editVideoModal').classList.add('show');
}, 10);
} else {
showToast('Failed to load video data', 'error');
}
})
.catch(error => {
console.error('Error:', error);
showToast('Failed to load video data', 'error');
});
}
function closeEditVideoModal() {
const modalEl = document.getElementById('editVideoModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) {
modal.hide();
}
modalEl.addEventListener('hidden.bs.modal', function() {
modalEl.classList.remove('show');
window.currentVideoId = null;
}, {
once: true
});
}
// Step Navigation
function editNextStep(step) {
// Validate current step before moving
if (step === 2) {
const title = document.getElementById('edit-video-title').value.trim();
if (!title) {
showToast('Please enter a title for your video', 'error');
return;
}
}
window.currentEditStep = step;
updateEditStepDisplay();
}
function editPrevStep(step) {
window.currentEditStep = step;
updateEditStepDisplay();
}
function updateEditStepDisplay() {
// Update step indicators
document.querySelectorAll('.edit-step').forEach((el, index) => {
const stepNum = index + 1;
el.classList.remove('active', 'completed');
if (stepNum === window.currentEditStep) {
el.classList.add('active');
} else if (stepNum < window.currentEditStep) {
el.classList.add('completed');
}
});
// Update step lines
document.querySelectorAll('.step-line').forEach((el, index) => {
if (index < window.currentEditStep - 1) {
el.classList.add('active');
} else {
el.classList.remove('active');
}
});
// Update step content
document.querySelectorAll('.edit-step-content').forEach(el => {
el.classList.remove('active');
if (parseInt(el.dataset.step) === window.currentEditStep) {
el.classList.add('active');
}
});
}
// Thumbnail dropzone handling
const editThumbnailDropzone = document.getElementById('edit-thumbnail-dropzone');
const editThumbnailInput = document.getElementById('edit-thumbnail-input');
if (editThumbnailInput) {
editThumbnailInput.addEventListener('change', function() {
handleEditThumbnailSelect(this);
});
if (editThumbnailDropzone) {
editThumbnailDropzone.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-file')) return;
if (typeof window.openCropperModal_thumb_edit === 'function') {
window.openCropperModal_thumb_edit();
} else {
editThumbnailInput.click();
}
});
editThumbnailDropzone.addEventListener('dragover', (e) => {
e.preventDefault();
editThumbnailDropzone.classList.add('dragover');
});
editThumbnailDropzone.addEventListener('dragleave', () => {
editThumbnailDropzone.classList.remove('dragover');
});
editThumbnailDropzone.addEventListener('drop', (e) => {
e.preventDefault();
editThumbnailDropzone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
const droppedFile = e.dataTransfer.files[0];
if (typeof window.tcPreload_thumb_edit === 'function') {
window.tcPreload_thumb_edit(droppedFile);
window.openCropperModal_thumb_edit();
} else {
editThumbnailInput.files = e.dataTransfer.files;
handleEditThumbnailSelect(editThumbnailInput);
}
}
});
}
}
function handleEditThumbnailSelect(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
document.getElementById('edit-thumbnail-filename').textContent = file.name;
document.getElementById('edit-thumbnail-filesize').textContent = (file.size / 1024 / 1024).toFixed(2) +
' MB';
// Show thumbnail preview
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('edit-thumbnail-preview-img').src = e.target.result;
};
reader.readAsDataURL(file);
document.getElementById('edit-thumbnail-default').style.display = 'none';
document.getElementById('edit-thumbnail-info').classList.add('active');
}
}
function removeEditThumbnail(e) {
e.preventDefault();
e.stopPropagation();
editThumbnailInput.value = '';
document.getElementById('edit-thumbnail-default').style.display = 'block';
document.getElementById('edit-thumbnail-info').classList.remove('active');
}
// ── Slides manager (audio tracks) ────────────────────────────
let editSlidesData = []; // [{id, url}] for existing; {file, url} for new additions
let editDragSrc = null;
function _esSyncOrder() {
const strip = document.getElementById('edit-slides-strip');
const keptIds = editSlidesData.filter(s => s.id).map(s => s.id);
document.getElementById('edit-slides-order').value = JSON.stringify(keptIds);
const count = editSlidesData.length;
document.getElementById('edit-slides-hint').textContent =
count === 1 ? '1 image — static cover' : count + ' images — will crossfade';
}
function editSlidesRefresh() {
const strip = document.getElementById('edit-slides-strip');
const empty = document.getElementById('edit-slides-empty');
strip.querySelectorAll('.es-item').forEach(el => el.remove());
empty.style.display = editSlidesData.length === 0 ? 'flex' : 'none';
editSlidesData.forEach((item, idx) => {
const div = document.createElement('div');
div.className = 'es-item';
div.draggable = true;
div._esData = item; // reference stored on element — avoids stale-index issues during drag
div.innerHTML = `
<div class="es-drag-handle" title="Drag to reorder"><i class="bi bi-grip-vertical"></i></div>
<img src="${item.url}" alt="">
<button type="button" class="es-remove" title="Remove"><i class="bi bi-x-lg"></i></button>
`;
div.querySelector('.es-remove').addEventListener('click', () => {
editSlidesData.splice(editSlidesData.indexOf(div._esData), 1);
editSlidesRefresh();
});
div.addEventListener('dragstart', e => {
editDragSrc = div;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => div.classList.add('es-dragging'), 0);
});
// On dragend: sync editSlidesData from current DOM order, then update inputs
div.addEventListener('dragend', () => {
div.classList.remove('es-dragging');
editDragSrc = null;
const newData = [];
strip.querySelectorAll('.es-item').forEach(el => newData.push(el._esData));
editSlidesData = newData;
_esSyncOrder();
});
// On dragover: just move the element in the DOM — never re-render
div.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (!editDragSrc || editDragSrc === div) return;
const items = [...strip.querySelectorAll('.es-item')];
const srcPos = items.indexOf(editDragSrc);
const tgtPos = items.indexOf(div);
if (srcPos < tgtPos) div.after(editDragSrc);
else div.before(editDragSrc);
});
strip.appendChild(div);
});
_esSyncOrder();
}
// "Add Images" picker
document.getElementById('edit-slides-add-input').addEventListener('change', function() {
Array.from(this.files).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
editSlidesData.push({ file, url: e.target.result });
editSlidesRefresh();
};
reader.readAsDataURL(file);
});
// Reset input so same files can be re-added after removal
this.value = '';
});
function populateEditSlides(slides, isAudio) {
document.getElementById('edit-thumb-section').style.display = isAudio ? 'none' : '';
document.getElementById('edit-slides-section').style.display = isAudio ? '' : 'none';
if (! isAudio) return;
editSlidesData = slides.map(s => ({ id: s.id, url: s.url }));
editSlidesRefresh();
}
// Inject new file inputs into the form on submit (FormData can't handle dynamic files otherwise)
function buildEditSlidesFormData(formData) {
// Remove existing slides_add from formData (the file input may have stale data)
// Instead append each new file individually
const newFiles = editSlidesData.filter(s => s.file);
newFiles.forEach(s => formData.append('slides_add[]', s.file, s.file.name));
return formData;
}
// Video type options
const videoTypeOptions = document.querySelectorAll('.video-type-option');
videoTypeOptions.forEach(option => {
const radio = option.querySelector('input[type="radio"]');
radio.addEventListener('change', function() {
videoTypeOptions.forEach(opt => opt.classList.remove('active'));
option.classList.add('active');
});
});
// Visibility options
const editVisibilityOptions = document.querySelectorAll('.visibility-option-modal');
editVisibilityOptions.forEach(option => {
const radio = option.querySelector('input[type="radio"]');
radio.addEventListener('change', function() {
editVisibilityOptions.forEach(opt => opt.classList.remove('active'));
option.classList.add('active');
});
});
// Download access options
document.querySelectorAll('#edit-download-options-modal .option-item').forEach(item => {
item.querySelector('input[type="radio"]').addEventListener('change', function() {
document.querySelectorAll('#edit-download-options-modal .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
// Form submission
document.getElementById('edit-video-form-modal').addEventListener('submit', function(e) {
e.preventDefault();
let formData = new FormData(this);
// Append any newly added slide files (not captured by FormData from hidden input)
formData = buildEditSlidesFormData(formData);
const submitBtn = document.getElementById('edit-submit-btn');
// Disable button and show loading
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Saving...';
document.getElementById('edit-status-message').className = 'status-message-modal';
fetch(`/videos/${window.currentVideoId}`, {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
document.getElementById('edit-status-message').innerHTML =
'<i class="bi bi-check-circle-fill"></i> ' + data.message;
document.getElementById('edit-status-message').className =
'status-message-modal success';
// Close modal and reload page after short delay
setTimeout(() => {
closeEditVideoModal();
window.location.reload();
}, 1000);
} else {
throw new Error(data.message || 'Update failed');
}
})
.catch(error => {
console.error('Error:', error);
document.getElementById('edit-status-message').innerHTML =
'<i class="bi bi-exclamation-circle-fill"></i> ' + error.message;
document.getElementById('edit-status-message').className = 'status-message-modal error';
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-check-lg"></i> Save Changes';
});
});
// ── Replace File ──────────────────────────────────────────────────────
let _rflFile = null;
const _rflFmtSize = b => b >= 1073741824 ? (b/1073741824).toFixed(1)+' GB' : b >= 1048576 ? (b/1048576).toFixed(1)+' MB' : Math.round(b/1024)+' KB';
function rflFileSelected(input) {
_rflFile = input.files[0] || null;
const info = document.getElementById('rfl-file-info');
if (_rflFile) {
document.getElementById('rfl-file-name').textContent = _rflFile.name;
document.getElementById('rfl-file-size').textContent = _rflFmtSize(_rflFile.size);
document.getElementById('rfl-drop-label').textContent = _rflFile.name;
info.style.display = 'flex';
} else {
info.style.display = 'none';
document.getElementById('rfl-drop-label').textContent = 'Click to choose replacement file';
}
document.getElementById('rfl-submit-btn').disabled = !_rflFile;
document.getElementById('rfl-status').style.display = 'none';
}
function rflClearFile() {
_rflFile = null;
document.getElementById('rfl-input').value = '';
document.getElementById('rfl-file-info').style.display = 'none';
document.getElementById('rfl-drop-label').textContent = 'Click to choose replacement file';
document.getElementById('rfl-submit-btn').disabled = true;
document.getElementById('rfl-status').style.display = 'none';
}
function rflSubmit() {
if (!_rflFile || !window.currentVideoId) return;
const btn = document.getElementById('rfl-submit-btn');
const status = document.getElementById('rfl-status');
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Uploading…';
status.style.display = 'none';
const fd = new FormData();
fd.append('_token', '{{ csrf_token() }}');
fd.append('replacement_file', _rflFile);
fetch(`/videos/${window.currentVideoId}/replace-file`, {
method: 'POST',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
body: fd
})
.then(r => r.json())
.then(data => {
if (data.success) {
status.style.cssText = 'display:block;background:rgba(74,222,128,.1);border:1px solid rgba(74,222,128,.25);color:#4ade80;';
status.innerHTML = '<i class="bi bi-check-circle-fill"></i> ' + data.message;
rflClearFile();
setTimeout(() => location.reload(), 2000);
} else {
status.style.cssText = 'display:block;background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.25);color:#f87171;';
status.textContent = data.message || 'Upload failed. Please try again.';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> <span>Replace File</span>';
}
})
.catch(() => {
status.style.cssText = 'display:block;background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.25);color:#f87171;';
status.textContent = 'Upload failed. Please try again.';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> <span>Replace File</span>';
});
}
</script>
<x-image-cropper
id="thumb_edit"
:width="448"
:height="252"
shape="square"
target-input="edit-thumbnail-input"
preview-img="edit-thumbnail-preview-img"
output-width="1280"
title="Crop Thumbnail"
/>