mirror of
https://github.com/rudy3333/eversync.git
synced 2025-07-01 08:36:02 +00:00
whiteboard images wip
This commit is contained in:
parent
0b6f9d8e72
commit
42a694b24d
5 changed files with 215 additions and 29 deletions
18
eversyncc/migrations/0026_whiteboard_images.py
Normal file
18
eversyncc/migrations/0026_whiteboard_images.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -100,7 +100,8 @@ 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)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
|
|
@ -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'),
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
@ -876,4 +889,52 @@ 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)
|
||||
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)
|
157
whiteboard.html
157
whiteboard.html
|
@ -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');
|
||||
|
@ -223,9 +247,28 @@
|
|||
}
|
||||
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; // Don’t 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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue