From 47aca9d64d63f6c783609c49b3412dee99f09f2b Mon Sep 17 00:00:00 2001 From: bramval Date: Sun, 25 Jan 2026 13:50:22 +0100 Subject: [PATCH] Update playlist detail UI (priority/schedule/add-item) --- app/templates/company/playlist_detail.html | 338 +++++++++++++++------ 1 file changed, 245 insertions(+), 93 deletions(-) diff --git a/app/templates/company/playlist_detail.html b/app/templates/company/playlist_detail.html index 92dae52..be1ea52 100644 --- a/app/templates/company/playlist_detail.html +++ b/app/templates/company/playlist_detail.html @@ -319,46 +319,33 @@ -
- - -
+ {# Step 1: pick type, then show relevant inputs #} +
+
Select a slide type.
+
+
+ + -
- - -
+ + -
- -
- - - - - - - - - - - + + +
-
- -
- - + {# Step 2 (only for video): choose upload vs YouTube #} +
+
Choose how you want to add the video.
+
+ + - - - - - + +
-
Cropping is optional. If enabled, we center-crop to the chosen aspect ratio.
-
-
Select or upload your media. If you upload an image, you’ll crop it next.
+
+
Fill in the details for the new slide.
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + + + + + + +
+
Cropping is optional. If enabled, we center-crop to the chosen aspect ratio.
+
{# Image section #}
@@ -433,7 +445,7 @@
- {# YouTube section #} + {# YouTube section (also used as video source) #}
@@ -445,9 +457,9 @@
Tip: set a duration; YouTube embeds will advance after that time.
- {# Video section #} + {# Video upload section #}
- +
Drag & drop a video here
or click to select a file
@@ -636,53 +648,87 @@ const sectionYoutube = document.getElementById('section-youtube'); const sectionVideo = document.getElementById('section-video'); - const stepSelect = document.getElementById('step-select'); + const stepType = document.getElementById('step-type'); + const stepVideoSource = document.getElementById('step-video-source'); + const stepInput = document.getElementById('step-input'); const stepCrop = document.getElementById('step-crop'); const backBtn = document.getElementById('add-item-back'); const errorEl = document.getElementById('add-item-error'); + const stepInputHint = document.getElementById('step-input-hint'); function setError(msg) { if (!errorEl) return; errorEl.textContent = (msg || '').trim(); } + let currentStep = 'type'; + + function updatePrimaryButton() { + // Primary button acts as Next in early steps, and Add in the final steps. + if (!submitBtn) return; + const isNextStep = (currentStep === 'type' || currentStep === 'video-source'); + submitBtn.textContent = isNextStep ? 'Next' : 'Add'; + } + function showStep(which) { - stepSelect?.classList.toggle('active', which === 'select'); + currentStep = which; + stepType?.classList.toggle('active', which === 'type'); + stepVideoSource?.classList.toggle('active', which === 'video-source'); + stepInput?.classList.toggle('active', which === 'input'); stepCrop?.classList.toggle('active', which === 'crop'); - const isCrop = which === 'crop'; - backBtn.disabled = !isCrop; + // Back is enabled for all steps except the first. + backBtn.disabled = (which === 'type'); - // For image: allow Add only in crop step (so we always crop if image) - if (typeHidden.value === 'image') { - submitBtn.disabled = !isCrop; + // Enable Next for the initial steps. + if (which === 'type' || which === 'video-source') { + submitBtn.disabled = false; + updatePrimaryButton(); + return; } + + // For input/crop steps: image requires crop step before enabling Add. + if (typeHidden.value === 'image') { + submitBtn.disabled = (which !== 'crop'); + } else { + submitBtn.disabled = false; + } + + updatePrimaryButton(); + } + + function videoSource() { + return document.getElementById('video-source-youtube')?.checked ? 'youtube' : 'upload'; } function setType(t) { + // For this UI, "video" is a top-level type, but it can map to item_type=video OR item_type=youtube. typeHidden.value = t; setError(''); - sectionImage.classList.toggle('d-none', t !== 'image'); - sectionWebpage.classList.toggle('d-none', t !== 'webpage'); - sectionYoutube.classList.toggle('d-none', t !== 'youtube'); - sectionVideo.classList.toggle('d-none', t !== 'video'); - // duration applies to image/webpage/youtube. Video plays until ended. - durationGroup.classList.toggle('d-none', t === 'video'); - cropModeGroup?.classList.toggle('d-none', t !== 'image'); - submitBtn.disabled = false; - submitBtn.title = ''; - if (t !== 'image') { - destroyCropper(); - showStep('select'); - backBtn.disabled = true; + // Visible section is decided by type + (video source) + const vs = (t === 'video') ? videoSource() : null; + const effectiveType = (t === 'video' && vs === 'youtube') ? 'youtube' : t; + typeHidden.value = effectiveType; + + sectionImage.classList.toggle('d-none', effectiveType !== 'image'); + sectionWebpage.classList.toggle('d-none', effectiveType !== 'webpage'); + sectionYoutube.classList.toggle('d-none', effectiveType !== 'youtube'); + sectionVideo.classList.toggle('d-none', effectiveType !== 'video'); + + // duration applies to image/webpage/youtube. Video upload plays until ended. + durationGroup.classList.toggle('d-none', effectiveType === 'video'); + cropModeGroup?.classList.toggle('d-none', effectiveType !== 'image'); + + if (stepInputHint) { + if (effectiveType === 'image') stepInputHint.textContent = 'Select an image. After selecting, you\'ll crop it.'; + else if (effectiveType === 'webpage') stepInputHint.textContent = 'Enter a webpage URL.'; + else if (effectiveType === 'youtube') stepInputHint.textContent = 'Paste a YouTube URL.'; + else stepInputHint.textContent = 'Upload a video file.'; } - // For images we enforce crop step before allowing submit. - if (t === 'image') { - submitBtn.disabled = true; - backBtn.disabled = true; - } + // Reset cropper when leaving image. + if (effectiveType !== 'image') destroyCropper(); } function currentCropMode() { @@ -705,10 +751,39 @@ } } - document.getElementById('type-image')?.addEventListener('change', () => setType('image')); - document.getElementById('type-webpage')?.addEventListener('change', () => setType('webpage')); - document.getElementById('type-youtube')?.addEventListener('change', () => setType('youtube')); - document.getElementById('type-video')?.addEventListener('change', () => setType('video')); + document.getElementById('type-image')?.addEventListener('change', () => { + setType('image'); + // Stay on type step; user clicks Next. + showStep('type'); + }); + document.getElementById('type-webpage')?.addEventListener('change', () => { + setType('webpage'); + // Stay on type step; user clicks Next. + showStep('type'); + }); + document.getElementById('type-video')?.addEventListener('change', () => { + // We show an intermediate step for video so user chooses upload vs YouTube. + // Keep item_type unset until that choice is made. + setError(''); + destroyCropper(); + // Hide crop/duration while selecting source (they depend on source). + cropModeGroup?.classList.add('d-none'); + durationGroup?.classList.add('d-none'); + showStep('video-source'); + }); + + document.getElementById('video-source-upload')?.addEventListener('change', () => { + // effective type becomes "video" + setType('video'); + // Stay on source step; user clicks Next. + showStep('video-source'); + }); + document.getElementById('video-source-youtube')?.addEventListener('change', () => { + // effective type becomes "youtube" + setType('video'); + // Stay on source step; user clicks Next. + showStep('video-source'); + }); // ------------------------- // Image: drag/drop + crop @@ -754,7 +829,7 @@ cropStatus.textContent = ''; if (imageSelectStatus) imageSelectStatus.textContent = `Selected: ${file.name}`; - // Move to crop step after image selection (requested behavior) + // Move to crop step after image selection showStep('crop'); // Wait for image to be ready @@ -991,15 +1066,7 @@ } // Reset modal state + close - form.reset(); - typeHidden.value = 'image'; - document.getElementById('type-image')?.click(); - if (cropModeHidden) cropModeHidden.value = '16:9'; - document.getElementById('crop-16-9')?.click(); - destroyCropper(); - showStep('select'); - submitBtn.disabled = true; - resetVideoProgress(); + resetModalState(); modal?.hide(); } @@ -1098,32 +1165,87 @@ if (t === 'webpage') { // Keep preview behavior schedulePreview(); - } else { - // Hide webpage preview if not active - preview?.classList.add('d-none'); - if (iframe) iframe.src = 'about:blank'; - if (openLink) openLink.href = '#'; + return; } + + // Hide webpage preview if not active + preview?.classList.add('d-none'); + if (iframe) iframe.src = 'about:blank'; + if (openLink) openLink.href = '#'; } - // Set initial state - setType('image'); - if (cropModeHidden) cropModeHidden.value = '16:9'; - showStep('select'); - syncEnabledInputs(); - updateCropHint(); + function resetModalState() { + setError(''); + try { form.reset(); } catch (e) {} + destroyCropper(); + + // Default selections (without triggering change handlers) + const typeImage = document.getElementById('type-image'); + const typeWebpage = document.getElementById('type-webpage'); + const typeVideo = document.getElementById('type-video'); + if (typeImage) typeImage.checked = true; + if (typeWebpage) typeWebpage.checked = false; + if (typeVideo) typeVideo.checked = false; + + const vsUpload = document.getElementById('video-source-upload'); + const vsYoutube = document.getElementById('video-source-youtube'); + if (vsUpload) vsUpload.checked = true; + if (vsYoutube) vsYoutube.checked = false; + + if (cropModeHidden) cropModeHidden.value = '16:9'; + document.getElementById('crop-16-9')?.click(); + + // Set UI for default type, but start at type selection step + setType('image'); + showStep('type'); + syncEnabledInputs(); + updateCropHint(); + + // Also reset video upload progress UI if present + try { + const bar = document.getElementById('video-upload-progress-bar'); + if (bar) { + bar.style.width = '0%'; + bar.setAttribute('aria-valuenow', '0'); + } + document.getElementById('video-upload-progress-text') && (document.getElementById('video-upload-progress-text').textContent = 'Uploading…'); + document.getElementById('video-upload-progress')?.classList.add('d-none'); + } catch (e) {} + } + + // Initialize modal state once on page load + resetModalState(); // Modal open openBtn?.addEventListener('click', () => { + // Always start from the beginning. + resetModalState(); modal?.show(); }); - // Back button: only relevant for image crop step + // Back button: stepwise navigation backBtn?.addEventListener('click', () => { - if (typeHidden.value === 'image') { - showStep('select'); + if (currentStep === 'crop') { + // Going back from crop returns to input for image + showStep('input'); submitBtn.disabled = true; - backBtn.disabled = true; + return; + } + + if (currentStep === 'input') { + // If top-level selected is video, go back to video source selection. + const isTopLevelVideo = document.getElementById('type-video')?.checked; + if (isTopLevelVideo) { + showStep('video-source'); + } else { + showStep('type'); + } + return; + } + + if (currentStep === 'video-source') { + showStep('type'); + return; } }); @@ -1165,13 +1287,43 @@ document.getElementById('crop-none')?.addEventListener('change', () => setCropMode('none')); // Whenever type changes, keep enabled inputs in sync - ['type-image','type-webpage','type-youtube','type-video'].forEach((id) => { + ['type-image','type-webpage','type-video','video-source-upload','video-source-youtube'].forEach((id) => { document.getElementById(id)?.addEventListener('change', syncEnabledInputs); }); // Add button submitBtn?.addEventListener('click', async () => { try { + // Multi-step behavior: + // - Type step: Next -> (video ? source step : input step) + // - Video source step: Next -> input step + // - Input/crop: Add -> submit + if (currentStep === 'type') { + const isVideo = document.getElementById('type-video')?.checked; + const isWebpage = document.getElementById('type-webpage')?.checked; + + if (isVideo) { + // Hide crop/duration while selecting source. + cropModeGroup?.classList.add('d-none'); + durationGroup?.classList.add('d-none'); + showStep('video-source'); + return; + } + + setType(isWebpage ? 'webpage' : 'image'); + showStep('input'); + syncEnabledInputs(); + return; + } + + if (currentStep === 'video-source') { + // Apply the chosen source (upload vs YouTube) and continue. + setType('video'); + showStep('input'); + syncEnabledInputs(); + return; + } + await submitViaAjax(); } catch (err) { console.warn(err);