Update app_enhanced.py
Browse files- app_enhanced.py +105 -19
app_enhanced.py
CHANGED
|
@@ -386,7 +386,16 @@ INDEX_HTML = '''
|
|
| 386 |
.comic-grid { width: 100%; height: 100%; position: relative; background: #ffffff; }
|
| 387 |
|
| 388 |
/* Panel Background White */
|
| 389 |
-
.panel {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
.panel.selected { z-index: 20; }
|
| 391 |
.panel.selected img { outline: 3px solid #2196F3; outline-offset: -3px; }
|
| 392 |
|
|
@@ -438,12 +447,17 @@ INDEX_HTML = '''
|
|
| 438 |
/*
|
| 439 |
IMAGE FIT: 'contain' ensures full image is visible (100% fit) without crop.
|
| 440 |
Gaps are handled by the white background of the panel/grid.
|
|
|
|
| 441 |
*/
|
| 442 |
.panel img {
|
| 443 |
-
width: 100%;
|
|
|
|
| 444 |
object-fit: contain; /* DEFAULT: Show full image, padded with white */
|
| 445 |
-
|
| 446 |
-
transform
|
|
|
|
|
|
|
|
|
|
| 447 |
}
|
| 448 |
/* Toggle classes */
|
| 449 |
.panel img.fit-cover { object-fit: cover !important; }
|
|
@@ -786,7 +800,7 @@ INDEX_HTML = '''
|
|
| 786 |
img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
|
| 787 |
|
| 788 |
// RESTORE FIT STATE
|
| 789 |
-
|
| 790 |
|
| 791 |
updateImageTransform(img);
|
| 792 |
img.onmousedown = (e) => startPan(e, img);
|
|
@@ -885,6 +899,7 @@ INDEX_HTML = '''
|
|
| 885 |
document.addEventListener('mousemove', (e) => {
|
| 886 |
if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
|
| 887 |
if(isResizing && selectedBubble) { resizeBubble(e); }
|
|
|
|
| 888 |
if(isPanning && selectedPanel) { panImage(e); }
|
| 889 |
});
|
| 890 |
|
|
@@ -974,25 +989,86 @@ INDEX_HTML = '''
|
|
| 974 |
document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
|
| 975 |
document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
|
| 976 |
|
| 977 |
-
function handleZoom(el) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
document.getElementById('zoom-slider').addEventListener('change', () => saveDraft(true));
|
| 979 |
-
function startPan(e, img) {
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
|
| 984 |
function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
|
| 985 |
async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 986 |
async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 987 |
|
| 988 |
-
// NEW: Toggle between '
|
| 989 |
function toggleFitCover() {
|
| 990 |
if(!selectedPanel) return alert("Select a panel");
|
| 991 |
const img = selectedPanel.querySelector('img');
|
| 992 |
-
const current = img.style.objectFit || 'contain';
|
| 993 |
-
if(current === 'contain')
|
| 994 |
-
|
| 995 |
-
else
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 996 |
saveDraft(true);
|
| 997 |
}
|
| 998 |
|
|
@@ -1005,6 +1081,13 @@ INDEX_HTML = '''
|
|
| 1005 |
if(selectedPanel) selectedPanel.classList.remove('selected');
|
| 1006 |
alert(`Exporting ${pgs.length} page(s)...`);
|
| 1007 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
// --- 0% ERROR FIX ---
|
| 1009 |
// 1. Lock specific pixel dimensions + 1px buffer to prevent word wrapping
|
| 1010 |
const bubbles = document.querySelectorAll('.speech-bubble');
|
|
@@ -1022,8 +1105,8 @@ INDEX_HTML = '''
|
|
| 1022 |
for(let i = 0; i < pgs.length; i++) {
|
| 1023 |
try {
|
| 1024 |
const u = await htmlToImage.toPng(pgs[i], {
|
| 1025 |
-
pixelRatio: 2, // High quality
|
| 1026 |
-
style: { transform: 'none' }
|
| 1027 |
});
|
| 1028 |
const a = document.createElement('a');
|
| 1029 |
a.href = u;
|
|
@@ -1035,8 +1118,11 @@ INDEX_HTML = '''
|
|
| 1035 |
}
|
| 1036 |
}
|
| 1037 |
|
| 1038 |
-
//
|
| 1039 |
-
|
|
|
|
|
|
|
|
|
|
| 1040 |
}
|
| 1041 |
|
| 1042 |
function goBackToUpload() { if(confirm('Go home? Unsaved changes will be lost.')) { document.getElementById('editor-container').style.display = 'none'; document.getElementById('upload-container').style.display = 'flex'; document.getElementById('loading-view').style.display = 'none'; } }
|
|
|
|
| 386 |
.comic-grid { width: 100%; height: 100%; position: relative; background: #ffffff; }
|
| 387 |
|
| 388 |
/* Panel Background White */
|
| 389 |
+
.panel {
|
| 390 |
+
position: absolute;
|
| 391 |
+
overflow: hidden;
|
| 392 |
+
background: #ffffff;
|
| 393 |
+
cursor: pointer;
|
| 394 |
+
border: 0;
|
| 395 |
+
display:flex;
|
| 396 |
+
justify-content:center;
|
| 397 |
+
align-items:center; /* Center image within the panel if object-fit: contain */
|
| 398 |
+
}
|
| 399 |
.panel.selected { z-index: 20; }
|
| 400 |
.panel.selected img { outline: 3px solid #2196F3; outline-offset: -3px; }
|
| 401 |
|
|
|
|
| 447 |
/*
|
| 448 |
IMAGE FIT: 'contain' ensures full image is visible (100% fit) without crop.
|
| 449 |
Gaps are handled by the white background of the panel/grid.
|
| 450 |
+
Removed all transform:scale from here.
|
| 451 |
*/
|
| 452 |
.panel img {
|
| 453 |
+
width: 100%;
|
| 454 |
+
height: 100%;
|
| 455 |
object-fit: contain; /* DEFAULT: Show full image, padded with white */
|
| 456 |
+
image-rendering: auto; /* Ensures no blur from scaling */
|
| 457 |
+
transform: none !important; /* Force no JS transforms on image itself */
|
| 458 |
+
max-width: 100%; /* Force original size */
|
| 459 |
+
max-height: 100%; /* Force original size */
|
| 460 |
+
display: block; /* Important for proper centering and sizing */
|
| 461 |
}
|
| 462 |
/* Toggle classes */
|
| 463 |
.panel img.fit-cover { object-fit: cover !important; }
|
|
|
|
| 800 |
img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
|
| 801 |
|
| 802 |
// RESTORE FIT STATE
|
| 803 |
+
img.style.objectFit = pan.fit || 'contain'; // Default to 'contain' to avoid initial crop
|
| 804 |
|
| 805 |
updateImageTransform(img);
|
| 806 |
img.onmousedown = (e) => startPan(e, img);
|
|
|
|
| 899 |
document.addEventListener('mousemove', (e) => {
|
| 900 |
if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
|
| 901 |
if(isResizing && selectedBubble) { resizeBubble(e); }
|
| 902 |
+
// No panning when object-fit is contain (unless zoomed manually via slider)
|
| 903 |
if(isPanning && selectedPanel) { panImage(e); }
|
| 904 |
});
|
| 905 |
|
|
|
|
| 989 |
document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
|
| 990 |
document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
|
| 991 |
|
| 992 |
+
function handleZoom(el) {
|
| 993 |
+
if(!selectedPanel) return;
|
| 994 |
+
const img = selectedPanel.querySelector('img');
|
| 995 |
+
img.dataset.zoom = el.value;
|
| 996 |
+
// Only apply transform:scale if zoom > 100 and object-fit is NOT contain/fill
|
| 997 |
+
if(parseFloat(el.value) > 100 && img.style.objectFit !== 'contain' && img.style.objectFit !== 'fill') {
|
| 998 |
+
updateImageTransform(img);
|
| 999 |
+
} else if (parseFloat(el.value) === 100) {
|
| 1000 |
+
img.style.transform = 'none'; // Reset transform if zoom is 100
|
| 1001 |
+
img.dataset.translateX = 0; // Reset pan as well
|
| 1002 |
+
img.dataset.translateY = 0;
|
| 1003 |
+
}
|
| 1004 |
+
}
|
| 1005 |
document.getElementById('zoom-slider').addEventListener('change', () => saveDraft(true));
|
| 1006 |
+
function startPan(e, img) {
|
| 1007 |
+
// Only allow pan if image is 'cover' or zoomed in
|
| 1008 |
+
if((img.style.objectFit === 'contain' && parseFloat(img.dataset.zoom || 100) <= 100) || img.style.objectFit === 'fill') return;
|
| 1009 |
+
e.preventDefault();
|
| 1010 |
+
isPanning = true;
|
| 1011 |
+
selectedPanel = img.closest('.panel');
|
| 1012 |
+
panStartX = e.clientX;
|
| 1013 |
+
panStartY = e.clientY;
|
| 1014 |
+
panStartTx = parseFloat(img.dataset.translateX || 0);
|
| 1015 |
+
panStartTy = parseFloat(img.dataset.translateY || 0);
|
| 1016 |
+
img.classList.add('panning');
|
| 1017 |
+
}
|
| 1018 |
+
function panImage(e) {
|
| 1019 |
+
if(!isPanning || !selectedPanel) return;
|
| 1020 |
+
const img = selectedPanel.querySelector('img');
|
| 1021 |
+
img.dataset.translateX = panStartTx + (e.clientX - panStartX);
|
| 1022 |
+
img.dataset.translateY = panStartTy + (e.clientY - panStartY);
|
| 1023 |
+
updateImageTransform(img);
|
| 1024 |
+
}
|
| 1025 |
+
function updateImageTransform(img) {
|
| 1026 |
+
// THIS FUNCTION IS NOW MODIFIED TO ONLY APPLY TRANSFORM IF NECESSARY
|
| 1027 |
+
const z = (img.dataset.zoom || 100) / 100;
|
| 1028 |
+
const x = img.dataset.translateX || 0;
|
| 1029 |
+
const y = img.dataset.translateY || 0;
|
| 1030 |
+
|
| 1031 |
+
// If zoom is 100 and object-fit is 'contain' or 'fill', don't apply transform
|
| 1032 |
+
if (z === 1 && (img.style.objectFit === 'contain' || img.style.objectFit === 'fill')) {
|
| 1033 |
+
img.style.transform = 'none';
|
| 1034 |
+
} else {
|
| 1035 |
+
img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`;
|
| 1036 |
+
}
|
| 1037 |
+
img.classList.toggle('pannable', z > 1 || img.style.objectFit === 'cover');
|
| 1038 |
+
}
|
| 1039 |
+
function resetPanelTransform() {
|
| 1040 |
+
if(!selectedPanel) return alert("Select a panel");
|
| 1041 |
+
const img = selectedPanel.querySelector('img');
|
| 1042 |
+
img.dataset.zoom = 100;
|
| 1043 |
+
img.dataset.translateX = 0;
|
| 1044 |
+
img.dataset.translateY = 0;
|
| 1045 |
+
document.getElementById('zoom-slider').value = 100;
|
| 1046 |
+
updateImageTransform(img);
|
| 1047 |
+
saveDraft(true);
|
| 1048 |
+
}
|
| 1049 |
|
| 1050 |
function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
|
| 1051 |
async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 1052 |
async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 1053 |
|
| 1054 |
+
// NEW: Toggle between 'contain' (no crop/zoom, but bars), 'fill' (stretch, no bars), and 'cover' (crop, no bars)
|
| 1055 |
function toggleFitCover() {
|
| 1056 |
if(!selectedPanel) return alert("Select a panel");
|
| 1057 |
const img = selectedPanel.querySelector('img');
|
| 1058 |
+
const current = img.style.objectFit || 'contain'; // Default if not set
|
| 1059 |
+
if(current === 'contain') {
|
| 1060 |
+
img.style.objectFit = 'fill'; // Stretch to fill
|
| 1061 |
+
} else if(current === 'fill') {
|
| 1062 |
+
img.style.objectFit = 'cover'; // Fill, but crop to maintain aspect ratio
|
| 1063 |
+
} else { // current === 'cover'
|
| 1064 |
+
img.style.objectFit = 'contain'; // Show full image
|
| 1065 |
+
}
|
| 1066 |
+
// Reset transforms when toggling object-fit to avoid weird interactions
|
| 1067 |
+
img.dataset.zoom = 100;
|
| 1068 |
+
img.dataset.translateX = 0;
|
| 1069 |
+
img.dataset.translateY = 0;
|
| 1070 |
+
document.getElementById('zoom-slider').value = 100;
|
| 1071 |
+
updateImageTransform(img); // Re-evaluate transform based on new object-fit
|
| 1072 |
saveDraft(true);
|
| 1073 |
}
|
| 1074 |
|
|
|
|
| 1081 |
if(selectedPanel) selectedPanel.classList.remove('selected');
|
| 1082 |
alert(`Exporting ${pgs.length} page(s)...`);
|
| 1083 |
|
| 1084 |
+
// --- PREPARE FOR EXPORT: Remove editor scaling ---
|
| 1085 |
+
const comicPages = document.querySelectorAll('.comic-page');
|
| 1086 |
+
comicPages.forEach(page => {
|
| 1087 |
+
page.style.transform = 'none'; // Remove scaling from comic-page
|
| 1088 |
+
page.style.marginBottom = '0'; // Remove margin compensation
|
| 1089 |
+
});
|
| 1090 |
+
|
| 1091 |
// --- 0% ERROR FIX ---
|
| 1092 |
// 1. Lock specific pixel dimensions + 1px buffer to prevent word wrapping
|
| 1093 |
const bubbles = document.querySelectorAll('.speech-bubble');
|
|
|
|
| 1105 |
for(let i = 0; i < pgs.length; i++) {
|
| 1106 |
try {
|
| 1107 |
const u = await htmlToImage.toPng(pgs[i], {
|
| 1108 |
+
pixelRatio: 2, // High quality for print
|
| 1109 |
+
style: { transform: 'none' } // Ensure no transforms for html-to-image
|
| 1110 |
});
|
| 1111 |
const a = document.createElement('a');
|
| 1112 |
a.href = u;
|
|
|
|
| 1118 |
}
|
| 1119 |
}
|
| 1120 |
|
| 1121 |
+
// --- RESTORE EDITOR SCALING AFTER EXPORT ---
|
| 1122 |
+
comicPages.forEach(page => {
|
| 1123 |
+
page.style.transform = 'scale(0.6)';
|
| 1124 |
+
page.style.marginBottom = '-280px';
|
| 1125 |
+
});
|
| 1126 |
}
|
| 1127 |
|
| 1128 |
function goBackToUpload() { if(confirm('Go home? Unsaved changes will be lost.')) { document.getElementById('editor-container').style.display = 'none'; document.getElementById('upload-container').style.display = 'flex'; document.getElementById('loading-view').style.display = 'none'; } }
|