Files
yoto-covers/app.js
T
Ross cda15991e3 feat: Initial commit of Yoto Up Covers web application
- Added main HTML file (index.html) for the web app interface.
- Created a sample cover template (sample_cover.html) for Yoto cards.
- Implemented a simple HTTP server (server.py) to serve the web app.
- Configured package.json for project metadata and scripts.
2026-01-04 20:43:57 +00:00

1025 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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'
}
};
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.globalTitleFontSize = 20; // Global title font size in pixels
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-title-shadow').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-show-title').addEventListener('change', () => this.updateActiveCard());
document.getElementById('card-show-footer').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';
});
}
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 = `
<div class="image-info">
<div class="image-name">${image.name}</div>
<div class="image-meta">${image.width || 0} × ${image.height || 0}px</div>
</div>
<div class="image-actions">
<button class="btn btn-danger" onclick="app.removeImage(${index})">×</button>
</div>
`;
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;
this.updateActiveCard();
}
addCardVariation() {
const variation = {
title: '',
footer: '',
imageIndex: -1,
backgroundStyle: 'solid',
fontFamily: 'Arial',
fontWeight: 'normal',
footerColor: '#111111',
titleStyle: 'folded',
accentColor: '#f1c40f',
titleColor: '#111111',
fullBleed: true,
titleShadow: false,
showTitle: true,
showFooter: true
};
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 = '<p style="color: #999999; text-align: center; padding: 20px;">No cards yet. Upload images or click "Add Card".</p>';
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 = `
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<strong>Card ${index + 1}</strong>
<span style="color: #666666; margin-left: 10px;">
${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'}
</span>
</div>
</div>
`;
container.appendChild(cardItem);
});
this.updateActiveCardSelector();
}
updateActiveCardSelector() {
const selector = document.getElementById('active-card-select');
selector.innerHTML = '<option value="-1">No card selected</option>';
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)) {
if (variation.backgroundStyle === preset.backgroundStyle &&
variation.fontFamily === preset.fontFamily &&
variation.fontWeight === preset.fontWeight &&
variation.footerColor === preset.footerColor) {
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 || 'folded'));
});
// Set colors
document.getElementById('card-accent-color').value = variation.accentColor || '#f1c40f';
document.getElementById('card-accent-color-text').value = variation.accentColor || '#f1c40f';
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;
// Populate image options
const imageSelect = document.getElementById('card-image-select');
imageSelect.innerHTML = '<option value="-1">Use default image</option>';
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';
}
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 || 'folded';
variation.accentColor = document.getElementById('card-accent-color').value;
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.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 = '<div class="loading"></div> 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 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;
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) || 'folded';
cardAccentColor = (variation && variation.accentColor) || '#f1c40f';
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;
} else {
// No variations - auto-assign images
imageIndex = i < this.images.length ? i : 0;
cardTitle = `Card ${i + 1}`;
cardFooter = '';
cardTitleStyle = 'folded';
cardAccentColor = '#f1c40f';
cardTitleColor = '#111111';
cardFullBleed = true;
cardTitleShadow = false;
// Default features
cardBackgroundStyle = 'solid';
cardFontFamily = 'Arial';
cardFontWeight = 'normal';
cardFooterColor = '#111111';
cardShowTitle = true;
cardShowFooter = true;
}
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,
titleColor: cardTitleColor,
fullBleed: cardFullBleed,
titleShadow: cardTitleShadow,
showTitle: cardShowTitle,
showFooter: cardShowFooter,
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',
titleColor: '#111111',
fullBleed: true,
titleShadow: false,
showTitle: false,
showFooter: 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 += `
<div class="print-page" style="page-break-after: ${page < pagesNeeded - 1 ? 'always' : 'auto'}; width: ${pageWidthMm}mm; padding: ${this.printLayout.pageMargin}mm; box-sizing: border-box;">
<div class="print-sheet" style="
display: grid;
grid-template-columns: repeat(${gridCols}, ${cardWidthMm}mm);
grid-template-rows: repeat(${gridRows}, ${cardHeightMm}mm);
gap: ${this.printLayout.cardSpacing}mm;
width: ${gridCols * cardWidthMm + (gridCols - 1) * this.printLayout.cardSpacing}mm;
height: ${gridRows * cardHeightMm + (gridRows - 1) * this.printLayout.cardSpacing}mm;
margin: 0;
padding: 0;
box-sizing: border-box;
">
${pageCards.map((card, index) => `
<div class="card card-${startIndex + index} ${card.isVisible ? '' : 'empty-card'}"
data-card-index="${startIndex + index}"
onclick="if (window.parent && window.parent.selectCardFromPreview) { window.parent.selectCardFromPreview(${startIndex + index}); }"
style="cursor: pointer;
width: ${cardWidthMm}mm;
height: ${cardHeightMm}mm;
box-sizing: border-box;
font-family: 'DejaVuSans', serif;
${card.isVisible && card.backgroundStyle === 'gradient' ? 'background: linear-gradient(135deg, #e8f4fd 0%, #d1e8f0 100%);' : ''}
${card.isVisible && card.backgroundStyle === 'pattern' ? 'background: linear-gradient(45deg, #f5f5dc 0%, #deb887 100%);' : ''}
${card.isVisible && card.backgroundStyle === 'comic' ? 'background: radial-gradient(circle at 30% 40%, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #ffeaa7);' : ''}
${card.isVisible && card.backgroundStyle === 'solid' ? 'background: #f8f9fa;' : ''}
${card.isVisible && card.fullBleed ? `background: url('${card.imageUrl}') no-repeat center/cover;` : ''}
position: relative;
border: ${card.isVisible ? 'none' : '1px dashed #ccc'};
overflow: hidden;
">
${card.isVisible ? `
${card.showTitle ? `<div class="title ${card.titleStyle}" data-heading="${this.escapeHtml(card.title)}" style="
color: ${card.titleColor};
font-weight: ${card.fontWeight === 'bold' ? '700' : '400'};
font-family: '${card.fontFamily}', serif;
font-size: calc(${this.globalTitleFontSize}px + 0.5rem);
${card.titleShadow ? 'text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);' : ''}
">${this.escapeHtml(card.title)}</div>` : ''}
${!card.fullBleed ? `<div class="hero"><img src="${card.imageUrl}" alt="cover"/></div>` : ''}
<div class="overlay" style="background: ${card.accentColor}40;"></div>
${card.showFooter ? `<div class="footer" style="
color: ${card.footerColor};
font-family: '${card.fontFamily}', serif;
font-weight: ${card.fontWeight};
">${this.escapeHtml(card.footer)}</div>` : ''}
` : ''}
</div>
`).join('')}
</div>
</div>`;
}
// Generate HTML for print sheet
const html = `
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Yoto Covers Print Sheet</title>
<style>
@font-face { font-family: 'DejaVuSans'; src: local('DejaVu Sans'); }
@media screen {
html, body { margin: 0; padding: 20px; background: #f5f5f5; }
.print-page { border: 2px dashed #ccc; margin: 20px auto; background: white; page-break-after: always; width: 210mm; max-width: 100%; }
.print-page:last-child { page-break-after: auto; }
.print-sheet { max-width: 100%; height: auto !important; }
}
@media print {
html, body { margin: 0; padding: 0; background: white; }
.print-page { border: none; margin: 0; page-break-after: always; }
.print-page:last-child { page-break-after: auto; }
@page { margin: 0; size: A4; }
}
.card {
box-sizing: border-box;
position: relative;
}
.empty-card {
background: #f9f9f9;
}
${cards.map((card, index) => `
.card-${index} .title {
color: ${card.titleColor};
font-family: '${card.fontFamily}', serif;
font-weight: ${card.fontWeight === 'bold' ? '700' : '400'};
}
.card-${index} .footer {
color: ${card.footerColor};
}
`).join('')}
.title {
position: absolute;
top: 6%;
left: 6%;
right: 6%;
text-align: center;
font-size: calc(108px + 0.5rem);
color: ${this.templateSettings.titleColor};
font-weight: 900;
text-transform: uppercase;
z-index: 10;
${this.templateSettings.titleShadow ? 'text-shadow: 1px 4px 6px rgba(0,0,0,0.3);' : ''}
}
.title.classic {
/* Default classic style - already defined above */
}
.title.large {
font-size: calc(120px + 0.5rem);
font-weight: 900;
}
.title.small {
font-size: calc(80px + 0.5rem);
font-weight: 700;
}
.title.condensed {
font-size: calc(96px + 0.5rem);
font-weight: 900;
letter-spacing: -2px;
font-stretch: condensed;
}
.title.italic {
font-style: italic;
}
.title.bold {
font-weight: 900;
}
.title.outlined {
-webkit-text-stroke: 2px white;
text-stroke: 2px white;
color: transparent;
}
.title.shadow {
text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
}
.title.folded {
position: absolute;
left: 6%;
right: 6%;
top: 6%;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Source Code Pro', monospace;
font-weight: 900;
white-space: nowrap;
color: lch(76 39.21 9.23 / 0.5);
text-transform: uppercase;
transform: skew(10deg) rotate(-10deg);
text-shadow: 1px 4px 6px lch(90 2.22 62.5), 0 0 0 lch(28 26.21 12.27), 1px 4px 6px lch(90 2.22 62.5);
z-index: 10;
}
.title.folded::before {
content: attr(data-heading);
position: absolute;
left: 0;
top: -4.8%;
overflow: hidden;
height: 50%;
color: lch(97 2.19 62.49);
transform: translate(0.15em, 0) skew(-13deg) scale(1, 1.2);
text-shadow: 2px -1px 6px rgba(0,0,0,0.2);
}
.title.folded::after {
content: attr(data-heading);
position: absolute;
left: 0;
color: lch(83 2.26 62.51);
transform: translate(0, 0) skew(13deg) scale(1, 0.8);
clip-path: polygon(0 50%, 100% 50%, 100% 100%, 0 100%);
text-shadow: 2px -1px 6px lch(0 0 0 / 0.3);
}
.hero {
position: absolute;
top: 18%;
left: 6%;
right: 6%;
bottom: 18%;
display: flex;
align-items: center;
justify-content: center;
}
.hero img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.overlay {
position: absolute;
top: 18%;
left: 6%;
right: 6%;
bottom: 18%;
border-radius: 8px;
pointer-events: none;
background: rgba(0,0,0,0);
}
.footer {
position: absolute;
bottom: 4%;
left: 6%;
right: 6%;
height: 10%;
background: ${this.templateSettings.accentColor};
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #000;
border-radius: 6px;
}
</style>
</head>
<body>
${htmlContent}
</body>
</html>`;
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;
// Initialize global title font size
document.getElementById('global-title-font-size').value = this.globalTitleFontSize;
}
}
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);
});