Update playlist detail UI (priority/schedule/add-item)
This commit is contained in:
@@ -319,46 +319,33 @@
|
||||
<input type="hidden" name="item_type" id="item_type" value="image" />
|
||||
<input type="hidden" name="crop_mode" id="crop_mode" value="16:9" />
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Title (optional)</label>
|
||||
<input class="form-control" name="title" />
|
||||
</div>
|
||||
{# Step 1: pick type, then show relevant inputs #}
|
||||
<div id="step-type" class="step active">
|
||||
<div class="text-muted small mb-2">Select a slide type.</div>
|
||||
<div class="mb-2">
|
||||
<div class="btn-group w-100" role="group" aria-label="Slide type">
|
||||
<input type="radio" class="btn-check" name="item_type_choice" id="type-image" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="type-image">Image</label>
|
||||
|
||||
<div class="mb-2" id="duration-group">
|
||||
<label class="form-label">Duration (seconds, for images/webpages/YouTube)</label>
|
||||
<input class="form-control" type="number" name="duration_seconds" value="10" min="1" />
|
||||
</div>
|
||||
<input type="radio" class="btn-check" name="item_type_choice" id="type-webpage" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="type-webpage">Webpage</label>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<div class="btn-group w-100" role="group" aria-label="Item type">
|
||||
<input type="radio" class="btn-check" name="item_type_choice" id="type-image" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="type-image">Image</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="item_type_choice" id="type-webpage" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="type-webpage">Webpage</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="item_type_choice" id="type-youtube" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="type-youtube">YouTube</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="item_type_choice" id="type-video" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="type-video">Video</label>
|
||||
<input type="radio" class="btn-check" name="item_type_choice" id="type-video" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="type-video">Video</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="crop-mode-group">
|
||||
<label class="form-label">Image crop</label>
|
||||
<div class="btn-group w-100" role="group" aria-label="Crop mode">
|
||||
<input type="radio" class="btn-check" name="crop_mode_choice" id="crop-16-9" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="crop-16-9">16:9 (landscape)</label>
|
||||
{# Step 2 (only for video): choose upload vs YouTube #}
|
||||
<div id="step-video-source" class="step">
|
||||
<div class="text-muted small mb-2">Choose how you want to add the video.</div>
|
||||
<div class="btn-group w-100" role="group" aria-label="Video source">
|
||||
<input type="radio" class="btn-check" name="video_source_choice" id="video-source-upload" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="video-source-upload">Upload video</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="crop_mode_choice" id="crop-9-16" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="crop-9-16">9:16 (portrait)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="crop_mode_choice" id="crop-none" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="crop-none">No crop</label>
|
||||
<input type="radio" class="btn-check" name="video_source_choice" id="video-source-youtube" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="video-source-youtube">YouTube</label>
|
||||
</div>
|
||||
<div class="text-muted small mt-1">Cropping is optional. If enabled, we center-crop to the chosen aspect ratio.</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -393,8 +380,33 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="step-select" class="step active">
|
||||
<div class="text-muted small mb-2">Select or upload your media. If you upload an image, you’ll crop it next.</div>
|
||||
<div id="step-input" class="step">
|
||||
<div class="text-muted small mb-2" id="step-input-hint">Fill in the details for the new slide.</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Title (optional)</label>
|
||||
<input class="form-control" name="title" />
|
||||
</div>
|
||||
|
||||
<div class="mb-2" id="duration-group">
|
||||
<label class="form-label">Duration (seconds, for images/webpages/YouTube)</label>
|
||||
<input class="form-control" type="number" name="duration_seconds" value="10" min="1" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="crop-mode-group">
|
||||
<label class="form-label">Image crop</label>
|
||||
<div class="btn-group w-100" role="group" aria-label="Crop mode">
|
||||
<input type="radio" class="btn-check" name="crop_mode_choice" id="crop-16-9" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="crop-16-9">16:9 (landscape)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="crop_mode_choice" id="crop-9-16" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="crop-9-16">9:16 (portrait)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="crop_mode_choice" id="crop-none" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="crop-none">No crop</label>
|
||||
</div>
|
||||
<div class="text-muted small mt-1">Cropping is optional. If enabled, we center-crop to the chosen aspect ratio.</div>
|
||||
</div>
|
||||
|
||||
{# Image section #}
|
||||
<div id="section-image" class="item-type-section">
|
||||
@@ -433,7 +445,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# YouTube section #}
|
||||
{# YouTube section (also used as video source) #}
|
||||
<div id="section-youtube" class="item-type-section d-none">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">YouTube URL</label>
|
||||
@@ -445,9 +457,9 @@
|
||||
<div class="text-muted small">Tip: set a duration; YouTube embeds will advance after that time.</div>
|
||||
</div>
|
||||
|
||||
{# Video section #}
|
||||
{# Video upload section #}
|
||||
<div id="section-video" class="item-type-section d-none">
|
||||
<label class="form-label">Video</label>
|
||||
<label class="form-label">Video upload</label>
|
||||
<div id="video-dropzone" class="dropzone mb-2">
|
||||
<div><strong>Drag & drop</strong> a video here</div>
|
||||
<div class="text-muted small">or click to select a file</div>
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user