cda15991e3
- 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.
748 lines
27 KiB
Plaintext
748 lines
27 KiB
Plaintext
// 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 = `
|
||
<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-secondary" onclick="app.selectImage(${index})">Edit</button>
|
||
<button class="btn btn-danger" onclick="app.removeImage(${index})">×</button>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<input type="text" class="text-overlay-input" value="${overlay.text}" placeholder="Text overlay" oninput="app.updateTextOverlay(${index}, 'text', this.value)">
|
||
<div class="text-overlay-controls">
|
||
<input type="color" value="${overlay.color}" onchange="app.updateTextOverlay(${index}, 'color', this.value)">
|
||
<input type="number" value="${overlay.fontSize}" min="8" max="72" onchange="app.updateTextOverlay(${index}, 'fontSize', this.value)">
|
||
<button class="btn btn-danger" onclick="app.removeTextOverlay(${index})">×</button>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
|
||
<strong>Card ${index + 1}:</strong>
|
||
<button class="btn btn-danger" onclick="app.removeCardVariation(${index})" style="font-size: 12px; padding: 4px 8px;">×</button>
|
||
</div>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||
<div class="form-group" style="margin: 0;">
|
||
<label>Title</label>
|
||
<input type="text" value="${variation.title}" placeholder="Custom title" oninput="app.updateCardVariation(${index}, 'title', this.value)">
|
||
</div>
|
||
<div class="form-group" style="margin: 0;">
|
||
<label>Footer</label>
|
||
<input type="text" value="${variation.footer}" placeholder="Custom footer" oninput="app.updateCardVariation(${index}, 'footer', this.value)">
|
||
</div>
|
||
</div>
|
||
<div class="form-group" style="margin: 0; grid-column: span 2;">
|
||
<label>Image</label>
|
||
<select onchange="app.updateCardVariation(${index}, 'imageIndex', parseInt(this.value))">
|
||
<option value="-1">Use default image</option>
|
||
${this.images.map((img, imgIndex) => `<option value="${imgIndex}" ${variation.imageIndex === imgIndex ? 'selected' : ''}>${img.name}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
`;
|
||
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 = '<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 {
|
||
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 = `
|
||
<!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; }
|
||
.print-page:last-child { page-break-after: auto; }
|
||
}
|
||
@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;
|
||
}
|
||
.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;
|
||
${this.templateSettings.titleShadow ? 'text-shadow: 1px 4px 6px rgba(0,0,0,0.3);' : ''}
|
||
}
|
||
.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);
|
||
}
|
||
.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>
|
||
<div class="print-sheet">
|
||
${cards.map((card, index) => `
|
||
<div class="card">
|
||
<div class="title ${this.currentTitleStyle === 'folded' ? 'folded' : ''}" data-heading="${this.escapeHtml(card.title)}">${this.escapeHtml(card.title)}</div>
|
||
${!this.templateSettings.fullBleed ? `<div class="hero"><img src="${card.imageUrl}" alt="cover"/></div>` : ''}
|
||
<div class="overlay"></div>
|
||
<div class="footer">${this.escapeHtml(card.footer)}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
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(); |