feat: add zoom and fullscreen controls to preview, enhance card fit options
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+147
@@ -287,6 +287,8 @@
|
||||
height: 600px;
|
||||
border: none;
|
||||
display: block;
|
||||
transform-origin: top left;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
@@ -301,6 +303,118 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.zoom-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.zoom-btn:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.zoom-btn:active {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.zoom-level {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.fullscreen-btn:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.preview-container.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.preview-container.fullscreen .preview-iframe {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.preview-container.fullscreen::before {
|
||||
content: '✕ Close (ESC)';
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.preview-container.fullscreen:hover::before {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.fullscreen-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 9998;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fullscreen-overlay.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -612,6 +726,16 @@
|
||||
<label for="card-spacing">Card spacing (mm)</label>
|
||||
<input type="number" id="card-spacing" value="5" min="0" max="20">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="global-card-fit">Global card fit</label>
|
||||
<select id="global-card-fit">
|
||||
<option value="-0.02">Overfit (+2%)</option>
|
||||
<option value="0" selected>Exact fit</option>
|
||||
<option value="0.02">Underfit (-2%)</option>
|
||||
</select>
|
||||
<small style="color: #666; display: block; margin-top: 5px;">Adjust the print size for all cards (can be overridden per card)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -644,6 +768,16 @@
|
||||
<div class="panel preview-panel">
|
||||
<h3 style="margin-bottom: 20px;">Preview</h3>
|
||||
|
||||
<div class="preview-controls">
|
||||
<div class="zoom-controls">
|
||||
<button class="zoom-btn" id="zoom-out" title="Zoom Out">-</button>
|
||||
<span class="zoom-level" id="zoom-level">100%</span>
|
||||
<button class="zoom-btn" id="zoom-in" title="Zoom In">+</button>
|
||||
<button class="zoom-btn" id="zoom-reset" title="Reset Zoom">↻</button>
|
||||
</div>
|
||||
<button class="fullscreen-btn" id="fullscreen-btn" title="Full Screen (Press ESC to exit)">⛶ Full Screen</button>
|
||||
</div>
|
||||
|
||||
<div class="preview-container">
|
||||
<div class="preview-placeholder" id="preview-placeholder">
|
||||
Click "Generate Preview" to see your cover
|
||||
@@ -930,6 +1064,17 @@
|
||||
<input type="number" id="card-footer-font-size" value="12" min="8" max="100" step="2">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="card-fit">Card print fit override</label>
|
||||
<select id="card-fit">
|
||||
<option value="">Use global setting</option>
|
||||
<option value="-0.02">Overfit (+2%)</option>
|
||||
<option value="0">Exact fit</option>
|
||||
<option value="0.02">Underfit (-2%)</option>
|
||||
</select>
|
||||
<small style="color: #666; display: block; margin-top: 5px;">Override the global card fit setting for this card</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Footer Position</label>
|
||||
<div class="position-controls">
|
||||
@@ -988,6 +1133,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fullscreen-overlay" id="fullscreen-overlay"></div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user