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.
This commit is contained in:
+748
@@ -0,0 +1,748 @@
|
||||
// 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();
|
||||
Reference in New Issue
Block a user