Update app_enhanced.py
Browse files- app_enhanced.py +109 -46
app_enhanced.py
CHANGED
|
@@ -509,7 +509,7 @@ class EnhancedComicGenerator:
|
|
| 509 |
.panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; transition: transform 0.1s ease-out; }
|
| 510 |
.panel img.pannable { cursor: grab; }
|
| 511 |
.panel img.panning { cursor: grabbing; }
|
| 512 |
-
.speech-bubble { font-family: 'Comic Neue', cursive; position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; min-width: 50px;
|
| 513 |
.bubble-text { padding: 2px; word-wrap: break-word; }
|
| 514 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; }
|
| 515 |
.speech-bubble textarea { position: absolute; top: 0; left: 0; width: 100%; height: 100%; box-sizing: border-box; border: 1px solid #4CAF50; background: rgba(255,255,255,0.95); font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102; }
|
|
@@ -623,6 +623,7 @@ class EnhancedComicGenerator:
|
|
| 623 |
let currentlySelectedBubble = null;
|
| 624 |
let currentlySelectedPanel = null;
|
| 625 |
let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
|
|
|
|
| 626 |
|
| 627 |
function renderComic(data) {
|
| 628 |
const container = document.getElementById('comic-pages');
|
|
@@ -685,8 +686,6 @@ class EnhancedComicGenerator:
|
|
| 685 |
bubble.addEventListener('dblclick', e => { e.stopPropagation(); editBubbleText(bubble); });
|
| 686 |
bubble.addEventListener('mousedown', e => { e.stopPropagation(); startDrag(e); });
|
| 687 |
bubble.addEventListener('click', e => { e.stopPropagation(); selectBubble(bubble); });
|
| 688 |
-
|
| 689 |
-
// Add resize handles
|
| 690 |
['nw', 'ne', 'sw', 'se'].forEach(dir => {
|
| 691 |
const handle = document.createElement('div');
|
| 692 |
handle.className = `resize-handle ${dir}`;
|
|
@@ -762,20 +761,12 @@ class EnhancedComicGenerator:
|
|
| 762 |
panel.classList.add('selected');
|
| 763 |
currentlySelectedPanel = panel;
|
| 764 |
selectBubble(null);
|
| 765 |
-
|
| 766 |
-
const img = currentlySelectedPanel.querySelector('img');
|
| 767 |
-
document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
|
| 768 |
-
document.getElementById('zoom-slider').disabled = false;
|
| 769 |
}
|
| 770 |
|
| 771 |
function selectBubble(bubble) {
|
| 772 |
if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
|
| 773 |
currentlySelectedBubble = bubble;
|
| 774 |
-
const
|
| 775 |
-
const fillColorPicker = document.getElementById('bubble-fill-color');
|
| 776 |
-
const typeSelect = document.getElementById('bubble-type-select');
|
| 777 |
-
const fontSelect = document.getElementById('font-select');
|
| 778 |
-
const bubbleControls = [textColorPicker, fillColorPicker, typeSelect, fontSelect];
|
| 779 |
|
| 780 |
if (currentlySelectedBubble) {
|
| 781 |
currentlySelectedBubble.classList.add('selected');
|
|
@@ -783,15 +774,16 @@ class EnhancedComicGenerator:
|
|
| 783 |
currentlySelectedPanel = null;
|
| 784 |
|
| 785 |
const styles = window.getComputedStyle(currentlySelectedBubble);
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
|
| 791 |
document.getElementById('zoom-slider').disabled = true;
|
| 792 |
-
bubbleControls.forEach(
|
| 793 |
} else {
|
| 794 |
-
bubbleControls.forEach(
|
|
|
|
| 795 |
}
|
| 796 |
}
|
| 797 |
|
|
@@ -800,9 +792,8 @@ class EnhancedComicGenerator:
|
|
| 800 |
currentlyEditing = bubble;
|
| 801 |
const textSpan = bubble.querySelector('.bubble-text');
|
| 802 |
const textarea = document.createElement('textarea');
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
bubble.style.height = originalHeight;
|
| 806 |
|
| 807 |
textarea.value = textSpan.textContent;
|
| 808 |
bubble.appendChild(textarea);
|
|
@@ -812,9 +803,6 @@ class EnhancedComicGenerator:
|
|
| 812 |
textSpan.textContent = textarea.value;
|
| 813 |
bubble.removeChild(textarea);
|
| 814 |
textSpan.style.display = '';
|
| 815 |
-
// FIX: Re-apply height, then set to auto to allow natural flow
|
| 816 |
-
bubble.style.height = originalHeight;
|
| 817 |
-
setTimeout(() => { bubble.style.height = 'auto'; }, 0);
|
| 818 |
currentlyEditing = null;
|
| 819 |
};
|
| 820 |
textarea.addEventListener('blur', finishEditing, { once: true });
|
|
@@ -851,9 +839,6 @@ class EnhancedComicGenerator:
|
|
| 851 |
}
|
| 852 |
}
|
| 853 |
|
| 854 |
-
let isResizing = false;
|
| 855 |
-
let resizeHandle, originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
|
| 856 |
-
|
| 857 |
function startResize(e, dir) {
|
| 858 |
e.preventDefault();
|
| 859 |
e.stopPropagation();
|
|
@@ -875,25 +860,19 @@ class EnhancedComicGenerator:
|
|
| 875 |
const dy = e.clientY - originalMouseY;
|
| 876 |
const bubble = currentlySelectedBubble;
|
| 877 |
|
| 878 |
-
if (resizeHandle.includes('e')) {
|
| 879 |
-
bubble.style.width = `${originalWidth + dx}px`;
|
| 880 |
-
}
|
| 881 |
if (resizeHandle.includes('w')) {
|
| 882 |
bubble.style.width = `${originalWidth - dx}px`;
|
| 883 |
bubble.style.left = `${originalX + dx}px`;
|
| 884 |
}
|
| 885 |
-
if (resizeHandle.includes('s')) {
|
| 886 |
-
bubble.style.height = `${originalHeight + dy}px`;
|
| 887 |
-
}
|
| 888 |
if (resizeHandle.includes('n')) {
|
| 889 |
bubble.style.height = `${originalHeight - dy}px`;
|
| 890 |
bubble.style.top = `${originalY + dy}px`;
|
| 891 |
}
|
| 892 |
}
|
| 893 |
|
| 894 |
-
function stopResize() {
|
| 895 |
-
isResizing = false;
|
| 896 |
-
}
|
| 897 |
|
| 898 |
function clearSavedState() {
|
| 899 |
if (confirm("Reset all edits?")) {
|
|
@@ -903,7 +882,18 @@ class EnhancedComicGenerator:
|
|
| 903 |
}
|
| 904 |
|
| 905 |
async function exportPagesToPNG() {
|
| 906 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
}
|
| 908 |
|
| 909 |
function replacePanelImage() {
|
|
@@ -921,8 +911,8 @@ class EnhancedComicGenerator:
|
|
| 921 |
.then(data => {
|
| 922 |
if (data.success) {
|
| 923 |
img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
|
| 924 |
-
img.style.objectFit = 'contain';
|
| 925 |
-
resetPanelTransform();
|
| 926 |
} else { alert('Error replacing image: ' + data.error); }
|
| 927 |
img.style.opacity = '1';
|
| 928 |
})
|
|
@@ -936,15 +926,41 @@ class EnhancedComicGenerator:
|
|
| 936 |
}
|
| 937 |
|
| 938 |
function adjustFrame(direction) {
|
| 939 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 940 |
}
|
| 941 |
|
| 942 |
function updateImageTransform(img) {
|
| 943 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 944 |
}
|
| 945 |
|
| 946 |
function handleZoom(event) {
|
| 947 |
-
|
|
|
|
|
|
|
|
|
|
| 948 |
}
|
| 949 |
|
| 950 |
function resetPanelTransform() {
|
|
@@ -958,15 +974,30 @@ class EnhancedComicGenerator:
|
|
| 958 |
}
|
| 959 |
|
| 960 |
function startPan(event) {
|
| 961 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 962 |
}
|
| 963 |
|
| 964 |
function panImage(event) {
|
| 965 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 966 |
}
|
| 967 |
|
| 968 |
function stopPan() {
|
| 969 |
-
|
|
|
|
|
|
|
| 970 |
}
|
| 971 |
|
| 972 |
function addBubbleToPanel() {
|
|
@@ -984,7 +1015,39 @@ class EnhancedComicGenerator:
|
|
| 984 |
}
|
| 985 |
|
| 986 |
function gotoTimestamp() {
|
| 987 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
}
|
| 989 |
</script>
|
| 990 |
</body>
|
|
|
|
| 509 |
.panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; transition: transform 0.1s ease-out; }
|
| 510 |
.panel img.pannable { cursor: grab; }
|
| 511 |
.panel img.panning { cursor: grabbing; }
|
| 512 |
+
.speech-bubble { font-family: 'Comic Neue', cursive; position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box; padding: 8px; box-shadow: 2px 2px 5px rgba(0,0,0,0.3); z-index: 10; cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center; }
|
| 513 |
.bubble-text { padding: 2px; word-wrap: break-word; }
|
| 514 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; }
|
| 515 |
.speech-bubble textarea { position: absolute; top: 0; left: 0; width: 100%; height: 100%; box-sizing: border-box; border: 1px solid #4CAF50; background: rgba(255,255,255,0.95); font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102; }
|
|
|
|
| 623 |
let currentlySelectedBubble = null;
|
| 624 |
let currentlySelectedPanel = null;
|
| 625 |
let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
|
| 626 |
+
let isResizing = false, resizeHandle, originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
|
| 627 |
|
| 628 |
function renderComic(data) {
|
| 629 |
const container = document.getElementById('comic-pages');
|
|
|
|
| 686 |
bubble.addEventListener('dblclick', e => { e.stopPropagation(); editBubbleText(bubble); });
|
| 687 |
bubble.addEventListener('mousedown', e => { e.stopPropagation(); startDrag(e); });
|
| 688 |
bubble.addEventListener('click', e => { e.stopPropagation(); selectBubble(bubble); });
|
|
|
|
|
|
|
| 689 |
['nw', 'ne', 'sw', 'se'].forEach(dir => {
|
| 690 |
const handle = document.createElement('div');
|
| 691 |
handle.className = `resize-handle ${dir}`;
|
|
|
|
| 761 |
panel.classList.add('selected');
|
| 762 |
currentlySelectedPanel = panel;
|
| 763 |
selectBubble(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
}
|
| 765 |
|
| 766 |
function selectBubble(bubble) {
|
| 767 |
if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
|
| 768 |
currentlySelectedBubble = bubble;
|
| 769 |
+
const bubbleControls = ['bubble-text-color', 'bubble-fill-color', 'bubble-type-select', 'font-select'];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 770 |
|
| 771 |
if (currentlySelectedBubble) {
|
| 772 |
currentlySelectedBubble.classList.add('selected');
|
|
|
|
| 774 |
currentlySelectedPanel = null;
|
| 775 |
|
| 776 |
const styles = window.getComputedStyle(currentlySelectedBubble);
|
| 777 |
+
document.getElementById('bubble-text-color').value = rgbToHex(styles.color);
|
| 778 |
+
document.getElementById('bubble-fill-color').value = rgbToHex(styles.backgroundColor);
|
| 779 |
+
document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
|
| 780 |
+
document.getElementById('font-select').value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
|
| 781 |
|
| 782 |
document.getElementById('zoom-slider').disabled = true;
|
| 783 |
+
bubbleControls.forEach(id => document.getElementById(id).disabled = false);
|
| 784 |
} else {
|
| 785 |
+
bubbleControls.forEach(id => document.getElementById(id).disabled = true);
|
| 786 |
+
if(currentlySelectedPanel) document.getElementById('zoom-slider').disabled = false;
|
| 787 |
}
|
| 788 |
}
|
| 789 |
|
|
|
|
| 792 |
currentlyEditing = bubble;
|
| 793 |
const textSpan = bubble.querySelector('.bubble-text');
|
| 794 |
const textarea = document.createElement('textarea');
|
| 795 |
+
const originalHeight = bubble.offsetHeight;
|
| 796 |
+
bubble.style.height = `${originalHeight}px`;
|
|
|
|
| 797 |
|
| 798 |
textarea.value = textSpan.textContent;
|
| 799 |
bubble.appendChild(textarea);
|
|
|
|
| 803 |
textSpan.textContent = textarea.value;
|
| 804 |
bubble.removeChild(textarea);
|
| 805 |
textSpan.style.display = '';
|
|
|
|
|
|
|
|
|
|
| 806 |
currentlyEditing = null;
|
| 807 |
};
|
| 808 |
textarea.addEventListener('blur', finishEditing, { once: true });
|
|
|
|
| 839 |
}
|
| 840 |
}
|
| 841 |
|
|
|
|
|
|
|
|
|
|
| 842 |
function startResize(e, dir) {
|
| 843 |
e.preventDefault();
|
| 844 |
e.stopPropagation();
|
|
|
|
| 860 |
const dy = e.clientY - originalMouseY;
|
| 861 |
const bubble = currentlySelectedBubble;
|
| 862 |
|
| 863 |
+
if (resizeHandle.includes('e')) bubble.style.width = `${originalWidth + dx}px`;
|
|
|
|
|
|
|
| 864 |
if (resizeHandle.includes('w')) {
|
| 865 |
bubble.style.width = `${originalWidth - dx}px`;
|
| 866 |
bubble.style.left = `${originalX + dx}px`;
|
| 867 |
}
|
| 868 |
+
if (resizeHandle.includes('s')) bubble.style.height = `${originalHeight + dy}px`;
|
|
|
|
|
|
|
| 869 |
if (resizeHandle.includes('n')) {
|
| 870 |
bubble.style.height = `${originalHeight - dy}px`;
|
| 871 |
bubble.style.top = `${originalY + dy}px`;
|
| 872 |
}
|
| 873 |
}
|
| 874 |
|
| 875 |
+
function stopResize() { isResizing = false; }
|
|
|
|
|
|
|
| 876 |
|
| 877 |
function clearSavedState() {
|
| 878 |
if (confirm("Reset all edits?")) {
|
|
|
|
| 882 |
}
|
| 883 |
|
| 884 |
async function exportPagesToPNG() {
|
| 885 |
+
const pages = document.querySelectorAll('.comic-page');
|
| 886 |
+
if (pages.length === 0) return alert("No pages found.");
|
| 887 |
+
alert(`Starting export of ${pages.length} page(s).`);
|
| 888 |
+
for (let i = 0; i < pages.length; i++) {
|
| 889 |
+
try {
|
| 890 |
+
const canvas = await html2canvas(pages[i], { scale: 2 });
|
| 891 |
+
const link = document.createElement('a');
|
| 892 |
+
link.download = `comic-page-${i + 1}.png`;
|
| 893 |
+
link.href = canvas.toDataURL('image/png');
|
| 894 |
+
link.click();
|
| 895 |
+
} catch (err) { alert(`Failed to export page ${i + 1}.`); }
|
| 896 |
+
}
|
| 897 |
}
|
| 898 |
|
| 899 |
function replacePanelImage() {
|
|
|
|
| 911 |
.then(data => {
|
| 912 |
if (data.success) {
|
| 913 |
img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
|
| 914 |
+
img.style.objectFit = 'contain';
|
| 915 |
+
resetPanelTransform();
|
| 916 |
} else { alert('Error replacing image: ' + data.error); }
|
| 917 |
img.style.opacity = '1';
|
| 918 |
})
|
|
|
|
| 926 |
}
|
| 927 |
|
| 928 |
function adjustFrame(direction) {
|
| 929 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 930 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 931 |
+
let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
|
| 932 |
+
img.style.opacity = '0.5';
|
| 933 |
+
fetch('/regenerate_frame', {
|
| 934 |
+
method: 'POST',
|
| 935 |
+
headers: { 'Content-Type': 'application/json' },
|
| 936 |
+
body: JSON.stringify({ filename, direction })
|
| 937 |
+
})
|
| 938 |
+
.then(res => res.json())
|
| 939 |
+
.then(data => {
|
| 940 |
+
if (data.success) {
|
| 941 |
+
img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
|
| 942 |
+
} else { alert('Error: ' + data.message); }
|
| 943 |
+
img.style.opacity = '1';
|
| 944 |
+
})
|
| 945 |
+
.catch(() => {
|
| 946 |
+
alert('An error occurred.');
|
| 947 |
+
img.style.opacity = '1';
|
| 948 |
+
});
|
| 949 |
}
|
| 950 |
|
| 951 |
function updateImageTransform(img) {
|
| 952 |
+
const zoom = (img.dataset.zoom || 100) / 100;
|
| 953 |
+
const x = img.dataset.translateX || 0;
|
| 954 |
+
const y = img.dataset.translateY || 0;
|
| 955 |
+
img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${zoom})`;
|
| 956 |
+
img.classList.toggle('pannable', zoom > 1);
|
| 957 |
}
|
| 958 |
|
| 959 |
function handleZoom(event) {
|
| 960 |
+
if (!currentlySelectedPanel) return;
|
| 961 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 962 |
+
img.dataset.zoom = event.target.value;
|
| 963 |
+
updateImageTransform(img);
|
| 964 |
}
|
| 965 |
|
| 966 |
function resetPanelTransform() {
|
|
|
|
| 974 |
}
|
| 975 |
|
| 976 |
function startPan(event) {
|
| 977 |
+
if (event.button !== 0) return;
|
| 978 |
+
const img = event.target;
|
| 979 |
+
if (parseFloat(img.dataset.zoom || 100) <= 100) return;
|
| 980 |
+
event.preventDefault();
|
| 981 |
+
isPanning = true;
|
| 982 |
+
img.classList.add('panning');
|
| 983 |
+
panStartX = event.clientX;
|
| 984 |
+
panStartY = event.clientY;
|
| 985 |
+
panStartTranslateX = parseFloat(img.dataset.translateX || 0);
|
| 986 |
+
panStartTranslateY = parseFloat(img.dataset.translateY || 0);
|
| 987 |
}
|
| 988 |
|
| 989 |
function panImage(event) {
|
| 990 |
+
if (!isPanning || !currentlySelectedPanel) return;
|
| 991 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 992 |
+
img.dataset.translateX = panStartTranslateX + (event.clientX - panStartX);
|
| 993 |
+
img.dataset.translateY = panStartTranslateY + (event.clientY - panStartY);
|
| 994 |
+
updateImageTransform(img);
|
| 995 |
}
|
| 996 |
|
| 997 |
function stopPan() {
|
| 998 |
+
if (!isPanning) return;
|
| 999 |
+
isPanning = false;
|
| 1000 |
+
currentlySelectedPanel?.querySelector('img')?.classList.remove('panning');
|
| 1001 |
}
|
| 1002 |
|
| 1003 |
function addBubbleToPanel() {
|
|
|
|
| 1015 |
}
|
| 1016 |
|
| 1017 |
function gotoTimestamp() {
|
| 1018 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 1019 |
+
const input = document.getElementById('timestamp-input');
|
| 1020 |
+
const timeStr = input.value.trim();
|
| 1021 |
+
if (!timeStr) return;
|
| 1022 |
+
let parsedSeconds = 0;
|
| 1023 |
+
if (timeStr.includes(':')) {
|
| 1024 |
+
const parts = timeStr.split(':');
|
| 1025 |
+
parsedSeconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
|
| 1026 |
+
} else {
|
| 1027 |
+
parsedSeconds = parseFloat(timeStr);
|
| 1028 |
+
}
|
| 1029 |
+
if (isNaN(parsedSeconds)) { alert("Invalid time format."); return; }
|
| 1030 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 1031 |
+
let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
|
| 1032 |
+
img.style.opacity = '0.5';
|
| 1033 |
+
fetch('/goto_timestamp', {
|
| 1034 |
+
method: 'POST',
|
| 1035 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1036 |
+
body: JSON.stringify({ filename, timestamp: parsedSeconds })
|
| 1037 |
+
})
|
| 1038 |
+
.then(res => res.json())
|
| 1039 |
+
.then(data => {
|
| 1040 |
+
if (data.success) {
|
| 1041 |
+
img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
|
| 1042 |
+
input.value = '';
|
| 1043 |
+
resetPanelTransform();
|
| 1044 |
+
} else { alert('Error: ' + data.message); }
|
| 1045 |
+
img.style.opacity = '1';
|
| 1046 |
+
})
|
| 1047 |
+
.catch(() => {
|
| 1048 |
+
alert('An error occurred.');
|
| 1049 |
+
img.style.opacity = '1';
|
| 1050 |
+
});
|
| 1051 |
}
|
| 1052 |
</script>
|
| 1053 |
</body>
|