// 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' }, 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 }; this.templateSettings = { title: '', footer: '', accentColor: '#f1c40f', titleColor: '#111111', fullBleed: true, titleShadow: false, titleFontSize: null, footerFontSize: null }; 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(); }); // 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()); // 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-image-fit').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-image-position').addEventListener('change', () => this.updateActiveCard()); document.getElementById('card-image-select').addEventListener('change', () => 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; } 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' }; 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 (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 image options document.getElementById('card-image-fit').value = variation.imageFit || 'cover'; document.getElementById('card-image-position').value = variation.imagePosition || 'center'; // 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 titleSection = document.getElementById('title-section'); const footerSection = document.getElementById('footer-section'); if (showTitle) { titleSection.classList.remove('collapsed'); } else { titleSection.classList.add('collapsed'); } if (showFooter) { footerSection.classList.remove('collapsed'); } else { footerSection.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'); 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' if (title.style.cursor !== 'move') { 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); }); } 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.imageFit = document.getElementById('card-image-fit').value; variation.imagePosition = document.getElementById('card-image-position').value; variation.imageIndex = parseInt(document.getElementById('card-image-select').value); 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(); }; // 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 cardShowAccent; 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; } 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; } 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, 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: 20, titleDraggable: false, isVisible: false }); } } // Fixed card dimensions: 54mm x 85.6mm const cardWidthMm = 54; const cardHeightMm = 85.6; // 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; 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; } 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); } 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; } } 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); });