mirror of
https://github.com/rudy3333/eversync.git
synced 2025-07-01 16:46: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)
|
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)
|
||||||
user = models.ForeignKey(User, 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('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'),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -876,4 +889,52 @@ 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)
|
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;">
|
<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');
|
||||||
|
@ -223,9 +247,28 @@
|
||||||
}
|
}
|
||||||
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; // Don’t 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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue