feat: add zoom and fullscreen controls to preview, enhance card fit options

This commit is contained in:
Ross
2026-01-11 21:34:32 +00:00
parent 554075a3a9
commit 6e32ffe450
2 changed files with 306 additions and 20 deletions
+159 -20
View File
@@ -57,7 +57,8 @@ class YotoCoversApp {
cardsPerRow: 2,
cardsPerColumn: 5,
pageMargin: 10,
cardSpacing: 5
cardSpacing: 5,
cardFit: 0
};
this.templateSettings = {
title: '',
@@ -70,6 +71,15 @@ class YotoCoversApp {
footerFontSize: null
};
this.previewZoom = {
level: 1.0,
min: 0.25,
max: 3.0,
step: 0.25
};
this.isFullscreen = false;
this.cardVariations = [];
this.activeCardIndex = -1; // Currently selected card for editing
this.init();
@@ -103,6 +113,11 @@ class YotoCoversApp {
this.generatePreview();
});
document.getElementById('global-card-fit').addEventListener('change', (e) => {
this.printLayout.cardFit = parseFloat(e.target.value);
this.generatePreview();
});
// Template settings - REMOVED: Now per-card only
// document.getElementById('template-select').addEventListener('change', (e) => {
// this.currentTemplate = e.target.value;
@@ -181,6 +196,36 @@ class YotoCoversApp {
document.getElementById('open-preview').addEventListener('click', () => this.openPreviewInNewTab());
document.getElementById('print-preview').addEventListener('click', () => this.printPreview());
// Preview zoom and fullscreen
document.getElementById('zoom-in').addEventListener('click', () => this.zoomIn());
document.getElementById('zoom-out').addEventListener('click', () => this.zoomOut());
document.getElementById('zoom-reset').addEventListener('click', () => this.resetZoom());
document.getElementById('fullscreen-btn').addEventListener('click', () => this.toggleFullscreen());
// ESC key to exit fullscreen
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isFullscreen) {
this.toggleFullscreen();
}
});
// Click on preview container close button area to exit fullscreen
document.querySelector('.preview-container').addEventListener('click', (e) => {
if (this.isFullscreen) {
const rect = e.currentTarget.getBoundingClientRect();
const closeButtonArea = {
left: rect.right - 200,
right: rect.right - 20,
top: rect.top + 20,
bottom: rect.top + 60
};
if (e.clientX >= closeButtonArea.left && e.clientX <= closeButtonArea.right &&
e.clientY >= closeButtonArea.top && e.clientY <= closeButtonArea.bottom) {
this.toggleFullscreen();
}
}
});
// Card management
document.getElementById('add-card').addEventListener('click', () => this.addCardVariation());
document.getElementById('active-card-select').addEventListener('change', (e) => {
@@ -637,6 +682,9 @@ class YotoCoversApp {
document.getElementById('card-image-panel-rotation').value = variation.imagePanel?.rotation || 0;
document.getElementById('card-footer-panel-border').value = variation.footerPanel?.borderWidth || 0;
document.getElementById('card-footer-panel-rotation').value = variation.footerPanel?.rotation || 0;
// Set card fit
document.getElementById('card-fit').value = variation.cardFit !== undefined ? variation.cardFit : '';
// Populate image options
const imageSelect = document.getElementById('card-image-select');
@@ -940,6 +988,14 @@ class YotoCoversApp {
borderWidth: parseInt(document.getElementById('card-footer-panel-border').value),
rotation: parseInt(document.getElementById('card-footer-panel-rotation').value)
};
// Update card fit
const cardFitValue = document.getElementById('card-fit').value;
if (cardFitValue === '') {
delete variation.cardFit;
} else {
variation.cardFit = parseFloat(cardFitValue);
}
this.updateCardList();
this.generatePreview();
@@ -984,6 +1040,7 @@ class YotoCoversApp {
// Apply zoom after iframe loads
iframe.onload = () => {
this.initDragFunctionality();
this.updateZoom();
};
// Clean up the previous blob URL
@@ -1025,6 +1082,7 @@ class YotoCoversApp {
let cardFooterFontSize, cardFooterX, cardFooterY, cardFooterDraggable;
let cardShowAccent;
let cardPanelMode, cardTitlePanel, cardImagePanel, cardFooterPanel;
let cardFit;
if (this.cardVariations.length > 0) {
// Use variation settings
@@ -1059,6 +1117,7 @@ class YotoCoversApp {
cardTitlePanel = variation && variation.titlePanel ? variation.titlePanel : { borderWidth: 0, rotation: 0, expand: false };
cardImagePanel = variation && variation.imagePanel ? variation.imagePanel : { borderWidth: 0, rotation: 0 };
cardFooterPanel = variation && variation.footerPanel ? variation.footerPanel : { borderWidth: 0, rotation: 0 };
cardFit = variation && variation.cardFit !== undefined ? variation.cardFit : this.printLayout.cardFit;
} else {
// No variations - auto-assign images
imageIndex = i < this.images.length ? i : 0;
@@ -1091,6 +1150,7 @@ class YotoCoversApp {
cardTitlePanel = { borderWidth: 0, rotation: 0, expand: false };
cardImagePanel = { borderWidth: 0, rotation: 0 };
cardFooterPanel = { borderWidth: 0, rotation: 0 };
cardFit = this.printLayout.cardFit;
}
const imageUrl = this.images.length > 0 ? this.images[imageIndex].dataUrl : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
@@ -1125,6 +1185,7 @@ class YotoCoversApp {
titlePanel: cardTitlePanel,
imagePanel: cardImagePanel,
footerPanel: cardFooterPanel,
cardFit: cardFit,
isVisible: true
});
} else {
@@ -1155,14 +1216,17 @@ class YotoCoversApp {
titlePanel: { borderWidth: 0, rotation: 0, expand: false },
imagePanel: { borderWidth: 0, rotation: 0 },
footerPanel: { borderWidth: 0, rotation: 0 },
cardFit: this.printLayout.cardFit,
isVisible: false
});
}
}
// Fixed card dimensions: 54mm x 85.6mm
const cardWidthMm = 54;
const cardHeightMm = 85.6;
const baseCardWidthMm = 54;
const baseCardHeightMm = 85.6;
const cardWidthMm = baseCardWidthMm;
const cardHeightMm = baseCardHeightMm;
// Calculate grid layout
const pageWidthMm = 210; // A4 width
@@ -1180,23 +1244,33 @@ class YotoCoversApp {
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) => `
const pageBreak = page < pagesNeeded - 1 ? "always" : "auto";
const gridColsValue = gridCols;
const cardWidthValue = cardWidthMm;
const cardHeightValue = cardHeightMm;
const spacingValue = this.printLayout.cardSpacing;
const widthValue = gridCols * cardWidthMm + (gridCols - 1) * this.printLayout.cardSpacing;
const heightValue = gridRows * cardHeightMm + (gridRows - 1) * this.printLayout.cardSpacing;
htmlContent += `<div class="print-page" style="page-break-after: ${pageBreak}; width: ${pageWidthMm}mm; padding: ${this.printLayout.pageMargin}mm; box-sizing: border-box;">
<div class="print-sheet" style="
display: grid;
grid-template-columns: repeat(${gridColsValue}, ${cardWidthValue}mm);
grid-template-rows: repeat(${gridRows}, ${cardHeightValue}mm);
gap: ${spacingValue}mm;
width: ${widthValue}mm;
height: ${heightValue}mm;
margin: 0;
padding: 0;
box-sizing: border-box;
">
${pageCards.map((card, index) => {
const cardFitFactor = card.cardFit || 0;
return `
<div class="card card-${startIndex + index} ${card.isVisible ? '' : 'empty-card'}"
data-card-index="${startIndex + index}"
data-background-style="${card.backgroundStyle}"
data-card-fit="${cardFitFactor}"
onclick="if (window.parent && window.parent.selectCardFromPreview) { window.parent.selectCardFromPreview(${startIndex + index}); }"
style="cursor: pointer;
width: ${cardWidthMm}mm;
@@ -1243,7 +1317,7 @@ class YotoCoversApp {
-webkit-text-stroke: #fff calc(var(--o));
text-transform: uppercase;
mix-blend-mode: screen;
font: clamp(1em, min(65vh, 35vw), 2em)/ 0.75 'Bebas Neue', sans-serif;
font: ${card.titleFontSize * 0.8}px/ 0.75 'Bebas Neue', sans-serif;
transform: rotate(${card.titlePanel.rotation}deg);
box-shadow: 2px 2px 4px rgba(0,0,0,0.2);
@supports (line-height: 1cap) { line-height: 1cap; }
@@ -1353,7 +1427,7 @@ class YotoCoversApp {
-webkit-text-stroke: #fff calc(var(--o));
text-transform: uppercase;
mix-blend-mode: screen;
font: clamp(2em, min(65vh, 35vw), 3.9em)/ 0.75 'Bebas Neue', sans-serif;
font: ${card.titleFontSize}px/ 0.75 'Bebas Neue', sans-serif;
${card.titleDraggable ? 'cursor: move; user-select: none;' : ''}
@supports (line-height: 1cap) { line-height: 1cap; }
}
@@ -1394,7 +1468,7 @@ class YotoCoversApp {
`}
` : ''}
</div>
`).join('')}
`;}).join('')}
</div>
</div>`;
}
@@ -1427,6 +1501,14 @@ class YotoCoversApp {
.empty-card {
background: #f9f9f9;
}
@media print {
${cards.map((card, index) => {
const fitFactor = card.cardFit || 0;
const widthMm = 54 * (1 + fitFactor);
const heightMm = 85.6 * (1 + fitFactor);
return `.card-${index} { width: ${widthMm}mm !important; height: ${heightMm}mm !important; }`;
}).join('\n ')}
}
${cards.map((card, index) => `
.card-${index} .title {
color: ${card.titleColor};
@@ -1624,12 +1706,69 @@ class YotoCoversApp {
}, 5000);
}
zoomIn() {
if (this.previewZoom.level < this.previewZoom.max) {
this.previewZoom.level = Math.min(this.previewZoom.max, this.previewZoom.level + this.previewZoom.step);
this.updateZoom();
}
}
zoomOut() {
if (this.previewZoom.level > this.previewZoom.min) {
this.previewZoom.level = Math.max(this.previewZoom.min, this.previewZoom.level - this.previewZoom.step);
this.updateZoom();
}
}
resetZoom() {
this.previewZoom.level = 1.0;
this.updateZoom();
}
updateZoom() {
const iframe = document.getElementById('preview-iframe');
const zoomLevel = document.getElementById('zoom-level');
if (iframe) {
iframe.style.transform = `scale(${this.previewZoom.level})`;
iframe.style.transformOrigin = 'top left';
}
if (zoomLevel) {
zoomLevel.textContent = Math.round(this.previewZoom.level * 100) + '%';
}
}
toggleFullscreen() {
const container = document.querySelector('.preview-container');
const overlay = document.getElementById('fullscreen-overlay');
const btn = document.getElementById('fullscreen-btn');
if (this.isFullscreen) {
// Exit fullscreen
container.classList.remove('fullscreen');
overlay.classList.remove('active');
btn.textContent = '⛶ Full Screen';
this.isFullscreen = false;
} else {
// Enter fullscreen
container.classList.add('fullscreen');
overlay.classList.add('active');
btn.textContent = '⛶ Exit Full Screen';
this.isFullscreen = true;
// Add click handler to overlay to exit fullscreen
overlay.onclick = () => this.toggleFullscreen();
}
}
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;
document.getElementById('global-card-fit').value = this.printLayout.cardFit;
}
}