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)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
images = models.JSONField(default=list, blank=True)
class Stroke(models.Model):
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('redirect-after-login/', views.login_redirect, name='login_redirect'),
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 functools import wraps
from .forms import RegisterForm
from django.core.files.storage import FileSystemStorage
import uuid
from django.conf import settings
def email_verified_required(view_func):
@ -718,10 +721,20 @@ def whiteboard_view(request, whiteboard_id=None):
if whiteboard_id:
whiteboard = get_object_or_404(Whiteboard, id=whiteboard_id, owner=request.user)
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]
context = {
'whiteboard': whiteboard,
'strokes_json': json.dumps(stroke_data),
'images_json': images_json
}
return render(request, 'whiteboard.html', context)
else:
@ -877,3 +890,51 @@ def pin_message(request, message_id):
msg.pinned = not msg.pinned
msg.save()
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;">
Export as PNG
</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>
<h2 style="text-align: center;">{{ whiteboard.title }}</h2>
<canvas id="whiteboard" width="800" height="600"></canvas>
<input type="file" id="imageUpload" accept="image/*" />
<script>
let imagesOnCanvas = []; // Stores added image objects
let draggingImage = null;
let dragOffsetX = 0;
let dragOffsetY = 0;
let scalingImage = null;
const resizeMargin = 10;
document.getElementById('imageUpload').addEventListener('change', function (e) {
const file = e.target.files[0];
if (!file) return;
const file = e.target.files[0];
if (!file) return;
const img = new Image();
img.onload = () => {
const imageObj = {
img,
x: 100,
y: 100,
width: img.width * 0.3,
height: img.height * 0.3,
};
imagesOnCanvas.push(imageObj);
redrawCanvas();
};
img.src = URL.createObjectURL(file);
const formData = new FormData();
formData.append('image', file);
fetch(`/whiteboard/{{ whiteboard.id }}/upload-image/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
},
body: formData,
})
.then(res => res.json())
.then(data => {
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() {
const canvas = document.getElementById('whiteboard');
@ -202,8 +226,8 @@
return cookieValue;
}
// Parse saved strokes from Django context
const savedStrokes = JSON.parse('{{ strokes_json|escapejs }}');
const savedImages = JSON.parse('{{ images_json|escapejs }}');
const canvas = document.getElementById('whiteboard');
const ctx = canvas.getContext('2d');
@ -224,8 +248,27 @@
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
savedStrokes.forEach(drawStroke);
loadSavedImages();
let drawing = false;
let currentStroke = [];
@ -264,16 +307,25 @@
for (let i = imagesOnCanvas.length - 1; i >= 0; i--) {
const img = imagesOnCanvas[i];
if (
x >= img.x &&
x <= img.x + img.width &&
y >= img.y &&
y <= img.y + img.height
) {
draggingImage = img;
dragOffsetX = x - img.x;
dragOffsetY = y - img.y;
return; // Dont start drawing if dragging an image
const withinX = x >= img.x && x <= img.x + img.width;
const withinY = y >= img.y && y <= img.y + img.height;
const nearBottomRight = (
x >= img.x + img.width - resizeMargin &&
x <= img.x + img.width &&
y >= img.y + img.height - resizeMargin &&
y <= img.y + img.height
);
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
for (const imgObj of imagesOnCanvas) {
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
@ -323,6 +383,16 @@
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;
const point = { x, y };
@ -340,6 +410,8 @@
});
canvas.addEventListener('mouseup', () => {
draggingImage = null;
scalingImage = null;
if (draggingImage) {
draggingImage = null;
return;
@ -348,9 +420,12 @@
if (!drawing) return;
drawing = false;
saveStrokeToServer(currentStroke);
saveImagesToServer();
});
canvas.addEventListener('mouseleave', () => {
draggingImage = null;
scalingImage = null;
if (draggingImage) {
draggingImage = null;
return;
@ -383,6 +458,34 @@
})
.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>
</div>