USER
# app.py
from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit
from flask_sqlalchemy import SQLAlchemy
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///notes.db'
db = SQLAlchemy(app)
socketio = SocketIO(app)
UPLOAD_FOLDER = 'static/uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.String(500), nullable=False)
x = db.Column(db.Integer, nullable=False)
y = db.Column(db.Integer, nullable=False)
width = db.Column(db.Integer, nullable=False)
height = db.Column(db.Integer, nullable=False)
class Image(db.Model):
id = db.Column(db.Integer, primary_key=True)
url = db.Column(db.String(500), nullable=False)
x = db.Column(db.Integer, nullable=False)
y = db.Column(db.Integer, nullable=False)
width = db.Column(db.Integer, nullable=False)
height = db.Column(db.Integer, nullable=False)
@app.route('/')
def index():
notes = Note.query.all()
images = Image.query.all()
return render_template('index.html', notes=notes, images=images)
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
filename = file.filename
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
return jsonify({'url': os.path.join('/', app.config['UPLOAD_FOLDER'], filename)})
@socketio.on('create_note')
def handle_create_note(data):
text = data['text']
x = data['x']
y = data['y']
width = data['width']
height = data['height']
new_note = Note(text=text, x=x, y=y, width=width, height=height)
db.session.add(new_note)
db.session.commit()
emit('new_note', {'id': new_note.id, 'text': text, 'x': x, 'y': y, 'width': width, 'height': height},
broadcast=True)
@socketio.on('update_note')
def handle_update_note(data):
note = Note.query.get(data['id'])
note.text = data['text']
note.x = data['x']
note.y = data['y']
note.width = data['width']
note.height = data['height']
db.session.commit()
emit('updated_note',
{'id': note.id, 'text': note.text, 'x': note.x, 'y': note.y, 'width': note.width, 'height': note.height},
broadcast=True)
@socketio.on('create_image')
def handle_create_image(data):
url = data.get('url')
x = data.get('x')
y = data.get('y')
width = data.get('width')
height = data.get('height')
if not url or not x or not y or not width or not height:
return jsonify({'error': 'Missing required data'}), 400
new_image = Image(url=url, x=x, y=y, width=width, height=height)
db.session.add(new_image)
db.session.commit()
emit('new_image', {'id': new_image.id, 'url': url, 'x': x, 'y': y, 'width': width, 'height': height},
broadcast=True)
@socketio.on('update_image')
def handle_update_image(data):
image = Image.query.get(data['id'])
image.url = data.get('url', image.url)
image.x = data.get('x', image.x)
image.y = data.get('y', image.y)
image.width = data.get('width', image.width)
image.height = data.get('height', image.height)
db.session.commit()
emit('updated_image',
{'id': image.id, 'url': image.url, 'x': image.x, 'y': image.y, 'width': image.width, 'height': image.height},
broadcast=True)
@socketio.on('delete_element')
def handle_delete_element(data):
element_id = data['id']
element_type = data.get('type') # Добавляем тип элемента (note или image)
if element_type == 'note':
element = Note.query.get(element_id)
elif element_type == 'image':
element = Image.query.get(element_id)
else:
return jsonify({'error': 'Invalid element type'}), 400
if element:
# Если это изображение, удаляем также файл
if element_type == 'image':
image_path = os.path.join(app.config['UPLOAD_FOLDER'], os.path.basename(element.url))
# Проверяем, существует ли файл
if os.path.exists(image_path):
os.remove(image_path)
db.session.delete(element)
db.session.commit()
emit('element_deleted', {'id': element_id, 'type': element_type}, broadcast=True)
else:
return jsonify({'error': 'Element not found'}), 404
if __name__ == '__main__':
with app.app_context():
if not os.path.exists('notes.db'):
db.create_all()
socketio.run(app, host='192.168.0.105', port=7846, debug=True, allow_unsafe_werkzeug=True)
# static\css\style.css
body {
margin: 0;
padding: 0;
overflow: hidden;
}
#board-wrapper {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
cursor: grab;
background-image: url('/static/img/scale_1200.jpg');
background-repeat: repeat; /* Повторять изображение по горизонтали и вертикали */
background-size: 1200px 675px; /* Размер повторяющегося блока */
}
#board {
position: absolute;
top: 0;
left: 0;
transform-origin: 0 0;
}
.note, .image {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
padding: 10px;
box-sizing: border-box;
cursor: move;
}
textarea {
width: 100%;
height: 100%;
border: none;
resize: none;
outline: none;
}
#add-note, #add-image {
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#add-image {
right: 150px;
}
.image img {
width: 100%;
height: 100%;
object-fit: contain; /* Это свойство сохраняет соотношение сторон изображения */
}
.resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
background-color: #007bff;
cursor: nwse-resize;
}
.hidden {
visibility: hidden;
}
# static\js\board.js
// board.js
let scale = 0.5; // Initial scale
let translateX = 0;
let translateY = 0;
const board = document.getElementById('board');
const boardWrapper = document.getElementById('board-wrapper');
// Constants for zoom
const MIN_SCALE = 0.1; // Minimum scale
const MAX_SCALE = 5; // Maximum scale
const ZOOM_FACTOR = 0.1; // Zoom speed
// Apply initial scale
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
// Show images after setting scale
document.querySelectorAll('.image').forEach(img => {
img.classList.remove('hidden');
});
boardWrapper.addEventListener('wheel', (e) => {
e.preventDefault();
const previousScale = scale;
scale += e.deltaY < 0 ? ZOOM_FACTOR : -ZOOM_FACTOR;
scale = Math.min(Math.max(MIN_SCALE, scale), MAX_SCALE);
// Calculate cursor position relative to the board
const rect = board.getBoundingClientRect();
const offsetX = (e.clientX - rect.left) / previousScale;
const offsetY = (e.clientY - rect.top) / previousScale;
// Adjust translate to keep cursor point at the same board position
translateX -= offsetX * (scale - previousScale);
translateY -= offsetY * (scale - previousScale);
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
});
boardWrapper.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const startDistance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
const startScale = scale;
function zoom(e) {
if (e.touches.length !== 2) return;
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
const newScale = startScale * (currentDistance / startDistance);
scale = Math.min(Math.max(MIN_SCALE, newScale), MAX_SCALE);
// Calculate midpoint of touches
const midX = (touch1.clientX + touch2.clientX) / 2;
const midY = (touch1.clientY + touch2.clientY) / 2;
// Calculate offset relative to previous scale
const rect = board.getBoundingClientRect();
const offsetX = (midX - rect.left) / startScale;
const offsetY = (midY - rect.top) / startScale;
// Adjust translate to keep midpoint at same board position
translateX -= offsetX * (scale - startScale);
translateY -= offsetY * (scale - startScale);
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
}
document.addEventListener('touchmove', zoom);
document.addEventListener('touchend', () => {
document.removeEventListener('touchmove', zoom);
}, { once: true });
}
});
let isPanning = false;
let startPanX, startPanY;
boardWrapper.addEventListener('mousedown', (e) => {
if (e.button === 2) { // Right mouse button
isPanning = true;
startPanX = e.clientX - translateX;
startPanY = e.clientY - translateY;
boardWrapper.style.cursor = 'grabbing';
}
});
boardWrapper.addEventListener('mouseup', (e) => {
if (e.button === 2) { // Right mouse button
isPanning = false;
boardWrapper.style.cursor = 'grab';
}
});
boardWrapper.addEventListener('mousemove', (e) => {
if (!isPanning) return;
e.preventDefault();
translateX = e.clientX - startPanX;
translateY = e.clientY - startPanY;
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
});
// Add touch events for panning
boardWrapper.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
isPanning = true;
startPanX = e.touches[0].clientX - translateX;
startPanY = e.touches[0].clientY - translateY;
boardWrapper.style.cursor = 'grabbing';
}
});
boardWrapper.addEventListener('touchend', (e) => {
isPanning = false;
boardWrapper.style.cursor = 'grab';
});
boardWrapper.addEventListener('touchmove', (e) => {
if (!isPanning) return;
e.preventDefault();
translateX = e.touches[0].clientX - startPanX;
translateY = e.touches[0].clientY - startPanY;
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
});
// Prevent context menu on board-wrapper
boardWrapper.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
# static\js\contextMenu.js
// contextMenu.js
let activeContextMenu = null;
function showContextMenu(element, id, x, y) {
hideContextMenu(); // Ensure any existing menu is closed before showing a new one
const contextMenu = document.createElement('div');
contextMenu.className = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
contextMenu.style.backgroundColor = 'white';
contextMenu.style.borderRadius = '4px';
contextMenu.style.border = '1px solid #ccc';
contextMenu.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
contextMenu.style.zIndex = '1000';
activeContextMenu = contextMenu; // Track the active context menu
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.style.padding = '5px 10px';
deleteButton.style.border = 'none';
deleteButton.style.backgroundColor = '#ff4d4d';
deleteButton.style.color = 'white';
deleteButton.style.borderRadius = '4px';
deleteButton.style.cursor = 'pointer';
deleteButton.style.width = '100%';
deleteButton.style.textAlign = 'left';
deleteButton.addEventListener('click', () => {
element.remove();
socket.emit('delete_element', { id: id, type: element.classList.contains('note') ? 'note' : 'image' });
hideContextMenu(); // Close the menu after deleting
});
contextMenu.appendChild(deleteButton);
document.body.appendChild(contextMenu);
// Close context menu when clicking outside
document.addEventListener('click', (e) => {
if (!contextMenu.contains(e.target)) {
hideContextMenu();
}
}, { once: true });
}
function hideContextMenu() {
if (activeContextMenu) {
activeContextMenu.remove();
activeContextMenu = null;
}
}
# static\js\images.js
// images.js
// Assumes uploadFile, showContextMenu, addResizeListeners are available globally
socket.on('new_image', (data) => {
const image = createImageElement(data);
document.getElementById('board').appendChild(image);
});
socket.on('updated_image', (data) => {
const image = document.querySelector(`.image[data-id="${data.id}"]`);
if (image) {
image.style.left = `${data.x}px`;
image.style.top = `${data.y}px`;
image.style.width = `${data.width}px`;
image.style.height = `${data.height}px`;
image.querySelector('img').src = data.url;
}
});
socket.on('element_deleted', (data) => {
if (data.type === 'image') {
const element = document.querySelector(`.image[data-id="${data.id}"]`);
if (element) {
element.remove();
}
}
});
function createImageElement(data) {
const image = document.createElement('div');
image.className = 'image';
image.dataset.id = data.id;
image.style.left = `${data.x}px`;
image.style.top = `${data.y}px`;
image.style.width = `${data.width}px`;
image.style.height = `${data.height}px`;
const img = document.createElement('img');
img.src = data.url;
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain'; // Maintains aspect ratio
image.appendChild(img);
addImageEventListeners(image, data.id);
return image;
}
function addImageEventListeners(image, id) {
let isTouchDragEnabled = false; // Флаг для контроля активности сенсорных обработчиков
image.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // Только для левой кнопки мыши
e.preventDefault();
e.stopPropagation();
const startX = e.clientX;
const startY = e.clientY;
const startLeft = parseFloat(image.style.left) || 0;
const startTop = parseFloat(image.style.top) || 0;
function moveImage(e) {
const deltaX = (e.clientX - startX) / scale;
const deltaY = (e.clientY - startY) / scale;
image.style.left = `${startLeft + deltaX}px`;
image.style.top = `${startTop + deltaY}px`;
const x = parseFloat(image.style.left) || 0;
const y = parseFloat(image.style.top) || 0;
const width = image.offsetWidth;
const height = image.offsetHeight;
socket.emit('update_image', { id, url: image.querySelector('img').src, x, y, width, height });
}
document.addEventListener('mousemove', moveImage);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', moveImage);
}, { once: true });
});
function startTouchDrag(e) {
e.preventDefault();
e.stopPropagation();
hideContextMenu(); // Скрыть контекстное меню при начале сенсорного перетаскивания
const startX = e.touches[0].clientX;
const startY = e.touches[0].clientY;
const startLeft = parseFloat(image.style.left) || 0;
const startTop = parseFloat(image.style.top) || 0;
function moveTouch(e) {
const deltaX = (e.touches[0].clientX - startX) / scale;
const deltaY = (e.touches[0].clientY - startY) / scale;
image.style.left = `${startLeft + deltaX}px`;
image.style.top = `${startTop + deltaY}px`;
const x = parseFloat(image.style.left) || 0;
const y = parseFloat(image.style.top) || 0;
const width = image.offsetWidth;
const height = image.offsetHeight;
socket.emit('update_image', { id, url: image.querySelector('img').src, x, y, width, height });
}
function endTouchDrag() {
document.removeEventListener('touchmove', moveTouch);
document.removeEventListener('touchend', endTouchDrag);
}
document.addEventListener('touchmove', moveTouch);
document.addEventListener('touchend', endTouchDrag);
}
image.addEventListener('dblclick', (e) => {
e.preventDefault();
e.stopPropagation();
showContextMenu(image, id, e.clientX, e.clientY);
// Переключаем состояние перемещения
isTouchDragEnabled = !isTouchDragEnabled;
if (isTouchDragEnabled) {
image.addEventListener('touchstart', startTouchDrag);
// Добавляем слушатель, чтобы отключить перемещение при клике вне изображения
document.addEventListener('mousedown', checkClickOutside);
} else {
image.removeEventListener('touchstart', startTouchDrag);
document.removeEventListener('mousedown', checkClickOutside);
}
});
function checkClickOutside(e) {
if (!image.contains(e.target)) {
image.removeEventListener('touchstart', startTouchDrag);
isTouchDragEnabled = false;
document.removeEventListener('mousedown', checkClickOutside);
}
}
addResizeListeners(image, id, 'image');
}
function handleExistingImages() {
document.querySelectorAll('.image').forEach(image => {
const id = image.dataset.id;
addImageEventListeners(image, id);
});
}
# static\js\main.js
// static/js/main.js
// Проверяем, что 'socket' уже инициализирован
if (!window.socket) {
console.error('Socket is not initialized!');
}
// Event listener для кнопки 'add-note'
document.getElementById('add-note').addEventListener('click', () => {
const x = Math.floor(Math.random() * window.innerWidth);
const y = Math.floor(Math.random() * window.innerHeight);
const width = 200; // Ширина по умолчанию
const height = 40; // Высота по умолчанию
socket.emit('create_note', { text: 'New Comment', x, y, width, height });
});
// Event listener для кнопки 'add-image'
document.getElementById('add-image').addEventListener('click', () => {
const formHtml = `
<div id="image-form" style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); background:white; padding:20px; border-radius:8px; box-shadow:0 0 10px rgba(0,0,0,0.1); z-index: 1000;">
<label for="image-url" style="display:block; margin-bottom:8px;">Image URL:</label>
<input type="text" id="image-url" placeholder="Enter image URL" style="width: calc(100% - 22px); padding:10px; margin-bottom:12px; border:1px solid #ccc; border-radius:4px;"><br>
<label for="image-file" style="display:block; margin-bottom:8px;">Or Upload File:</label>
<input type="file" id="image-file" accept="image/*" style="margin-bottom:12px;"><br>
<button id="add-image-submit" style="padding:10px 20px; background-color:#007bff; color:white; border:none; border-radius:4px; cursor:pointer; margin-right:8px;">Add Image</button>
<button id="cancel-image" style="padding:10px 20px; background-color:#ccc; color:white; border:none; border-radius:4px; cursor:pointer;">Cancel</button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', formHtml);
document.getElementById('add-image-submit').addEventListener('click', async () => {
const url = document.getElementById('image-url').value;
const fileInput = document.getElementById('image-file');
let imageUrl;
if (url) {
imageUrl = url;
} else if (fileInput.files.length > 0) {
const file = fileInput.files[0];
imageUrl = await uploadFile(file);
}
if (imageUrl) {
const x = Math.floor(Math.random() * window.innerWidth);
const y = Math.floor(Math.random() * window.innerHeight);
const img = new Image();
img.src = imageUrl;
img.onload = () => {
const maxWidth = 720;
const maxHeight = 720;
let width = img.naturalWidth;
let height = img.naturalHeight;
if (width > maxWidth || height > maxHeight) {
const ratio = Math.min(maxWidth / width, maxHeight / height);
width *= ratio;
height *= ratio;
}
socket.emit('create_image', { url: imageUrl, x, y, width, height });
closeModal();
};
}
});
document.getElementById('cancel-image').addEventListener('click', closeModal);
function closeModal() {
const form = document.getElementById('image-form');
if (form) {
form.remove();
}
}
});
// Обработка существующих заметок и изображений после загрузки страницы
document.addEventListener('DOMContentLoaded', () => {
handleExistingNotes();
handleExistingImages();
});
# static\js\notes.js
// notes.js
// Assumes uploadFile, showContextMenu, addResizeListeners are available globally
socket.on('new_note', (data) => {
const note = createNoteElement(data);
document.getElementById('board').appendChild(note);
});
socket.on('updated_note', (data) => {
const note = document.querySelector(`.note[data-id="${data.id}"]`);
if (note) {
note.style.left = `${data.x}px`;
note.style.top = `${data.y}px`;
note.style.width = `${data.width}px`;
note.style.height = `${data.height}px`;
note.querySelector('textarea').value = data.text;
}
});
socket.on('element_deleted', (data) => {
if (data.type === 'note') {
const element = document.querySelector(`.note[data-id="${data.id}"]`);
if (element) {
element.remove();
}
}
});
function createNoteElement(data) {
const note = document.createElement('div');
note.className = 'note';
note.dataset.id = data.id;
note.style.left = `${data.x}px`;
note.style.top = `${data.y}px`;
note.style.width = `${data.width}px`;
note.style.height = `${data.height}px`;
const textarea = document.createElement('textarea');
textarea.value = data.text;
textarea.addEventListener('input', () => {
const x = note.offsetLeft;
const y = note.offsetTop;
const width = note.offsetWidth;
const height = note.offsetHeight;
socket.emit('update_note', { id: data.id, text: textarea.value, x, y, width, height });
});
textarea.addEventListener('mousedown', (e) => {
e.stopPropagation();
});
note.appendChild(textarea);
addNoteEventListeners(note, data.id);
return note;
}
function addNoteEventListeners(note, id) {
let isTouchDragEnabled = false; // Флаг для контроля активности сенсорных обработчиков
note.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // Left mouse button
e.preventDefault();
e.stopPropagation();
const startX = e.clientX;
const startY = e.clientY;
const startLeft = parseFloat(note.style.left) || 0;
const startTop = parseFloat(note.style.top) || 0;
function moveNote(e) {
const deltaX = (e.clientX - startX) / scale;
const deltaY = (e.clientY - startY) / scale;
note.style.left = `${startLeft + deltaX}px`;
note.style.top = `${startTop + deltaY}px`;
const x = parseFloat(note.style.left) || 0;
const y = parseFloat(note.style.top) || 0;
const width = note.offsetWidth;
const height = note.offsetHeight;
socket.emit('update_note', { id, text: note.querySelector('textarea').value, x, y, width, height });
}
document.addEventListener('mousemove', moveNote);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', moveNote);
}, { once: true });
});
function startTouchDrag(e) {
e.preventDefault();
e.stopPropagation();
hideContextMenu(); // Скрыть контекстное меню при начале сенсорного перетаскивания
const startX = e.touches[0].clientX;
const startY = e.touches[0].clientY;
const startLeft = parseFloat(note.style.left) || 0;
const startTop = parseFloat(note.style.top) || 0;
function moveNoteTouch(e) {
const deltaX = (e.touches[0].clientX - startX) / scale;
const deltaY = (e.touches[0].clientY - startY) / scale;
note.style.left = `${startLeft + deltaX}px`;
note.style.top = `${startTop + deltaY}px`;
const x = parseFloat(note.style.left) || 0;
const y = parseFloat(note.style.top) || 0;
const width = note.offsetWidth;
const height = note.offsetHeight;
socket.emit('update_note', { id, text: note.querySelector('textarea').value, x, y, width, height });
}
function endTouchDrag() {
document.removeEventListener('touchmove', moveNoteTouch);
document.removeEventListener('touchend', endTouchDrag);
}
document.addEventListener('touchmove', moveNoteTouch);
document.addEventListener('touchend', endTouchDrag);
}
note.addEventListener('dblclick', (e) => {
e.preventDefault();
e.stopPropagation();
showContextMenu(note, id, e.clientX, e.clientY);
// Переключаем состояние перемещения
isTouchDragEnabled = !isTouchDragEnabled;
if (isTouchDragEnabled) {
note.addEventListener('touchstart', startTouchDrag);
// Добавляем слушатель, чтобы отключить перемещение при клике вне записи
document.addEventListener('mousedown', checkClickOutside);
} else {
note.removeEventListener('touchstart', startTouchDrag);
document.removeEventListener('mousedown', checkClickOutside);
}
});
function checkClickOutside(e) {
if (!note.contains(e.target)) {
note.removeEventListener('touchstart', startTouchDrag);
isTouchDragEnabled = false;
document.removeEventListener('mousedown', checkClickOutside);
}
}
addResizeListeners(note, id, 'note');
}
function handleExistingNotes() {
document.querySelectorAll('.note').forEach(note => {
const id = note.dataset.id;
const text = note.querySelector('textarea').value;
const x = note.offsetLeft;
const y = note.offsetTop;
const width = note.offsetWidth;
const height = note.offsetHeight;
note.querySelector('textarea').addEventListener('input', () => {
const x = note.offsetLeft;
const y = note.offsetTop;
const width = note.offsetWidth;
const height = note.offsetHeight;
socket.emit('update_note', { id, text: note.querySelector('textarea').value, x, y, width, height });
});
note.querySelector('textarea').addEventListener('mousedown', (e) => {
e.stopPropagation();
});
addNoteEventListeners(note, id);
});
}
# static\js\resize.js
// resize.js
function addResizeListeners(element, id, type) {
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
element.appendChild(resizeHandle);
resizeHandle.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // Left mouse button
e.stopPropagation(); // Prevent triggering drag
e.preventDefault();
let startX = e.clientX;
let startY = e.clientY;
let startWidth = element.offsetWidth;
let startHeight = element.offsetHeight;
function resizeElement(e) {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
if (type === 'note') {
element.style.width = `${startWidth + deltaX}px`;
element.style.height = `${startHeight + deltaY}px`;
} else if (type === 'image') {
const ratio = startWidth / startHeight;
const newWidth = startWidth + deltaX;
const newHeight = newWidth / ratio;
element.style.width = `${newWidth}px`;
element.style.height = `${newHeight}px`;
}
const x = element.offsetLeft;
const y = element.offsetTop;
const width = element.offsetWidth;
const height = element.offsetHeight;
if (type === 'note') {
socket.emit('update_note', { id, text: element.querySelector('textarea').value, x, y, width, height });
} else if (type === 'image') {
socket.emit('update_image', { id, url: element.querySelector('img').src, x, y, width, height });
}
}
document.addEventListener('mousemove', resizeElement);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', resizeElement);
}, { once: true });
});
resizeHandle.addEventListener('touchstart', (e) => {
e.stopPropagation(); // Prevent triggering drag
e.preventDefault();
let startX = e.touches[0].clientX;
let startY = e.touches[0].clientY;
let startWidth = element.offsetWidth;
let startHeight = element.offsetHeight;
function resizeElement(e) {
const deltaX = e.touches[0].clientX - startX;
const deltaY = e.touches[0].clientY - startY;
if (type === 'note') {
element.style.width = `${startWidth + deltaX}px`;
element.style.height = `${startHeight + deltaY}px`;
} else if (type === 'image') {
const ratio = startWidth / startHeight;
const newWidth = startWidth + deltaX;
const newHeight = newWidth / ratio;
element.style.width = `${newWidth}px`;
element.style.height = `${newHeight}px`;
}
const x = element.offsetLeft;
const y = element.offsetTop;
const width = element.offsetWidth;
const height = element.offsetHeight;
if (type === 'note') {
socket.emit('update_note', { id, text: element.querySelector('textarea').value, x, y, width, height });
} else if (type === 'image') {
socket.emit('update_image', { id, url: element.querySelector('img').src, x, y, width, height });
}
}
document.addEventListener('touchmove', resizeElement);
document.addEventListener('touchend', () => {
document.removeEventListener('touchmove', resizeElement);
}, { once: true });
});
}
# static\js\socket.js
// static/js/socket.js
const socket = io();
window.socket = socket; // Делает 'socket' доступным глобально
# static\js\upload.js
// upload.js
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
return data.url;
}
# templates\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Miro Clone</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div id="board-wrapper">
<div id="board">
{% for note in notes %}
<div class="note" data-id="{{ note.id }}" style="left: {{ note.x }}px; top: {{ note.y }}px; width: {{ note.width }}px; height: {{ note.height }}px;">
<textarea>{{ note.text }}</textarea>
</div>
{% endfor %}
{% for image in images %}
<div class="image hidden" data-id="{{ image.id }}" style="left: {{ image.x }}px; top: {{ image.y }}px; width: {{ image.width }}px; height: {{ image.height }}px;">
<img src="{{ image.url }}" style="width: 100%; height: 100%; object-fit: contain;">
</div>
{% endfor %}
</div>
</div>
<button id="add-note">Add Comment</button>
<button id="add-image">Add Image</button>
<!-- Подключение Socket.io -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<!-- Инициализация Socket -->
<script src="{{ url_for('static', filename='js/socket.js') }}"></script>
<!-- Другие JavaScript файлы -->
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
<script src="{{ url_for('static', filename='js/contextMenu.js') }}"></script>
<script src="{{ url_for('static', filename='js/resize.js') }}"></script>
<script src="{{ url_for('static', filename='js/notes.js') }}"></script>
<script src="{{ url_for('static', filename='js/images.js') }}"></script>
<script src="{{ url_for('static', filename='js/board.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
Помоги мне сделать так чтобы последний вид доски сохранялся в куки браузера