whiteboard images wip

This commit is contained in:
Max Młynarczyk 2025-06-03 18:44:38 +02:00
parent 0b6f9d8e72
commit 42a694b24d
5 changed files with 215 additions and 29 deletions

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-06-03 16:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('eversyncc', '0025_message_pinned'),
]
operations = [
migrations.AddField(
model_name='whiteboard',
name='images',
field=models.JSONField(blank=True, default=list),
),
]

View file

@ -100,6 +100,7 @@ class Whiteboard(models.Model):
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
images = models.JSONField(default=list, blank=True)
class Stroke(models.Model): class Stroke(models.Model):
whiteboard = models.ForeignKey(Whiteboard, related_name='strokes', on_delete=models.CASCADE) whiteboard = models.ForeignKey(Whiteboard, related_name='strokes', on_delete=models.CASCADE)

View file

@ -66,6 +66,9 @@ urlpatterns = [
path('update-email/', views.update_email, name='update_email'), path('update-email/', views.update_email, name='update_email'),
path('redirect-after-login/', views.login_redirect, name='login_redirect'), path('redirect-after-login/', views.login_redirect, name='login_redirect'),
path('pin/<int:message_id>/', views.pin_message, name='pin_message'), path('pin/<int:message_id>/', views.pin_message, name='pin_message'),
path('whiteboard/<int:whiteboard_id>/save-images/', views.save_images, name='save_images'),
path('whiteboard/<int:whiteboard_id>/upload-image/', views.upload_image, name='upload_image'),

View file

@ -32,6 +32,9 @@ from .forms import EmailUpdateForm
from allauth.account.utils import send_email_confirmation from allauth.account.utils import send_email_confirmation
from functools import wraps from functools import wraps
from .forms import RegisterForm from .forms import RegisterForm
from django.core.files.storage import FileSystemStorage
import uuid
from django.conf import settings
def email_verified_required(view_func): def email_verified_required(view_func):
@ -718,10 +721,20 @@ def whiteboard_view(request, whiteboard_id=None):
if whiteboard_id: if whiteboard_id:
whiteboard = get_object_or_404(Whiteboard, id=whiteboard_id, owner=request.user) whiteboard = get_object_or_404(Whiteboard, id=whiteboard_id, owner=request.user)
strokes = Stroke.objects.filter(whiteboard=whiteboard).order_by('created_at') strokes = Stroke.objects.filter(whiteboard=whiteboard).order_by('created_at')
# Process images to include full URLs
images = []
for img_data in whiteboard.images:
if isinstance(img_data, dict) and 'id' in img_data:
img_data['url'] = request.build_absolute_uri(settings.MEDIA_URL + img_data['id'])
images.append(img_data)
images_json = json.dumps(images)
stroke_data = [stroke.data for stroke in strokes] stroke_data = [stroke.data for stroke in strokes]
context = { context = {
'whiteboard': whiteboard, 'whiteboard': whiteboard,
'strokes_json': json.dumps(stroke_data), 'strokes_json': json.dumps(stroke_data),
'images_json': images_json
} }
return render(request, 'whiteboard.html', context) return render(request, 'whiteboard.html', context)
else: else:
@ -877,3 +890,51 @@ def pin_message(request, message_id):
msg.pinned = not msg.pinned msg.pinned = not msg.pinned
msg.save() msg.save()
return redirect('chat_with_user', username=msg.receiver.username if msg.sender == request.user else msg.sender.username) return redirect('chat_with_user', username=msg.receiver.username if msg.sender == request.user else msg.sender.username)
@email_verified_required
@login_required
def save_images(request, whiteboard_id):
if request.method == 'POST':
try:
data = json.loads(request.body)
images = data.get('images', [])
whiteboard = Whiteboard.objects.get(id=whiteboard_id)
whiteboard.images = images
whiteboard.save()
return JsonResponse({'success': True})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
return JsonResponse({'success': False, 'error': 'Invalid method'})
@email_verified_required
@login_required
def upload_image(request, whiteboard_id):
if request.method == 'POST':
try:
whiteboard = Whiteboard.objects.get(id=whiteboard_id, owner=request.user)
image = request.FILES.get('image')
if not image:
return JsonResponse({'error': 'No image provided'}, status=400)
# Generate a unique filename
ext = os.path.splitext(image.name)[1]
filename = f'whiteboard_{whiteboard_id}_{uuid.uuid4().hex}{ext}'
# Save the image
fs = FileSystemStorage()
filename = fs.save(f'whiteboard_images/{filename}', image)
image_url = fs.url(filename)
return JsonResponse({
'success': True,
'image_url': image_url,
'image_id': filename
})
except Whiteboard.DoesNotExist:
return JsonResponse({'error': 'Whiteboard not found'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
return JsonResponse({'error': 'Invalid request method'}, status=405)

View file

@ -103,35 +103,59 @@
<button id="export-png" style="padding: 8px 12px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;"> <button id="export-png" style="padding: 8px 12px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
Export as PNG Export as PNG
</button> </button>
<label for="imageUpload" style="padding: 8px 12px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; display: inline-block;">
📷 Upload Image
</label>
<input type="file" id="imageUpload" accept="image/*" style="display: none;" />
</div> </div>
</div> </div>
<h2 style="text-align: center;">{{ whiteboard.title }}</h2> <h2 style="text-align: center;">{{ whiteboard.title }}</h2>
<canvas id="whiteboard" width="800" height="600"></canvas> <canvas id="whiteboard" width="800" height="600"></canvas>
<input type="file" id="imageUpload" accept="image/*" />
<script> <script>
let imagesOnCanvas = []; // Stores added image objects let imagesOnCanvas = []; // Stores added image objects
let draggingImage = null; let draggingImage = null;
let dragOffsetX = 0; let dragOffsetX = 0;
let dragOffsetY = 0; let dragOffsetY = 0;
let scalingImage = null;
const resizeMargin = 10;
document.getElementById('imageUpload').addEventListener('change', function (e) { document.getElementById('imageUpload').addEventListener('change', function (e) {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
const img = new Image(); const formData = new FormData();
img.onload = () => { formData.append('image', file);
const imageObj = {
img, fetch(`/whiteboard/{{ whiteboard.id }}/upload-image/`, {
x: 100, method: 'POST',
y: 100, headers: {
width: img.width * 0.3, 'X-CSRFToken': getCookie('csrftoken'),
height: img.height * 0.3, },
}; body: formData,
imagesOnCanvas.push(imageObj); })
redrawCanvas(); .then(res => res.json())
}; .then(data => {
img.src = URL.createObjectURL(file); if (data.error) {
alert('Image upload failed: ' + data.error);
} else {
const imageObj = {
img: new Image(),
x: 100,
y: 100,
width: 200,
height: 200,
id: data.image_id,
};
imageObj.img.onload = () => {
redrawCanvas();
saveImagesToServer();
};
imageObj.img.src = data.image_url;
imagesOnCanvas.push(imageObj);
}
})
.catch(err => alert('Upload error: ' + err));
}); });
document.getElementById('export-png').addEventListener('click', function() { document.getElementById('export-png').addEventListener('click', function() {
const canvas = document.getElementById('whiteboard'); const canvas = document.getElementById('whiteboard');
@ -202,8 +226,8 @@
return cookieValue; return cookieValue;
} }
// Parse saved strokes from Django context
const savedStrokes = JSON.parse('{{ strokes_json|escapejs }}'); const savedStrokes = JSON.parse('{{ strokes_json|escapejs }}');
const savedImages = JSON.parse('{{ images_json|escapejs }}');
const canvas = document.getElementById('whiteboard'); const canvas = document.getElementById('whiteboard');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -224,8 +248,27 @@
ctx.stroke(); ctx.stroke();
} }
function loadSavedImages() {
savedImages.forEach(imgData => {
const img = new Image();
img.onload = () => {
imagesOnCanvas.push({
img: img,
x: imgData.x,
y: imgData.y,
width: imgData.width,
height: imgData.height,
id: imgData.id
});
redrawCanvas();
};
img.src = imgData.url;
});
}
// Draw all saved strokes when the page loads // Draw all saved strokes when the page loads
savedStrokes.forEach(drawStroke); savedStrokes.forEach(drawStroke);
loadSavedImages();
let drawing = false; let drawing = false;
let currentStroke = []; let currentStroke = [];
@ -264,16 +307,25 @@
for (let i = imagesOnCanvas.length - 1; i >= 0; i--) { for (let i = imagesOnCanvas.length - 1; i >= 0; i--) {
const img = imagesOnCanvas[i]; const img = imagesOnCanvas[i];
if ( const withinX = x >= img.x && x <= img.x + img.width;
x >= img.x && const withinY = y >= img.y && y <= img.y + img.height;
x <= img.x + img.width &&
y >= img.y && const nearBottomRight = (
y <= img.y + img.height x >= img.x + img.width - resizeMargin &&
) { x <= img.x + img.width &&
draggingImage = img; y >= img.y + img.height - resizeMargin &&
dragOffsetX = x - img.x; y <= img.y + img.height
dragOffsetY = y - img.y; );
return; // Dont start drawing if dragging an image
if (withinX && withinY) {
if (nearBottomRight && e.shiftKey) {
scalingImage = img;
} else {
draggingImage = img;
dragOffsetX = x - img.x;
dragOffsetY = y - img.y;
}
return;
} }
} }
@ -307,6 +359,14 @@
// Draw images // Draw images
for (const imgObj of imagesOnCanvas) { for (const imgObj of imagesOnCanvas) {
ctx.drawImage(imgObj.img, imgObj.x, imgObj.y, imgObj.width, imgObj.height); ctx.drawImage(imgObj.img, imgObj.x, imgObj.y, imgObj.width, imgObj.height);
// Draw resize handle
ctx.fillStyle = '#800080';
ctx.fillRect(
imgObj.x + imgObj.width - resizeMargin,
imgObj.y + imgObj.height - resizeMargin,
resizeMargin,
resizeMargin
);
} }
// Draw strokes // Draw strokes
@ -323,6 +383,16 @@
return; return;
} }
if (scalingImage) {
const newWidth = x - scalingImage.x;
const newHeight = y - scalingImage.y;
if (newWidth > 10 && newHeight > 10) {
scalingImage.width = newWidth;
scalingImage.height = newHeight;
redrawCanvas();
}
return;
}
if (!drawing) return; if (!drawing) return;
const point = { x, y }; const point = { x, y };
@ -340,6 +410,8 @@
}); });
canvas.addEventListener('mouseup', () => { canvas.addEventListener('mouseup', () => {
draggingImage = null;
scalingImage = null;
if (draggingImage) { if (draggingImage) {
draggingImage = null; draggingImage = null;
return; return;
@ -348,9 +420,12 @@
if (!drawing) return; if (!drawing) return;
drawing = false; drawing = false;
saveStrokeToServer(currentStroke); saveStrokeToServer(currentStroke);
saveImagesToServer();
}); });
canvas.addEventListener('mouseleave', () => { canvas.addEventListener('mouseleave', () => {
draggingImage = null;
scalingImage = null;
if (draggingImage) { if (draggingImage) {
draggingImage = null; draggingImage = null;
return; return;
@ -383,6 +458,34 @@
}) })
.catch(err => console.error('Fetch error:', err)); .catch(err => console.error('Fetch error:', err));
} }
// Save images metadata to server
function saveImagesToServer() {
const imagesData = imagesOnCanvas.map(imgObj => ({
id: imgObj.id || null,
x: imgObj.x,
y: imgObj.y,
width: imgObj.width,
height: imgObj.height,
}));
fetch(`/whiteboard/{{ whiteboard.id }}/save-images/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'),
},
body: JSON.stringify({ images: imagesData }),
})
.then(res => res.json())
.then(data => {
if (data.error) {
console.error('Save images error:', data.error);
} else {
console.log('Images saved!');
}
})
.catch(err => console.error('Fetch error:', err));
}
</script> </script>
</div> </div>