// 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 += `
${pageCards.map((card, index) => `
${card.isVisible ? `
${card.showTitle ? `
${this.escapeHtml(card.title)}
` : ''}
${!card.fullBleed ? `
` : ''}
${card.showAccent ? `
` : `
`}
${card.showFooter ? `` : ''}
` : ''}
`).join('')}
`;
}
// 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);
});