// Yoto Up Covers Web App class YotoCoversApp { constructor() { this.images = []; this.currentTemplate = 'classic'; this.currentTitleStyle = 'folded'; // Template presets - each defines a set of feature defaults this.templates = { classic: { backgroundStyle: 'solid', fontFamily: 'Arial', fontWeight: 'normal', footerColor: '#666666' }, frozen: { backgroundStyle: 'gradient', fontFamily: 'Arial', fontWeight: 'normal', footerColor: '#4a90e2' }, minimal: { backgroundStyle: 'solid', fontFamily: 'Arial', fontWeight: 'normal', footerColor: '#999999' }, vintage: { backgroundStyle: 'gradient', fontFamily: 'Georgia', fontWeight: 'normal', footerColor: '#654321' }, comic: { backgroundStyle: 'comic', fontFamily: 'Comic Sans MS', fontWeight: 'bold', footerColor: '#ff6b6b', fullBleed: false, panelMode: true, titlePanel: { borderWidth: 2, rotation: -2, expand: true }, imagePanel: { borderWidth: 3, rotation: 1 }, footerPanel: { borderWidth: 2, rotation: 3 } }, polaroid: { backgroundStyle: 'polaroid', fontFamily: 'Arial', fontWeight: 'normal', footerColor: '#666666', fullBleed: false, showFooter: false, imagePosition: 'center' } }; this.printLayout = { cardsPerRow: 2, cardsPerColumn: 5, pageMargin: 10, cardSpacing: 5, cardFit: 0 }; this.templateSettings = { title: '', footer: '', accentColor: '#f1c40f', titleColor: '#111111', fullBleed: true, titleShadow: false, titleFontSize: null, footerFontSize: null }; this.previewZoom = { level: 1.0, min: 0.25, max: 3.0, step: 0.25 }; this.isFullscreen = false; this.cardVariations = []; this.activeCardIndex = -1; // Currently selected card for editing this.init(); } init() { this.bindEvents(); this.updateUI(); this.updateCardList(); } bindEvents() { // Print layout settings document.getElementById('cards-per-row').addEventListener('change', (e) => { this.printLayout.cardsPerRow = parseInt(e.target.value); this.generatePreview(); }); document.getElementById('cards-per-column').addEventListener('change', (e) => { this.printLayout.cardsPerColumn = parseInt(e.target.value); this.generatePreview(); }); document.getElementById('page-margin').addEventListener('input', (e) => { this.printLayout.pageMargin = parseInt(e.target.value); this.generatePreview(); }); document.getElementById('card-spacing').addEventListener('input', (e) => { this.printLayout.cardSpacing = parseInt(e.target.value); this.generatePreview(); }); document.getElementById('global-card-fit').addEventListener('change', (e) => { this.printLayout.cardFit = parseFloat(e.target.value); this.generatePreview(); }); // Template settings - REMOVED: Now per-card only // document.getElementById('template-select').addEventListener('change', (e) => { // this.currentTemplate = e.target.value; // this.generatePreview(); // }); // document.getElementById('title-input').addEventListener('input', (e) => { // this.templateSettings.title = e.target.value; // this.generatePreview(); // }); // document.getElementById('footer-input').addEventListener('input', (e) => { // this.templateSettings.footer = e.target.value; // this.generatePreview(); // }); // Title style selection - REMOVED: Now per-card only // document.querySelectorAll('.title-style-option').forEach(option => { // option.addEventListener('click', () => { // document.querySelectorAll('.title-style-option').forEach(opt => opt.classList.remove('selected')); // option.classList.add('selected'); // this.currentTitleStyle = option.dataset.style; // this.generatePreview(); // }); // }); // Color pickers - REMOVED: Now per-card only // document.getElementById('accent-color').addEventListener('input', (e) => { // this.templateSettings.accentColor = e.target.value; // document.getElementById('accent-color-text').value = e.target.value; // this.generatePreview(); // }); // document.getElementById('title-color').addEventListener('input', (e) => { // this.templateSettings.titleColor = e.target.value; // document.getElementById('title-color-text').value = e.target.value; // this.generatePreview(); // }); // Checkboxes - REMOVED: Now per-card only // document.getElementById('full-bleed').addEventListener('change', (e) => { // this.templateSettings.fullBleed = e.target.checked; // this.generatePreview(); // }); // document.getElementById('title-shadow').addEventListener('change', (e) => { // this.templateSettings.titleShadow = e.target.checked; // this.generatePreview(); // }); // Image upload const imageUpload = document.getElementById('image-upload'); const fileInputLabel = document.querySelector('.file-input-label'); imageUpload.addEventListener('change', (e) => this.handleImageUpload(e.target.files)); imageUpload.addEventListener('dragover', (e) => { e.preventDefault(); fileInputLabel.classList.add('dragover'); }); imageUpload.addEventListener('dragleave', (e) => { e.preventDefault(); fileInputLabel.classList.remove('dragover'); }); imageUpload.addEventListener('drop', (e) => { e.preventDefault(); fileInputLabel.classList.remove('dragover'); this.handleImageUpload(e.dataTransfer.files); }); // Image management document.getElementById('clear-images').addEventListener('click', () => this.clearAllImages()); // Actions document.getElementById('generate-preview').addEventListener('click', () => this.generatePreview()); document.getElementById('download-html').addEventListener('click', () => this.downloadHTML()); document.getElementById('open-preview').addEventListener('click', () => this.openPreviewInNewTab()); document.getElementById('print-preview').addEventListener('click', () => this.printPreview()); // Preview zoom and fullscreen document.getElementById('zoom-in').addEventListener('click', () => this.zoomIn()); document.getElementById('zoom-out').addEventListener('click', () => this.zoomOut()); document.getElementById('zoom-reset').addEventListener('click', () => this.resetZoom()); document.getElementById('fullscreen-btn').addEventListener('click', () => this.toggleFullscreen()); // ESC key to exit fullscreen document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isFullscreen) { this.toggleFullscreen(); } }); // Click on preview container close button area to exit fullscreen document.querySelector('.preview-container').addEventListener('click', (e) => { if (this.isFullscreen) { const rect = e.currentTarget.getBoundingClientRect(); const closeButtonArea = { left: rect.right - 200, right: rect.right - 20, top: rect.top + 20, bottom: rect.top + 60 }; if (e.clientX >= closeButtonArea.left && e.clientX <= closeButtonArea.right && e.clientY >= closeButtonArea.top && e.clientY <= closeButtonArea.bottom) { this.toggleFullscreen(); } } }); // Card management document.getElementById('add-card').addEventListener('click', () => this.addCardVariation()); document.getElementById('active-card-select').addEventListener('change', (e) => { const index = parseInt(e.target.value); if (index >= 0) { this.selectCard(index); } else { this.activeCardIndex = -1; this.updateCardList(); this.showCardEditor(); } }); document.getElementById('update-card').addEventListener('click', () => this.updateActiveCard()); document.getElementById('delete-card').addEventListener('click', () => this.deleteActiveCard()); // Card editor auto-update document.getElementById('card-title-input').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-footer-input').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-template-preset').addEventListener('change', (e) => { const templateName = e.target.value; if (templateName) { this.applyTemplatePreset(templateName); } }); document.getElementById('card-background-style').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-font-family').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-font-weight').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-accent-color').addEventListener('input', (e) => { document.getElementById('card-accent-color-text').value = e.target.value; this.updateActiveCard(); }); document.getElementById('card-title-color').addEventListener('input', (e) => { document.getElementById('card-title-color-text').value = e.target.value; this.updateActiveCard(); }); document.getElementById('card-footer-color').addEventListener('input', (e) => { document.getElementById('card-footer-color-text').value = e.target.value; this.updateActiveCard(); }); document.getElementById('card-full-bleed').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-show-accent').addEventListener('change', (e) => { const accentGroup = document.getElementById('accent-color-group'); accentGroup.style.display = e.target.checked ? 'block' : 'none'; this.updateActiveCard(); }); document.getElementById('card-title-shadow').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-show-title').addEventListener('change', () => { this.updateActiveCard(); this.updateSectionVisibility(); }); document.getElementById('card-show-footer').addEventListener('change', () => { this.updateActiveCard(); this.updateSectionVisibility(); }); document.getElementById('card-title-font-size').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-title-x').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-title-y').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-title-draggable').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-footer-font-size').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-footer-x').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-footer-y').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-footer-draggable').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-image-fit').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-image-position').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-image-select').addEventListener('change', () => this.updateActiveCard()); // Panel settings document.getElementById('card-panel-mode').addEventListener('change', () => { this.updateActiveCard(); this.updateSectionVisibility(); }); document.getElementById('card-title-panel-border').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-title-panel-rotation').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-title-panel-expand').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-image-panel-border').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-image-panel-rotation').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-footer-panel-border').addEventListener('input', () => this.updateActiveCard()); document.getElementById('card-footer-panel-rotation').addEventListener('input', () => this.updateActiveCard()); // Title style selection auto-update document.querySelectorAll('.title-style-option').forEach(option => { option.addEventListener('click', () => { document.querySelectorAll('.title-style-option').forEach(opt => opt.classList.remove('selected')); option.classList.add('selected'); this.updateActiveCard(); }); }); // Global title font size // document.getElementById('global-title-font-size').addEventListener('input', (e) => { // this.globalTitleFontSize = parseInt(e.target.value); // this.generatePreview(); // }); // Mobile toggles document.getElementById('toggle-settings').addEventListener('click', () => { const panel = document.querySelector('.settings-panel'); panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; }); document.getElementById('toggle-editor').addEventListener('click', () => { const panel = document.querySelector('.edit-panel'); panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; }); // Section header click handlers for collapsing/expanding document.querySelectorAll('.config-section h4').forEach(header => { header.addEventListener('click', () => { const section = header.parentElement; section.classList.toggle('collapsed'); }); }); } async handleImageUpload(files) { const initialImageCount = this.images.length; for (const file of files) { if (file.type.startsWith('image/')) { try { const imageData = await this.readFileAsDataURL(file); const coverImage = new CoverImage(file.name, imageData); this.images.push(coverImage); // Automatically create a card variation for this image const variation = { title: '', footer: '', imageIndex: this.images.length - 1, // Use the index of the newly added image template: this.currentTemplate }; this.cardVariations.push(variation); } catch (error) { this.showStatusMessage(`Error loading ${file.name}: ${error.message}`, 'error'); } } } // If no images were uploaded before and we now have images, remove the default single card if (initialImageCount === 0 && this.images.length > 0 && this.cardVariations.length === 1 && !this.cardVariations[0].title && !this.cardVariations[0].footer && this.cardVariations[0].imageIndex === -1) { this.cardVariations = []; // Clear the default empty variation } this.updateImageList(); this.updateCardList(); this.generatePreview(); } readFileAsDataURL(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsDataURL(file); }); } updateImageList() { const imageList = document.getElementById('image-list'); imageList.innerHTML = ''; this.images.forEach((image, index) => { const item = document.createElement('div'); item.className = 'image-item'; item.innerHTML = `
${image.name}
${image.width || 0} × ${image.height || 0}px
`; imageList.appendChild(item); }); } removeImage(index) { this.images.splice(index, 1); this.updateImageList(); this.updateCardList(); this.generatePreview(); } clearAllImages() { this.images = []; this.updateImageList(); this.updateCardList(); this.generatePreview(); } applyTemplatePreset(templateName) { if (!this.templates[templateName]) return; const preset = this.templates[templateName]; document.getElementById('card-background-style').value = preset.backgroundStyle; document.getElementById('card-font-family').value = preset.fontFamily; document.getElementById('card-font-weight').value = preset.fontWeight; document.getElementById('card-footer-color').value = preset.footerColor; document.getElementById('card-footer-color-text').value = preset.footerColor; // Set additional properties if defined in preset if (preset.fullBleed !== undefined) { document.getElementById('card-full-bleed').checked = preset.fullBleed; } if (preset.showFooter !== undefined) { document.getElementById('card-show-footer').checked = preset.showFooter; } if (preset.imagePosition !== undefined) { document.getElementById('card-image-position').value = preset.imagePosition; } // Set panel properties if defined in preset document.getElementById('card-panel-mode').checked = preset.panelMode === true; if (preset.titlePanel !== undefined) { document.getElementById('card-title-panel-border').value = preset.titlePanel.borderWidth; document.getElementById('card-title-panel-rotation').value = preset.titlePanel.rotation; document.getElementById('card-title-panel-expand').checked = preset.titlePanel.expand; } if (preset.imagePanel !== undefined) { document.getElementById('card-image-panel-border').value = preset.imagePanel.borderWidth; document.getElementById('card-image-panel-rotation').value = preset.imagePanel.rotation; } if (preset.footerPanel !== undefined) { document.getElementById('card-footer-panel-border').value = preset.footerPanel.borderWidth; document.getElementById('card-footer-panel-rotation').value = preset.footerPanel.rotation; } this.updateActiveCard(); } addCardVariation() { const variation = { title: '', footer: '', imageIndex: -1, backgroundStyle: 'solid', fontFamily: 'Arial', fontWeight: 'normal', footerColor: '#111111', titleStyle: 'classic', accentColor: '#f1c40f', showAccent: false, titleColor: '#111111', fullBleed: true, titleShadow: false, showTitle: true, showFooter: true, imageFit: 'cover', imagePosition: 'center', titleFontSize: 20, footerFontSize: 12, titleX: 50, titleY: 20, footerX: 50, footerY: 90, titleDraggable: false, footerDraggable: false, panelMode: false, titlePanel: { borderWidth: 0, rotation: 0, expand: false }, imagePanel: { borderWidth: 0, rotation: 0 }, footerPanel: { borderWidth: 0, rotation: 0 } }; this.cardVariations.push(variation); this.updateCardList(); this.generatePreview(); } updateCardVariation(index, field, value) { this.cardVariations[index][field] = value; this.generatePreview(); } removeCardVariation(index) { this.cardVariations.splice(index, 1); this.updateCardVariations(); this.generatePreview(); } updateCardVariations() { // This method is now replaced by updateCardList this.updateCardList(); } updateCardList() { const container = document.getElementById('card-list'); container.innerHTML = ''; if (this.cardVariations.length === 0) { container.innerHTML = '

No cards yet. Upload images or click "Add Card".

'; return; } this.cardVariations.forEach((variation, index) => { const cardItem = document.createElement('div'); cardItem.className = `card-list-item ${index === this.activeCardIndex ? 'active' : ''}`; cardItem.onclick = () => this.selectCard(index); cardItem.innerHTML = `
Card ${index + 1} ${variation.title || 'No title'} • ${(() => { for (const [presetName, preset] of Object.entries(this.templates)) { if (variation.backgroundStyle === preset.backgroundStyle && variation.fontFamily === preset.fontFamily && variation.fontWeight === preset.fontWeight && variation.footerColor === preset.footerColor) { return presetName; } } return 'Custom'; })()} • ${variation.imageIndex >= 0 ? `Image ${variation.imageIndex + 1}` : 'Default image'}
`; container.appendChild(cardItem); }); this.updateActiveCardSelector(); } updateActiveCardSelector() { const selector = document.getElementById('active-card-select'); selector.innerHTML = ''; this.cardVariations.forEach((variation, index) => { const option = document.createElement('option'); option.value = index; option.textContent = `Card ${index + 1}: ${variation.title || 'Untitled'}`; if (index === this.activeCardIndex) option.selected = true; selector.appendChild(option); }); } selectCard(index) { this.activeCardIndex = index; this.updateCardList(); this.showCardEditor(); } selectCardFromPreview(index) { // Called from iframe when clicking on a card in the preview this.selectCard(index); } showCardEditor() { const editor = document.getElementById('card-editor'); if (this.activeCardIndex === -1) { editor.style.display = 'none'; return; } const variation = this.cardVariations[this.activeCardIndex]; if (!variation) return; // Populate card settings document.getElementById('card-editor-title').textContent = `Edit Card ${this.activeCardIndex + 1}`; // Set template preset (detect which preset matches current features) let matchingPreset = ''; for (const [presetName, preset] of Object.entries(this.templates)) { let matches = variation.backgroundStyle === preset.backgroundStyle && variation.fontFamily === preset.fontFamily && variation.fontWeight === preset.fontWeight && variation.footerColor === preset.footerColor; // Check additional properties if defined in preset if (preset.fullBleed !== undefined && variation.fullBleed !== preset.fullBleed) matches = false; if (preset.showFooter !== undefined && variation.showFooter !== preset.showFooter) matches = false; if (preset.imagePosition !== undefined && variation.imagePosition !== preset.imagePosition) matches = false; if (preset.panelMode !== undefined && variation.panelMode !== preset.panelMode) matches = false; if (preset.titlePanel !== undefined) { if (variation.titlePanel?.borderWidth !== preset.titlePanel.borderWidth || variation.titlePanel?.rotation !== preset.titlePanel.rotation || variation.titlePanel?.expand !== preset.titlePanel.expand) matches = false; } if (preset.imagePanel !== undefined) { if (variation.imagePanel?.borderWidth !== preset.imagePanel.borderWidth || variation.imagePanel?.rotation !== preset.imagePanel.rotation) matches = false; } if (preset.footerPanel !== undefined) { if (variation.footerPanel?.borderWidth !== preset.footerPanel.borderWidth || variation.footerPanel?.rotation !== preset.footerPanel.rotation) matches = false; } if (matches) { matchingPreset = presetName; break; } } document.getElementById('card-template-preset').value = matchingPreset; // Set individual features document.getElementById('card-background-style').value = variation.backgroundStyle || 'solid'; document.getElementById('card-font-family').value = variation.fontFamily || 'Arial'; document.getElementById('card-font-weight').value = variation.fontWeight || 'normal'; document.getElementById('card-footer-color').value = variation.footerColor || '#111111'; document.getElementById('card-footer-color-text').value = variation.footerColor || '#111111'; // Set title document.getElementById('card-title-input').value = variation.title || ''; // Set footer document.getElementById('card-footer-input').value = variation.footer || ''; // Set title style document.querySelectorAll('.title-style-option').forEach(option => { option.classList.toggle('selected', option.dataset.style === (variation.titleStyle || 'classic')); }); // Set colors document.getElementById('card-accent-color').value = variation.accentColor || '#f1c40f'; document.getElementById('card-accent-color-text').value = variation.accentColor || '#f1c40f'; document.getElementById('card-show-accent').checked = variation.showAccent === true; const accentGroup = document.getElementById('accent-color-group'); accentGroup.style.display = variation.showAccent ? 'block' : 'none'; document.getElementById('card-title-color').value = variation.titleColor || '#111111'; document.getElementById('card-title-color-text').value = variation.titleColor || '#111111'; // Set checkboxes document.getElementById('card-full-bleed').checked = variation.fullBleed !== false; document.getElementById('card-title-shadow').checked = variation.titleShadow === true; document.getElementById('card-show-title').checked = variation.showTitle !== false; document.getElementById('card-show-footer').checked = variation.showFooter !== false; // Set title options document.getElementById('card-title-font-size').value = variation.titleFontSize || 20; document.getElementById('card-title-x').value = variation.titleX !== undefined ? variation.titleX : 50; document.getElementById('card-title-y').value = variation.titleY !== undefined ? variation.titleY : 20; document.getElementById('card-title-draggable').checked = variation.titleDraggable === true; // Set footer options document.getElementById('card-footer-font-size').value = variation.footerFontSize || 12; document.getElementById('card-footer-x').value = variation.footerX !== undefined ? variation.footerX : 50; document.getElementById('card-footer-y').value = variation.footerY !== undefined ? variation.footerY : 90; document.getElementById('card-footer-draggable').checked = variation.footerDraggable === true; // Set image options document.getElementById('card-image-fit').value = variation.imageFit || 'cover'; document.getElementById('card-image-position').value = variation.imagePosition || 'center'; // Set panel options document.getElementById('card-panel-mode').checked = variation.panelMode === true; document.getElementById('card-title-panel-border').value = variation.titlePanel?.borderWidth || 0; document.getElementById('card-title-panel-rotation').value = variation.titlePanel?.rotation || 0; document.getElementById('card-title-panel-expand').checked = variation.titlePanel?.expand === true; document.getElementById('card-image-panel-border').value = variation.imagePanel?.borderWidth || 0; document.getElementById('card-image-panel-rotation').value = variation.imagePanel?.rotation || 0; document.getElementById('card-footer-panel-border').value = variation.footerPanel?.borderWidth || 0; document.getElementById('card-footer-panel-rotation').value = variation.footerPanel?.rotation || 0; // Set card fit document.getElementById('card-fit').value = variation.cardFit !== undefined ? variation.cardFit : ''; // Populate image options const imageSelect = document.getElementById('card-image-select'); imageSelect.innerHTML = ''; this.images.forEach((img, imgIndex) => { const option = document.createElement('option'); option.value = imgIndex; option.textContent = img.name; if (variation.imageIndex === imgIndex) option.selected = true; imageSelect.appendChild(option); }); editor.style.display = 'block'; // Update section visibility based on show/hide toggles this.updateSectionVisibility(); } updateSectionVisibility() { const showTitle = document.getElementById('card-show-title').checked; const showFooter = document.getElementById('card-show-footer').checked; const panelMode = document.getElementById('card-panel-mode').checked; const titleSection = document.getElementById('title-section'); const footerSection = document.getElementById('footer-section'); const panelSection = document.getElementById('panel-section'); if (showTitle) { titleSection.classList.remove('collapsed'); } else { titleSection.classList.add('collapsed'); } if (showFooter) { footerSection.classList.remove('collapsed'); } else { footerSection.classList.add('collapsed'); } if (panelMode) { panelSection.classList.remove('collapsed'); } else { panelSection.classList.add('collapsed'); } } initDragFunctionality() { console.log('initDragFunctionality called'); // This will be called when the preview iframe loads const iframe = document.getElementById('preview-iframe'); if (!iframe) { console.log('No iframe found'); return; } const initializeDrag = () => { console.log('initializeDrag called'); try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (!iframeDoc) { console.log('No iframe document'); return; } const titles = iframeDoc.querySelectorAll('.title, .comic-title-panel'); console.log('Found titles:', titles.length); titles.forEach((title, index) => { console.log(`Title ${index} cursor:`, title.style.cursor); // Only make draggable if cursor is set to 'move' and not a comic panel if (title.style.cursor !== 'move' || title.classList.contains('comic-title-panel')) { console.log(`Title ${index} not draggable, skipping`); return; } let isDragging = false; let startX, startY, initialLeft, initialTop; title.addEventListener('mousedown', (e) => { console.log('Title mousedown fired'); if (title.style.cursor !== 'move') return; isDragging = true; // Get iframe position and convert mouse coordinates to iframe-relative const iframeRect = iframe.getBoundingClientRect(); startX = e.clientX - iframeRect.left; startY = e.clientY - iframeRect.top; // Store initial position initialLeft = parseFloat(title.style.left) || 50; initialTop = parseFloat(title.style.top) || 20; title.style.zIndex = '1000'; title.style.userSelect = 'none'; e.preventDefault(); }); const handleMouseMove = (e) => { if (!isDragging) return; // Get iframe position in main document const iframeRect = iframe.getBoundingClientRect(); // Convert mouse coordinates to be relative to iframe viewport const mouseX = e.clientX - iframeRect.left; const mouseY = e.clientY - iframeRect.top; const card = title.closest('.card'); const cardRect = card.getBoundingClientRect(); // Convert card position to be relative to iframe const cardLeftRelative = cardRect.left - iframeRect.left; const cardTopRelative = cardRect.top - iframeRect.top; // Calculate mouse position relative to the card (within iframe) const relativeMouseX = mouseX - cardLeftRelative; const relativeMouseY = mouseY - cardTopRelative; // Convert to percentage const newLeft = Math.max(0, Math.min(100, (relativeMouseX / cardRect.width) * 100)); const newTop = Math.max(0, Math.min(100, (relativeMouseY / cardRect.height) * 100)); console.log('Drag debug:', { mouseX, mouseY, iframeRect, cardRect, cardLeftRelative, cardTopRelative, relativeMouseX, relativeMouseY, newLeft, newTop }); title.style.left = `${newLeft}%`; title.style.top = `${newTop}%`; console.log('Title style set to:', title.style.left, title.style.top); // Update the form values if this card is currently selected const cardIndex = parseInt(card.getAttribute('data-card-index')); if (cardIndex === this.activeCardIndex && this.cardVariations[cardIndex]) { document.getElementById('card-title-x').value = Math.round(newLeft); document.getElementById('card-title-y').value = Math.round(newTop); // Update the variation data this.cardVariations[cardIndex].titleX = Math.round(newLeft); this.cardVariations[cardIndex].titleY = Math.round(newTop); } }; const handleMouseUp = () => { if (isDragging) { isDragging = false; title.style.zIndex = ''; title.style.userSelect = ''; } }; // Add event listeners to both iframe document and main document // to handle mouse events that might occur outside the iframe iframeDoc.addEventListener('mousemove', handleMouseMove); iframeDoc.addEventListener('mouseup', handleMouseUp); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }); // Initialize footer drag functionality const footers = iframeDoc.querySelectorAll('.footer, .comic-footer-panel'); console.log('Found footers:', footers.length); footers.forEach((footer, index) => { console.log(`Footer ${index} cursor:`, footer.style.cursor); // Only make draggable if cursor is set to 'move' and not a comic panel if (footer.style.cursor !== 'move' || footer.classList.contains('comic-footer-panel')) { console.log(`Footer ${index} not draggable, skipping`); return; } let isDragging = false; let startX, startY, initialLeft, initialBottom; footer.addEventListener('mousedown', (e) => { console.log('Footer mousedown fired'); if (footer.style.cursor !== 'move') return; isDragging = true; // Get iframe position and convert mouse coordinates to iframe-relative const iframeRect = iframe.getBoundingClientRect(); startX = e.clientX - iframeRect.left; startY = e.clientY - iframeRect.top; // Store initial position initialLeft = parseFloat(footer.style.left) || 50; initialBottom = parseFloat(footer.style.bottom) || 10; footer.style.zIndex = '1000'; footer.style.userSelect = 'none'; e.preventDefault(); }); const handleMouseMove = (e) => { if (!isDragging) return; // Get iframe position in main document const iframeRect = iframe.getBoundingClientRect(); // Convert mouse coordinates to be relative to iframe viewport const mouseX = e.clientX - iframeRect.left; const mouseY = e.clientY - iframeRect.top; const card = footer.closest('.card'); const cardRect = card.getBoundingClientRect(); // Calculate new position as percentage const deltaX = mouseX - startX; const deltaY = mouseY - startY; const newLeftPercent = Math.max(0, Math.min(100, initialLeft + (deltaX / cardRect.width) * 100)); const newBottomPercent = Math.max(0, Math.min(100, initialBottom - (deltaY / cardRect.height) * 100)); footer.style.left = `${newLeftPercent}%`; footer.style.bottom = `${newBottomPercent}%`; // Update the card data const cardIndex = Array.from(card.parentElement.children).indexOf(card); if (this.cardVariations[cardIndex]) { this.cardVariations[cardIndex].footerX = Math.round(newLeftPercent); this.cardVariations[cardIndex].footerY = Math.round(100 - newBottomPercent); this.updateUI(); } }; const handleMouseUp = () => { if (isDragging) { isDragging = false; footer.style.zIndex = ''; footer.style.userSelect = ''; } }; // Add event listeners to both iframe document and main document // to handle mouse events that might occur outside the iframe iframeDoc.addEventListener('mousemove', handleMouseMove); iframeDoc.addEventListener('mouseup', handleMouseUp); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }); } catch (e) { // Ignore cross-origin errors console.log('Drag functionality initialization failed:', e); } }; // Try to initialize immediately if the iframe is already loaded if (iframe.contentDocument && (iframe.contentDocument.readyState === 'complete' || iframe.contentDocument.readyState === 'interactive')) { console.log('Iframe already loaded, initializing immediately'); initializeDrag(); } else { console.log('Setting iframe onload'); iframe.onload = () => { console.log('iframe onload fired'); initializeDrag(); }; } } updateActiveCard() { if (this.activeCardIndex === -1) return; const variation = this.cardVariations[this.activeCardIndex]; variation.backgroundStyle = document.getElementById('card-background-style').value; variation.fontFamily = document.getElementById('card-font-family').value; variation.fontWeight = document.getElementById('card-font-weight').value; variation.footerColor = document.getElementById('card-footer-color').value; variation.title = document.getElementById('card-title-input').value; variation.footer = document.getElementById('card-footer-input').value; variation.titleStyle = document.querySelector('.title-style-option.selected')?.dataset.style || 'classic'; variation.accentColor = document.getElementById('card-accent-color').value; variation.showAccent = document.getElementById('card-show-accent').checked; variation.titleColor = document.getElementById('card-title-color').value; variation.fullBleed = document.getElementById('card-full-bleed').checked; variation.titleShadow = document.getElementById('card-title-shadow').checked; variation.showTitle = document.getElementById('card-show-title').checked; variation.showFooter = document.getElementById('card-show-footer').checked; variation.titleFontSize = parseInt(document.getElementById('card-title-font-size').value); variation.titleX = parseInt(document.getElementById('card-title-x').value); variation.titleY = parseInt(document.getElementById('card-title-y').value); variation.titleDraggable = document.getElementById('card-title-draggable').checked; variation.footerFontSize = parseInt(document.getElementById('card-footer-font-size').value); variation.footerX = parseInt(document.getElementById('card-footer-x').value); variation.footerY = parseInt(document.getElementById('card-footer-y').value); variation.footerDraggable = document.getElementById('card-footer-draggable').checked; variation.imageFit = document.getElementById('card-image-fit').value; variation.imagePosition = document.getElementById('card-image-position').value; variation.imageIndex = parseInt(document.getElementById('card-image-select').value); // Panel settings variation.panelMode = document.getElementById('card-panel-mode').checked; variation.titlePanel = { borderWidth: parseInt(document.getElementById('card-title-panel-border').value), rotation: parseInt(document.getElementById('card-title-panel-rotation').value), expand: document.getElementById('card-title-panel-expand').checked }; variation.imagePanel = { borderWidth: parseInt(document.getElementById('card-image-panel-border').value), rotation: parseInt(document.getElementById('card-image-panel-rotation').value) }; variation.footerPanel = { borderWidth: parseInt(document.getElementById('card-footer-panel-border').value), rotation: parseInt(document.getElementById('card-footer-panel-rotation').value) }; // Update card fit const cardFitValue = document.getElementById('card-fit').value; if (cardFitValue === '') { delete variation.cardFit; } else { variation.cardFit = parseFloat(cardFitValue); } this.updateCardList(); this.generatePreview(); } deleteActiveCard() { if (this.activeCardIndex === -1) return; if (confirm(`Delete Card ${this.activeCardIndex + 1}?`)) { this.cardVariations.splice(this.activeCardIndex, 1); this.activeCardIndex = -1; this.updateCardList(); this.showCardEditor(); this.generatePreview(); } } async generatePreview() { const placeholder = document.getElementById('preview-placeholder'); const iframe = document.getElementById('preview-iframe'); if (this.images.length === 0 && !this.templateSettings.title) { placeholder.textContent = 'Add an image or enter a title to generate preview'; placeholder.style.display = 'flex'; iframe.style.display = 'none'; return; } placeholder.innerHTML = '
Generating preview...'; placeholder.style.display = 'flex'; iframe.style.display = 'none'; try { const html = await this.generateHTML(); const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); iframe.src = url; iframe.style.display = 'block'; placeholder.style.display = 'none'; // Apply zoom after iframe loads iframe.onload = () => { this.initDragFunctionality(); this.updateZoom(); }; // Clean up the previous blob URL if (iframe.dataset.previousUrl) { URL.revokeObjectURL(iframe.dataset.previousUrl); } iframe.dataset.previousUrl = url; } catch (error) { placeholder.textContent = `Error generating preview: ${error.message}`; this.showStatusMessage(`Preview generation failed: ${error.message}`, 'error'); } } async generateHTML() { // Determine number of cards to generate let totalCards; if (this.cardVariations.length > 0) { totalCards = this.cardVariations.length; } else if (this.images.length > 0) { totalCards = this.images.length; // Create one card per image if no variations } else { totalCards = 1; // Single card if no images or variations } // For print sheet, arrange cards in grid, but don't exceed available space const cardsPerPage = this.printLayout.cardsPerRow * this.printLayout.cardsPerColumn; const pagesNeeded = Math.ceil(totalCards / cardsPerPage); const totalCardsToGenerate = pagesNeeded * cardsPerPage; // Fill pages completely, but we'll hide empty slots const cards = []; // Generate cards for (let i = 0; i < totalCardsToGenerate; i++) { if (i < totalCards) { let imageIndex, cardTitle, cardFooter, cardTitleStyle, cardAccentColor, cardTitleColor, cardFullBleed, cardTitleShadow; let cardBackgroundStyle, cardFontFamily, cardFontWeight, cardFooterColor, cardShowTitle, cardShowFooter; let cardImageFit, cardImagePosition, cardTitleFontSize, cardTitleX, cardTitleY, cardTitleDraggable; let cardFooterFontSize, cardFooterX, cardFooterY, cardFooterDraggable; let cardShowAccent; let cardPanelMode, cardTitlePanel, cardImagePanel, cardFooterPanel; let cardFit; if (this.cardVariations.length > 0) { // Use variation settings const variation = this.cardVariations[i]; imageIndex = variation && variation.imageIndex >= 0 ? variation.imageIndex : 0; cardTitle = (variation && variation.title) || `Card ${i + 1}`; cardFooter = (variation && variation.footer) || ''; cardTitleStyle = (variation && variation.titleStyle) || 'classic'; cardAccentColor = (variation && variation.accentColor) || '#f1c40f'; cardShowAccent = variation && variation.showAccent !== undefined ? variation.showAccent : false; cardTitleColor = (variation && variation.titleColor) || '#111111'; cardFullBleed = variation && variation.fullBleed !== undefined ? variation.fullBleed : true; cardTitleShadow = variation && variation.titleShadow !== undefined ? variation.titleShadow : false; // Individual features cardBackgroundStyle = (variation && variation.backgroundStyle) || 'solid'; cardFontFamily = (variation && variation.fontFamily) || 'Arial'; cardFontWeight = (variation && variation.fontWeight) || 'normal'; cardFooterColor = (variation && variation.footerColor) || '#111111'; cardShowTitle = variation && variation.showTitle !== undefined ? variation.showTitle : true; cardShowFooter = variation && variation.showFooter !== undefined ? variation.showFooter : true; cardImageFit = (variation && variation.imageFit) || 'cover'; cardImagePosition = (variation && variation.imagePosition) || 'center'; cardTitleFontSize = (variation && variation.titleFontSize) || 20; cardTitleX = variation && variation.titleX !== undefined ? variation.titleX : 50; cardTitleY = variation && variation.titleY !== undefined ? variation.titleY : 20; cardTitleDraggable = variation && variation.titleDraggable !== undefined ? variation.titleDraggable : false; cardFooterFontSize = (variation && variation.footerFontSize) || 12; cardFooterX = variation && variation.footerX !== undefined ? variation.footerX : 50; cardFooterY = variation && variation.footerY !== undefined ? variation.footerY : 90; cardFooterDraggable = variation && variation.footerDraggable !== undefined ? variation.footerDraggable : false; cardPanelMode = variation && variation.panelMode !== undefined ? variation.panelMode : false; cardTitlePanel = variation && variation.titlePanel ? variation.titlePanel : { borderWidth: 0, rotation: 0, expand: false }; cardImagePanel = variation && variation.imagePanel ? variation.imagePanel : { borderWidth: 0, rotation: 0 }; cardFooterPanel = variation && variation.footerPanel ? variation.footerPanel : { borderWidth: 0, rotation: 0 }; cardFit = variation && variation.cardFit !== undefined ? variation.cardFit : this.printLayout.cardFit; } else { // No variations - auto-assign images imageIndex = i < this.images.length ? i : 0; cardTitle = `Card ${i + 1}`; cardFooter = ''; cardTitleStyle = 'classic'; cardAccentColor = '#f1c40f'; cardShowAccent = false; cardTitleColor = '#111111'; cardFullBleed = true; cardTitleShadow = false; // Default features cardBackgroundStyle = 'solid'; cardFontFamily = 'Arial'; cardFontWeight = 'normal'; cardFooterColor = '#111111'; cardShowTitle = true; cardShowFooter = true; cardImageFit = 'cover'; cardImagePosition = 'center'; cardTitleFontSize = 20; cardTitleX = 50; cardTitleY = 20; cardTitleDraggable = false; cardFooterFontSize = 12; cardFooterX = 50; cardFooterY = 90; cardFooterDraggable = false; cardPanelMode = false; cardTitlePanel = { borderWidth: 0, rotation: 0, expand: false }; cardImagePanel = { borderWidth: 0, rotation: 0 }; cardFooterPanel = { borderWidth: 0, rotation: 0 }; cardFit = this.printLayout.cardFit; } const imageUrl = this.images.length > 0 ? this.images[imageIndex].dataUrl : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; cards.push({ imageUrl, title: cardTitle, footer: cardFooter, backgroundStyle: cardBackgroundStyle, fontFamily: cardFontFamily, fontWeight: cardFontWeight, footerColor: cardFooterColor, titleStyle: cardTitleStyle, accentColor: cardAccentColor, showAccent: cardShowAccent, titleColor: cardTitleColor, fullBleed: cardFullBleed, titleShadow: cardTitleShadow, showTitle: cardShowTitle, showFooter: cardShowFooter, imageFit: cardImageFit, imagePosition: cardImagePosition, titleFontSize: cardTitleFontSize, titleX: cardTitleX, titleY: cardTitleY, titleDraggable: cardTitleDraggable, footerFontSize: cardFooterFontSize, footerX: cardFooterX, footerY: cardFooterY, footerDraggable: cardFooterDraggable, panelMode: cardPanelMode, titlePanel: cardTitlePanel, imagePanel: cardImagePanel, footerPanel: cardFooterPanel, cardFit: cardFit, isVisible: true }); } else { // Empty card for filling the grid cards.push({ imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', title: '', footer: '', backgroundStyle: 'solid', fontFamily: 'Arial', fontWeight: 'normal', footerColor: '#111111', titleStyle: 'folded', accentColor: '#f1c40f', showAccent: false, titleColor: '#111111', fullBleed: true, titleShadow: false, showTitle: false, showFooter: false, imageFit: 'cover', imagePosition: 'center', titleFontSize: 20, titleX: 50, titleY: 10, titleDraggable: false, panelMode: false, titlePanel: { borderWidth: 0, rotation: 0, expand: false }, imagePanel: { borderWidth: 0, rotation: 0 }, footerPanel: { borderWidth: 0, rotation: 0 }, cardFit: this.printLayout.cardFit, isVisible: false }); } } // Fixed card dimensions: 54mm x 85.6mm const baseCardWidthMm = 54; const baseCardHeightMm = 85.6; const cardWidthMm = baseCardWidthMm; const cardHeightMm = baseCardHeightMm; // Calculate grid layout const pageWidthMm = 210; // A4 width const pageHeightMm = 297; // A4 height // For multiple pages, we'll create multiple print-sheet divs let htmlContent = ''; for (let page = 0; page < pagesNeeded; page++) { const startIndex = page * cardsPerPage; const endIndex = Math.min(startIndex + cardsPerPage, totalCardsToGenerate); const pageCards = cards.slice(startIndex, endIndex); // Calculate positions for this page's grid const gridCols = this.printLayout.cardsPerRow; const gridRows = this.printLayout.cardsPerColumn; const pageBreak = page < pagesNeeded - 1 ? "always" : "auto"; const gridColsValue = gridCols; const cardWidthValue = cardWidthMm; const cardHeightValue = cardHeightMm; const spacingValue = this.printLayout.cardSpacing; const widthValue = gridCols * cardWidthMm + (gridCols - 1) * this.printLayout.cardSpacing; const heightValue = gridRows * cardHeightMm + (gridRows - 1) * this.printLayout.cardSpacing; htmlContent += ``; } // Generate HTML for print sheet const html = ` Yoto Covers Print Sheet ${htmlContent} `; return html; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML.replace(/\n/g, '
'); } openPreviewInNewTab() { const iframe = document.getElementById('preview-iframe'); if (iframe.src) { window.open(iframe.src, '_blank'); } else { this.showStatusMessage('No preview available. Generate a preview first.', 'error'); } } printPreview() { const iframe = document.getElementById('preview-iframe'); if (iframe.contentWindow) { iframe.contentWindow.print(); } else { this.showStatusMessage('No preview available to print.', 'error'); } } downloadHTML() { if (!this.templateSettings.title && this.images.length === 0) { this.showStatusMessage('Add content before downloading.', 'error'); return; } this.generateHTML().then(html => { const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `yoto-cover-${Date.now()}.html`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showStatusMessage('HTML file downloaded successfully!', 'success'); }).catch(error => { this.showStatusMessage(`Download failed: ${error.message}`, 'error'); }); } showStatusMessage(message, type = 'info') { const statusDiv = document.getElementById('status-message'); statusDiv.textContent = message; statusDiv.className = `status-message status-${type}`; statusDiv.style.display = 'block'; setTimeout(() => { statusDiv.style.display = 'none'; }, 5000); } zoomIn() { if (this.previewZoom.level < this.previewZoom.max) { this.previewZoom.level = Math.min(this.previewZoom.max, this.previewZoom.level + this.previewZoom.step); this.updateZoom(); } } zoomOut() { if (this.previewZoom.level > this.previewZoom.min) { this.previewZoom.level = Math.max(this.previewZoom.min, this.previewZoom.level - this.previewZoom.step); this.updateZoom(); } } resetZoom() { this.previewZoom.level = 1.0; this.updateZoom(); } updateZoom() { const iframe = document.getElementById('preview-iframe'); const zoomLevel = document.getElementById('zoom-level'); if (iframe) { iframe.style.transform = `scale(${this.previewZoom.level})`; iframe.style.transformOrigin = 'top left'; } if (zoomLevel) { zoomLevel.textContent = Math.round(this.previewZoom.level * 100) + '%'; } } toggleFullscreen() { const container = document.querySelector('.preview-container'); const overlay = document.getElementById('fullscreen-overlay'); const btn = document.getElementById('fullscreen-btn'); if (this.isFullscreen) { // Exit fullscreen container.classList.remove('fullscreen'); overlay.classList.remove('active'); btn.textContent = '⛶ Full Screen'; this.isFullscreen = false; } else { // Enter fullscreen container.classList.add('fullscreen'); overlay.classList.add('active'); btn.textContent = '⛶ Exit Full Screen'; this.isFullscreen = true; // Add click handler to overlay to exit fullscreen overlay.onclick = () => this.toggleFullscreen(); } } updateUI() { // Initialize print layout controls document.getElementById('cards-per-row').value = this.printLayout.cardsPerRow; document.getElementById('cards-per-column').value = this.printLayout.cardsPerColumn; document.getElementById('page-margin').value = this.printLayout.pageMargin; document.getElementById('card-spacing').value = this.printLayout.cardSpacing; document.getElementById('global-card-fit').value = this.printLayout.cardFit; } } class CoverImage { constructor(name, dataUrl) { this.name = name; this.dataUrl = dataUrl; this.width = 0; this.height = 0; // Get image dimensions this.getImageDimensions(); } async getImageDimensions() { return new Promise((resolve) => { const img = new Image(); img.onload = () => { this.width = img.naturalWidth; this.height = img.naturalHeight; resolve(); }; img.src = this.dataUrl; }); } } // Initialize the app when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.app = new YotoCoversApp(); window.selectCardFromPreview = (index) => window.app.selectCardFromPreview(index); });