Update app_enhanced.py
Browse files- app_enhanced.py +188 -31
app_enhanced.py
CHANGED
|
@@ -545,11 +545,12 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 545 |
<div class="edit-controls">
|
| 546 |
<h4>✏️ Interactive Editor</h4>
|
| 547 |
<div class="control-group">
|
| 548 |
-
<label for="bubble-type-select">Bubble Tools
|
| 549 |
<select id="bubble-type-select" onchange="changeBubbleType(this.value)">
|
| 550 |
<option value="speech">Speech</option><option value="thought">Thought</option><option value="reaction">Reaction</option><option value="narration">Narration</option><option value="idea">Idea</option>
|
| 551 |
</select>
|
| 552 |
<button onclick="rotateBubbleTail()" class="secondary-button">🔄 Rotate Tail</button>
|
|
|
|
| 553 |
</div>
|
| 554 |
<div class="control-group">
|
| 555 |
<label>Panel Tools (Select Panel):</label>
|
|
@@ -563,7 +564,7 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 563 |
<label>Zoom & Pan (Select Panel):</label>
|
| 564 |
<div class="zoom-controls">
|
| 565 |
<button onclick="resetPanelTransform()" class="secondary-button" style="padding: 4px 6px;">Reset</button>
|
| 566 |
-
<input type="range" id="zoom-slider" min="100" max="300" value="100" step="5">
|
| 567 |
</div>
|
| 568 |
</div>
|
| 569 |
<div class="control-group">
|
|
@@ -635,9 +636,9 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 635 |
|
| 636 |
function initializePanelImageEvents(img) {
|
| 637 |
img.addEventListener('mousedown', startPan);
|
| 638 |
-
document.addEventListener('mousemove', panImage);
|
| 639 |
document.addEventListener('mouseup', stopPan);
|
| 640 |
-
document.addEventListener('mouseleave', stopPan);
|
| 641 |
}
|
| 642 |
|
| 643 |
function initializeBubbleEvents(bubble) {
|
|
@@ -668,9 +669,43 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 668 |
return bubbleDiv;
|
| 669 |
}
|
| 670 |
|
| 671 |
-
function applyBubbleType(bubble, type) {
|
| 672 |
-
|
| 673 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
|
| 675 |
function selectPanel(panel) {
|
| 676 |
document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
|
|
@@ -678,7 +713,6 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 678 |
currentlySelectedPanel = panel;
|
| 679 |
selectBubble(null);
|
| 680 |
|
| 681 |
-
// Update zoom slider to reflect selected panel's state
|
| 682 |
const img = currentlySelectedPanel.querySelector('img');
|
| 683 |
const zoomSlider = document.getElementById('zoom-slider');
|
| 684 |
zoomSlider.value = img.dataset.zoom || 100;
|
|
@@ -690,34 +724,141 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 690 |
currentlySelectedBubble = bubble;
|
| 691 |
if (currentlySelectedBubble) {
|
| 692 |
currentlySelectedBubble.classList.add('selected');
|
| 693 |
-
|
|
|
|
| 694 |
document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
|
| 695 |
document.getElementById('zoom-slider').disabled = true;
|
| 696 |
}
|
| 697 |
}
|
| 698 |
|
| 699 |
-
function editBubbleText(bubble) {
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
|
| 710 |
function updateImageTransform(img) {
|
| 711 |
const zoom = (img.dataset.zoom || 100) / 100;
|
| 712 |
const x = img.dataset.translateX || 0;
|
| 713 |
const y = img.dataset.translateY || 0;
|
| 714 |
img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${zoom})`;
|
| 715 |
-
|
| 716 |
-
if (zoom > 1) {
|
| 717 |
-
img.classList.add('pannable');
|
| 718 |
-
} else {
|
| 719 |
-
img.classList.remove('pannable');
|
| 720 |
-
}
|
| 721 |
}
|
| 722 |
|
| 723 |
function handleZoom(event) {
|
|
@@ -728,7 +869,7 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 728 |
}
|
| 729 |
|
| 730 |
function resetPanelTransform() {
|
| 731 |
-
if (!currentlySelectedPanel) return;
|
| 732 |
const img = currentlySelectedPanel.querySelector('img');
|
| 733 |
img.dataset.zoom = 100;
|
| 734 |
img.dataset.translateX = 0;
|
|
@@ -738,13 +879,11 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 738 |
}
|
| 739 |
|
| 740 |
function startPan(event) {
|
| 741 |
-
// Only pan if left mouse button is clicked and image is zoomed
|
| 742 |
if (event.button !== 0) return;
|
| 743 |
const img = event.target;
|
| 744 |
const zoom = parseFloat(img.dataset.zoom || 100);
|
| 745 |
if (zoom <= 100) return;
|
| 746 |
-
|
| 747 |
-
event.preventDefault(); // Prevent default image drag behavior
|
| 748 |
isPanning = true;
|
| 749 |
img.classList.add('panning');
|
| 750 |
panStartX = event.clientX;
|
|
@@ -767,9 +906,27 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 767 |
if (!isPanning) return;
|
| 768 |
isPanning = false;
|
| 769 |
if (currentlySelectedPanel) {
|
| 770 |
-
|
| 771 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 773 |
}
|
| 774 |
</script>
|
| 775 |
</body>
|
|
|
|
| 545 |
<div class="edit-controls">
|
| 546 |
<h4>✏️ Interactive Editor</h4>
|
| 547 |
<div class="control-group">
|
| 548 |
+
<label for="bubble-type-select">Bubble Tools:</label>
|
| 549 |
<select id="bubble-type-select" onchange="changeBubbleType(this.value)">
|
| 550 |
<option value="speech">Speech</option><option value="thought">Thought</option><option value="reaction">Reaction</option><option value="narration">Narration</option><option value="idea">Idea</option>
|
| 551 |
</select>
|
| 552 |
<button onclick="rotateBubbleTail()" class="secondary-button">🔄 Rotate Tail</button>
|
| 553 |
+
<button onclick="addBubbleToPanel()" class="action-button">💬 Add Bubble</button>
|
| 554 |
</div>
|
| 555 |
<div class="control-group">
|
| 556 |
<label>Panel Tools (Select Panel):</label>
|
|
|
|
| 564 |
<label>Zoom & Pan (Select Panel):</label>
|
| 565 |
<div class="zoom-controls">
|
| 566 |
<button onclick="resetPanelTransform()" class="secondary-button" style="padding: 4px 6px;">Reset</button>
|
| 567 |
+
<input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" disabled>
|
| 568 |
</div>
|
| 569 |
</div>
|
| 570 |
<div class="control-group">
|
|
|
|
| 636 |
|
| 637 |
function initializePanelImageEvents(img) {
|
| 638 |
img.addEventListener('mousedown', startPan);
|
| 639 |
+
document.addEventListener('mousemove', panImage);
|
| 640 |
document.addEventListener('mouseup', stopPan);
|
| 641 |
+
document.addEventListener('mouseleave', stopPan);
|
| 642 |
}
|
| 643 |
|
| 644 |
function initializeBubbleEvents(bubble) {
|
|
|
|
| 669 |
return bubbleDiv;
|
| 670 |
}
|
| 671 |
|
| 672 |
+
function applyBubbleType(bubble, type) {
|
| 673 |
+
bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
|
| 674 |
+
let classesToKeep = 'speech-bubble';
|
| 675 |
+
if (bubble.classList.contains('selected')) classesToKeep += ' selected';
|
| 676 |
+
if (bubble.classList.contains('flipped')) classesToKeep += ' flipped';
|
| 677 |
+
if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
|
| 678 |
+
bubble.className = classesToKeep;
|
| 679 |
+
bubble.classList.add(type);
|
| 680 |
+
bubble.dataset.type = type;
|
| 681 |
+
if (type === 'thought') {
|
| 682 |
+
for (let i = 1; i <= 2; i++) {
|
| 683 |
+
const dot = document.createElement('div');
|
| 684 |
+
dot.className = `thought-dot thought-dot-${i}`;
|
| 685 |
+
bubble.appendChild(dot);
|
| 686 |
+
}
|
| 687 |
+
}
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
function changeBubbleType(type) {
|
| 691 |
+
if (!currentlySelectedBubble) return;
|
| 692 |
+
applyBubbleType(currentlySelectedBubble, type);
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
function rotateBubbleTail() {
|
| 696 |
+
if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
|
| 697 |
+
const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
|
| 698 |
+
const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
|
| 699 |
+
if (!isFlippedH && !isFlippedV) {
|
| 700 |
+
currentlySelectedBubble.classList.add('flipped');
|
| 701 |
+
} else if (isFlippedH && !isFlippedV) {
|
| 702 |
+
currentlySelectedBubble.classList.add('flipped-vertical');
|
| 703 |
+
} else if (isFlippedH && isFlippedV) {
|
| 704 |
+
currentlySelectedBubble.classList.remove('flipped');
|
| 705 |
+
} else {
|
| 706 |
+
currentlySelectedBubble.classList.remove('flipped-vertical');
|
| 707 |
+
}
|
| 708 |
+
}
|
| 709 |
|
| 710 |
function selectPanel(panel) {
|
| 711 |
document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
|
|
|
|
| 713 |
currentlySelectedPanel = panel;
|
| 714 |
selectBubble(null);
|
| 715 |
|
|
|
|
| 716 |
const img = currentlySelectedPanel.querySelector('img');
|
| 717 |
const zoomSlider = document.getElementById('zoom-slider');
|
| 718 |
zoomSlider.value = img.dataset.zoom || 100;
|
|
|
|
| 724 |
currentlySelectedBubble = bubble;
|
| 725 |
if (currentlySelectedBubble) {
|
| 726 |
currentlySelectedBubble.classList.add('selected');
|
| 727 |
+
if(currentlySelectedPanel) currentlySelectedPanel.classList.remove('selected');
|
| 728 |
+
currentlySelectedPanel = null;
|
| 729 |
document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
|
| 730 |
document.getElementById('zoom-slider').disabled = true;
|
| 731 |
}
|
| 732 |
}
|
| 733 |
|
| 734 |
+
function editBubbleText(bubble) {
|
| 735 |
+
if (currentlyEditing) return;
|
| 736 |
+
currentlyEditing = bubble;
|
| 737 |
+
const textSpan = bubble.querySelector('.bubble-text');
|
| 738 |
+
const currentText = textSpan.textContent;
|
| 739 |
+
textSpan.style.display = 'none';
|
| 740 |
+
bubble.style.height = 'auto';
|
| 741 |
+
const textarea = document.createElement('textarea');
|
| 742 |
+
textarea.value = currentText;
|
| 743 |
+
bubble.appendChild(textarea);
|
| 744 |
+
textarea.focus();
|
| 745 |
+
const finishEditing = () => {
|
| 746 |
+
textSpan.textContent = textarea.value;
|
| 747 |
+
bubble.removeChild(textarea);
|
| 748 |
+
textSpan.style.display = '';
|
| 749 |
+
currentlyEditing = null;
|
| 750 |
+
bubble.style.height = 'auto';
|
| 751 |
+
};
|
| 752 |
+
textarea.addEventListener('blur', finishEditing, { once: true });
|
| 753 |
+
textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
function startDrag(e) {
|
| 757 |
+
const bubble = e.target.closest('.speech-bubble');
|
| 758 |
+
if (!bubble || currentlyEditing) return;
|
| 759 |
+
draggedBubble = bubble;
|
| 760 |
+
selectBubble(bubble);
|
| 761 |
+
const rect = bubble.getBoundingClientRect();
|
| 762 |
+
offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
function drag(e) {
|
| 766 |
+
const parentRect = draggedBubble.parentElement.getBoundingClientRect();
|
| 767 |
+
let x = e.clientX - parentRect.left - offset.x;
|
| 768 |
+
let y = e.clientY - parentRect.top - offset.y;
|
| 769 |
+
draggedBubble.style.left = `${x}px`;
|
| 770 |
+
draggedBubble.style.top = `${y}px`;
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
function stopDrag() {
|
| 774 |
+
draggedBubble = null;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
function clearSavedState() {
|
| 778 |
+
if (confirm("Reset all edits to the original AI-generated comic?")) {
|
| 779 |
+
localStorage.removeItem('comicEditorState');
|
| 780 |
+
window.location.reload();
|
| 781 |
+
}
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
async function exportPagesToPNG() {
|
| 785 |
+
const pages = document.querySelectorAll('.comic-page');
|
| 786 |
+
if (pages.length === 0) return alert("No pages found.");
|
| 787 |
+
alert(`Starting export of ${pages.length} page(s).`);
|
| 788 |
+
for (let i = 0; i < pages.length; i++) {
|
| 789 |
+
try {
|
| 790 |
+
const canvas = await html2canvas(pages[i], { scale: 2 });
|
| 791 |
+
const link = document.createElement('a');
|
| 792 |
+
link.download = `comic-page-${i + 1}.png`;
|
| 793 |
+
link.href = canvas.toDataURL('image/png');
|
| 794 |
+
link.click();
|
| 795 |
+
} catch (err) {
|
| 796 |
+
alert(`Failed to export page ${i + 1}.`);
|
| 797 |
+
}
|
| 798 |
+
}
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
function replacePanelImage() {
|
| 802 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 803 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 804 |
+
const uploader = document.getElementById('image-uploader');
|
| 805 |
+
const oneTimeListener = (event) => {
|
| 806 |
+
const file = event.target.files[0];
|
| 807 |
+
if (!file) return;
|
| 808 |
+
const formData = new FormData();
|
| 809 |
+
formData.append('image', file);
|
| 810 |
+
img.style.opacity = '0.5';
|
| 811 |
+
fetch('/replace_panel', { method: 'POST', body: formData })
|
| 812 |
+
.then(response => response.json())
|
| 813 |
+
.then(data => {
|
| 814 |
+
if (data.success) {
|
| 815 |
+
img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
|
| 816 |
+
} else { alert('Error replacing image: ' + data.error); }
|
| 817 |
+
img.style.opacity = '1';
|
| 818 |
+
})
|
| 819 |
+
.catch(error => {
|
| 820 |
+
alert('An error occurred during the upload.');
|
| 821 |
+
img.style.opacity = '1';
|
| 822 |
+
});
|
| 823 |
+
uploader.removeEventListener('change', oneTimeListener);
|
| 824 |
+
uploader.value = '';
|
| 825 |
+
};
|
| 826 |
+
uploader.addEventListener('change', oneTimeListener, { once: true });
|
| 827 |
+
uploader.click();
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
function adjustFrame(direction) {
|
| 831 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first to adjust its frame."); return; }
|
| 832 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 833 |
+
const currentSrc = img.src;
|
| 834 |
+
let filename = currentSrc.substring(currentSrc.lastIndexOf('/') + 1);
|
| 835 |
+
if (filename.includes('?')) { filename = filename.split('?')[0]; }
|
| 836 |
+
img.style.opacity = '0.5';
|
| 837 |
+
fetch('/regenerate_frame', {
|
| 838 |
+
method: 'POST',
|
| 839 |
+
headers: { 'Content-Type': 'application/json' },
|
| 840 |
+
body: JSON.stringify({ filename: filename, direction: direction })
|
| 841 |
+
})
|
| 842 |
+
.then(response => response.json())
|
| 843 |
+
.then(data => {
|
| 844 |
+
if (data.success) {
|
| 845 |
+
img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
|
| 846 |
+
console.log(data.message);
|
| 847 |
+
} else { alert('Error: ' + data.message); }
|
| 848 |
+
img.style.opacity = '1';
|
| 849 |
+
})
|
| 850 |
+
.catch(error => {
|
| 851 |
+
alert('An error occurred during frame adjustment.');
|
| 852 |
+
img.style.opacity = '1';
|
| 853 |
+
});
|
| 854 |
+
}
|
| 855 |
|
| 856 |
function updateImageTransform(img) {
|
| 857 |
const zoom = (img.dataset.zoom || 100) / 100;
|
| 858 |
const x = img.dataset.translateX || 0;
|
| 859 |
const y = img.dataset.translateY || 0;
|
| 860 |
img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${zoom})`;
|
| 861 |
+
if (zoom > 1) { img.classList.add('pannable'); } else { img.classList.remove('pannable'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
}
|
| 863 |
|
| 864 |
function handleZoom(event) {
|
|
|
|
| 869 |
}
|
| 870 |
|
| 871 |
function resetPanelTransform() {
|
| 872 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 873 |
const img = currentlySelectedPanel.querySelector('img');
|
| 874 |
img.dataset.zoom = 100;
|
| 875 |
img.dataset.translateX = 0;
|
|
|
|
| 879 |
}
|
| 880 |
|
| 881 |
function startPan(event) {
|
|
|
|
| 882 |
if (event.button !== 0) return;
|
| 883 |
const img = event.target;
|
| 884 |
const zoom = parseFloat(img.dataset.zoom || 100);
|
| 885 |
if (zoom <= 100) return;
|
| 886 |
+
event.preventDefault();
|
|
|
|
| 887 |
isPanning = true;
|
| 888 |
img.classList.add('panning');
|
| 889 |
panStartX = event.clientX;
|
|
|
|
| 906 |
if (!isPanning) return;
|
| 907 |
isPanning = false;
|
| 908 |
if (currentlySelectedPanel) {
|
| 909 |
+
currentlySelectedPanel.querySelector('img').classList.remove('panning');
|
| 910 |
+
}
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
function addBubbleToPanel() {
|
| 914 |
+
if (!currentlySelectedPanel) {
|
| 915 |
+
alert("Please select a panel first to add a bubble to it.");
|
| 916 |
+
return;
|
| 917 |
}
|
| 918 |
+
|
| 919 |
+
const newBubble = createBubbleElement({
|
| 920 |
+
id: `new-bubble-${Date.now()}`,
|
| 921 |
+
text: 'New Text...',
|
| 922 |
+
left: '10%',
|
| 923 |
+
top: '10%'
|
| 924 |
+
});
|
| 925 |
+
|
| 926 |
+
currentlySelectedPanel.appendChild(newBubble);
|
| 927 |
+
initializeBubbleEvents(newBubble);
|
| 928 |
+
selectBubble(newBubble);
|
| 929 |
+
editBubbleText(newBubble);
|
| 930 |
}
|
| 931 |
</script>
|
| 932 |
</body>
|