// Yoto Up Covers Web App class YotoCoversApp { constructor() { this.images = []; this.selectedImageIndex = -1; this.zoomLevel = 1.0; this.currentTemplate = 'classic'; this.currentTitleStyle = 'folded'; 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.init(); } init() { this.bindEvents(); this.updateUI(); this.updateCardVariations(); } 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 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 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 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 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()); // Zoom controls document.getElementById('zoom-in').addEventListener('click', () => this.zoomIn()); document.getElementById('zoom-out').addEventListener('click', () => this.zoomOut()); document.getElementById('zoom-reset').addEventListener('click', () => this.resetZoom()); // Image editor document.getElementById('fit-mode').addEventListener('change', (e) => { if (this.selectedImageIndex >= 0) { this.images[this.selectedImageIndex].fitMode = e.target.value; this.generatePreview(); } }); document.getElementById('crop-position').addEventListener('change', (e) => { if (this.selectedImageIndex >= 0) { this.images[this.selectedImageIndex].cropPosition = e.target.value; this.generatePreview(); } }); document.getElementById('crop-offset-x').addEventListener('input', (e) => { if (this.selectedImageIndex >= 0) { this.images[this.selectedImageIndex].cropOffsetX = parseFloat(e.target.value); document.getElementById('crop-offset-x-value').textContent = e.target.value; this.generatePreview(); } }); document.getElementById('crop-offset-y').addEventListener('input', (e) => { if (this.selectedImageIndex >= 0) { this.images[this.selectedImageIndex].cropOffsetY = parseFloat(e.target.value); document.getElementById('crop-offset-y-value').textContent = e.target.value; this.generatePreview(); } }); document.getElementById('add-text-overlay').addEventListener('click', () => this.addTextOverlay()); document.getElementById('add-card-variation').addEventListener('click', () => this.addCardVariation()); // 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'; }); } async handleImageUpload(files) { 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); } catch (error) { this.showStatusMessage(`Error loading ${file.name}: ${error.message}`, 'error'); } } } this.updateImageList(); this.updateCardVariations(); 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); }); } selectImage(index) { this.selectedImageIndex = index; this.updateImageEditor(); } removeImage(index) { this.images.splice(index, 1); if (this.selectedImageIndex === index) { this.selectedImageIndex = -1; } else if (this.selectedImageIndex > index) { this.selectedImageIndex--; } this.updateImageList(); this.updateImageEditor(); this.generatePreview(); } clearAllImages() { this.images = []; this.selectedImageIndex = -1; this.updateImageList(); this.updateImageEditor(); this.updateCardVariations(); this.generatePreview(); } updateImageEditor() { const editor = document.getElementById('image-editor'); const noSelection = document.getElementById('no-image-selected'); if (this.selectedImageIndex >= 0) { const image = this.images[this.selectedImageIndex]; editor.style.display = 'block'; noSelection.style.display = 'none'; document.getElementById('fit-mode').value = image.fitMode; document.getElementById('crop-position').value = image.cropPosition; document.getElementById('crop-offset-x').value = image.cropOffsetX; document.getElementById('crop-offset-x-value').textContent = image.cropOffsetX; document.getElementById('crop-offset-y').value = image.cropOffsetY; document.getElementById('crop-offset-y-value').textContent = image.cropOffsetY; this.updateTextOverlays(); } else { editor.style.display = 'none'; noSelection.style.display = 'block'; } } updateTextOverlays() { const container = document.getElementById('text-overlays'); container.innerHTML = ''; if (this.selectedImageIndex >= 0) { const image = this.images[this.selectedImageIndex]; image.textOverlays.forEach((overlay, index) => { const item = document.createElement('div'); item.className = 'text-overlay-item'; item.innerHTML = `
`; container.appendChild(item); }); } } addTextOverlay() { if (this.selectedImageIndex >= 0) { const image = this.images[this.selectedImageIndex]; image.textOverlays.push({ text: 'New Text', color: '#ffffff', fontSize: 24, x: 50, y: 50 }); this.updateTextOverlays(); this.generatePreview(); } } updateTextOverlay(index, property, value) { if (this.selectedImageIndex >= 0) { const image = this.images[this.selectedImageIndex]; if (property === 'fontSize') { value = parseInt(value); } image.textOverlays[index][property] = value; this.generatePreview(); } } removeTextOverlay(index) { if (this.selectedImageIndex >= 0) { const image = this.images[this.selectedImageIndex]; image.textOverlays.splice(index, 1); this.updateTextOverlays(); this.generatePreview(); } } addCardVariation() { const variation = { title: '', footer: '', imageIndex: -1 }; this.cardVariations.push(variation); this.updateCardVariations(); 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() { const container = document.getElementById('card-variations'); container.innerHTML = ''; this.cardVariations.forEach((variation, index) => { const item = document.createElement('div'); item.className = 'card-variation-item'; item.innerHTML = `
Card ${index + 1}:
`; container.appendChild(item); }); } 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.applyZoomToPreview(); }; // 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 { totalCards = 1; // Single card if no 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) { // Use variation or default const variation = this.cardVariations[i]; const imageIndex = variation && variation.imageIndex >= 0 ? variation.imageIndex : 0; const imageUrl = this.images.length > 0 ? this.images[imageIndex].dataUrl : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; cards.push({ imageUrl, title: (variation && variation.title) || this.templateSettings.title || `Card ${i + 1}`, footer: (variation && variation.footer) || this.templateSettings.footer, isVisible: true }); } else { // Empty card for filling the grid cards.push({ imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', title: '', footer: '', isVisible: false }); } } // Fixed card dimensions: 54mm x 85.6mm const cardWidthMm = 54; const cardHeightMm = 85.6; // Generate HTML for print sheet const html = ` Yoto Covers Print Sheet `; return html; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } zoomIn() { this.zoomLevel = Math.min(this.zoomLevel * 1.2, 3.0); this.updateZoomDisplay(); this.applyZoomToPreview(); } zoomOut() { this.zoomLevel = Math.max(this.zoomLevel / 1.2, 0.3); this.updateZoomDisplay(); this.applyZoomToPreview(); } resetZoom() { this.zoomLevel = 1.0; this.updateZoomDisplay(); this.applyZoomToPreview(); } updateZoomDisplay() { const zoomPercent = Math.round(this.zoomLevel * 100); document.getElementById('zoom-level').textContent = zoomPercent === 100 ? 'Fit' : zoomPercent + '%'; } applyZoomToPreview() { const iframe = document.getElementById('preview-iframe'); if (iframe.contentDocument && iframe.contentDocument.body) { const printSheet = iframe.contentDocument.querySelector('.print-sheet'); if (printSheet) { printSheet.style.transform = `scale(${this.zoomLevel})`; printSheet.style.transformOrigin = 'top center'; } } } 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() { this.updateZoomDisplay(); // 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.fitMode = 'scale'; this.cropPosition = 'center'; this.cropOffsetX = 0; this.cropOffsetY = 0; this.textOverlays = []; 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 const app = new YotoCoversApp();