# app.py
from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import Session
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_IMAGE_FOLDER = 'static/uploads/images'
UPLOAD_AUDIO_FOLDER = 'static/uploads/audio'
app.config['UPLOAD_IMAGE_FOLDER'] = UPLOAD_IMAGE_FOLDER
app.config['UPLOAD_AUDIO_FOLDER'] = UPLOAD_AUDIO_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)


class Audio(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    file_name = db.Column(db.String(500), nullable=False)
    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()
    audios = Audio.query.all()
    return render_template('index.html', notes=notes, images=images, audios=audios)


@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_IMAGE_FOLDER'], filename)
    os.makedirs(app.config['UPLOAD_IMAGE_FOLDER'], exist_ok=True)  # Ensure the upload folder exists
    file.save(filepath)

    return jsonify({'url': os.path.join('/', app.config['UPLOAD_IMAGE_FOLDER'], filename)})


@app.route('/upload_audio', methods=['POST'])
def upload_audio():
    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_AUDIO_FOLDER'], filename)
    os.makedirs(app.config['UPLOAD_AUDIO_FOLDER'], exist_ok=True)
    file.save(filepath)

    return jsonify({'url': os.path.join('/', app.config['UPLOAD_AUDIO_FOLDER'], filename), 'file_name': 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 = db.session.get(Note, data['id'])
    if note:
        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:
        emit('error', {'error': 'Missing required data'})
        return

    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 = db.session.get(Image, data['id'])
    if image:
        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')

    if element_type == 'note':
        element = db.session.get(Note, element_id)
    elif element_type == 'image':
        element = db.session.get(Image, element_id)
    else:
        emit('error', {'error': 'Invalid element type'})
        return

    if element:
        if element_type == 'image':
            image_path = os.path.join(app.config['UPLOAD_IMAGE_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:
        emit('error', {'error': 'Element not found'})


@socketio.on('create_audio')
def handle_create_audio(data):
    file_name = data['file_name']
    url = data['url']
    x = data['x']
    y = data['y']
    width = data['width']
    height = data['height']

    new_audio = Audio(file_name=file_name, url=url, x=x, y=y, width=width, height=height)
    db.session.add(new_audio)
    db.session.commit()
    emit('new_audio',
         {'id': new_audio.id, 'file_name': file_name, 'url': url, 'x': x, 'y': y, 'width': width, 'height': height},
         broadcast=True)


@socketio.on('update_audio')
def handle_update_audio(data):
    audio = db.session.get(Audio, data['id'])
    if audio:
        audio.file_name = data.get('file_name', audio.file_name)
        audio.url = data.get('url', audio.url)
        audio.x = data.get('x', audio.x)
        audio.y = data.get('y', audio.y)
        audio.width = data.get('width', audio.width)
        audio.height = data.get('height', audio.height)
        db.session.commit()
        emit('updated_audio',
             {'id': audio.id, 'file_name': audio.file_name, 'url': audio.url, 'x': audio.x, 'y': audio.y,
              'width': audio.width, 'height': audio.height}, broadcast=True)


@socketio.on('delete_audio')
def handle_delete_audio(data):
    audio = db.session.get(Audio, data['id'])
    if audio:
        audio_path = os.path.join(app.config['UPLOAD_AUDIO_FOLDER'], os.path.basename(audio.url))
        if os.path.exists(audio_path):
            os.remove(audio_path)
        db.session.delete(audio)
        db.session.commit()
        emit('audio_deleted', {'id': audio.id}, broadcast=True)


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
/* Basic Reset */
body {
    margin: 0;
    padding: 0;
    overflow: hidden;
    font-family: 'Arial', sans-serif;
}

/* Main Board Styles */
#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 and Image Styles */
.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;
    margin: 0;
    padding: 0;
}

.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;
}

/* Audio Element Styles */
.audio {
    position: absolute;
    background-color: #ffffff;
    border: 1px solid #e0e0e0;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    padding: 10px 15px;
    box-sizing: border-box;
    border-radius: 5px;
    width: 300px;
    transition: box-shadow 0.2s;
    cursor: move;
}

.audio:hover {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}

.audio-file-name {
    font-size: 14px;
    font-weight: bold;
    margin-bottom: 8px;
    color: #444;
    text-align: center;
}

.controls {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 10px;
}

.controls button {
    background: #007bff;
    border: none;
    color: white;
    padding: 5px 12px;
    font-size: 16px;
    border-radius: 5px;
    cursor: pointer;
    transition: background 0.2s, transform 0.2s;
}

.controls button:hover {
    background: #0056b3;
    transform: scale(1.05);
}

.progress-bar {
    flex-grow: 1;
    margin: 0 10px;
    height: 6px;
    cursor: pointer;
    background: #e0e0e0;
    border-radius: 5px;
    outline: none;
    -webkit-appearance: none;
}

.progress-bar::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 12px;
    height: 12px;
    background: #007bff;
    border-radius: 50%;
    cursor: pointer;
}

audio {
    display: none;
}

/* Hidden Class */
.hidden {
    display: none;
}

/* Menu Styles */
#menu-container {
    position: fixed;
    bottom: 20px;
    right: 20px;
}

#menu-button {
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

#dropdown-menu {
    position: absolute;
    bottom: 50px; /* Adjust the dropdown position */
    right: 0;
    background-color: white;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    border-radius: 5px;
    z-index: 1000; /* Ensure menu appears on top */
}

#dropdown-menu button {
    width: 100%;
    padding: 10px 20px;
    background: none;
    color: #007bff;
    border: none;
    text-align: left;
    cursor: pointer;
}

#dropdown-menu button:hover {
    background-color: #f1f1f1;
}

#vr-wrapper {
    display: flex;
    width: 100vw;
    height: 100vh;
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10000; /* Ensure it appears on top */
}

.half-screen {
    width: 50%;
    overflow: hidden;
    position: relative;
}

#board-left, #board-right {
    width: 100%;
    height: 100%;
    transform-origin: 0 0;
}

# static\js\audio.js
socket.on('new_audio', (data) => {
    const audioElement = createAudioElement(data);
    document.getElementById('board').appendChild(audioElement);
});

socket.on('updated_audio', (data) => {
    const audioElement = document.querySelector(`.audio[data-id="${data.id}"]`);
    if (audioElement) {
        audioElement.style.left = `${data.x}px`;
        audioElement.style.top = `${data.y}px`;
        audioElement.style.width = `${data.width}px`;
        audioElement.style.height = `${data.height}px`;
        audioElement.querySelector('.audio-file-name').textContent = data.file_name;
        audioElement.querySelector('audio').src = data.url;
    }
});

socket.on('audio_deleted', (data) => {
    const element = document.querySelector(`.audio[data-id="${data.id}"]`);
    if (element) {
        element.remove();
    }
});

function createAudioElement(data) {
    // Создаем основной контейнер для аудио элемента
    const audioElement = document.createElement('div');
    audioElement.className = 'audio';
    audioElement.dataset.id = data.id;
    audioElement.style.left = `${data.x}px`;
    audioElement.style.top = `${data.y}px`;
    audioElement.style.width = `${data.width}px`;
    audioElement.style.height = `${data.height}px`;

    // Элемент для отображения имени файла
    const audioTitle = document.createElement('div');
    audioTitle.className = 'audio-file-name';
    audioTitle.textContent = data.file_name;

    // Сам аудио элемент
    const audioTag = document.createElement('audio');
    audioTag.src = data.url;

    // Контейнер для управляющих кнопок
    const controls = document.createElement('div');
    controls.className = 'controls';

    // Кнопка проигрывания/паузы
    const playPauseButton = document.createElement('button');
    playPauseButton.className = 'play-pause';
    playPauseButton.textContent = '▶️';

    // Кнопка остановки
    const stopButton = document.createElement('button');
    stopButton.className = 'stop';
    stopButton.textContent = '⏹️';

    // Полоса прогресса
    const progressBar = document.createElement('input');
    progressBar.className = 'progress-bar';
    progressBar.type = 'range';
    progressBar.min = 0;
    progressBar.value = 0;
    progressBar.step = 0.1;

    // Добавляем элементы управления в контейнер
    controls.appendChild(playPauseButton);
    controls.appendChild(stopButton);
    controls.appendChild(progressBar);

    // Добавляем все подэлементы в основной контейнер
    audioElement.appendChild(audioTitle);
    audioElement.appendChild(audioTag);
    audioElement.appendChild(controls);

    // Привязываем необходимые обработчики событий сразу после создания элементов
    attachAudioEventListeners(audioElement, audioTag, playPauseButton, stopButton, progressBar);
    addAudioEventListeners(audioElement, data.id); // Для перетаскивания, изменения размера и др.
    return audioElement;
}

function attachAudioEventListeners(audioElement, audioTag, playPauseButton, stopButton, progressBar) {
    // Событие, когда аудио метаданные загружены
    audioTag.addEventListener('loadedmetadata', () => {
        progressBar.max = audioTag.duration;
    });

    // Событие обновления времени проигрывания аудио
    audioTag.addEventListener('timeupdate', () => {
        progressBar.value = audioTag.currentTime;
    });

    // Обработчик клика для кнопки проигрывания/паузы
    playPauseButton.addEventListener('click', () => {
        if (audioTag.paused) {
            audioTag.play();
            playPauseButton.textContent = '⏸️';
        } else {
            audioTag.pause();
            playPauseButton.textContent = '▶️';
        }
    });

    // Обработчик клика для кнопки остановки
    stopButton.addEventListener('click', () => {
        audioTag.pause();
        audioTag.currentTime = 0;
        playPauseButton.textContent = '▶️';
    });

    // Обработчик изменения значения полосы прогресса
    progressBar.addEventListener('input', () => {
        audioTag.currentTime = progressBar.value;
    });
}

function addAudioEventListeners(audioElement, id) {
    // Обработчик для перетаскивания элемента
    audioElement.addEventListener('mousedown', (e) => {
        // Проверяем, что клик не на элементах управления
        if (e.target.closest('.play-pause') || e.target.closest('.stop') || e.target.closest('.progress-bar')) {
            return;
        }

        if (e.button !== 0) return;
        e.preventDefault();
        e.stopPropagation();

        const startX = e.clientX;
        const startY = e.clientY;
        const startLeft = parseFloat(audioElement.style.left) || 0;
        const startTop = parseFloat(audioElement.style.top) || 0;

        function moveAudio(e) {
            const deltaX = (e.clientX - startX) / scale;
            const deltaY = (e.clientY - startY) / scale;
            audioElement.style.left = `${startLeft + deltaX}px`;
            audioElement.style.top = `${startTop + deltaY}px`;
            const x = parseFloat(audioElement.style.left) || 0;
            const y = parseFloat(audioElement.style.top) || 0;
            const width = audioElement.offsetWidth;
            const height = audioElement.offsetHeight;
            socket.emit('update_audio', { id, x, y, width, height });
        }

        // Добавляем обработчик движения мыши
        document.addEventListener('mousemove', moveAudio);

        // Обработчик отпускания кнопки мыши
        document.addEventListener('mouseup', () => {
            document.removeEventListener('mousemove', moveAudio);
        }, { once: true });
    });

    // Обработчик двойного клика
    audioElement.addEventListener('dblclick', (e) => {
        e.preventDefault();
        e.stopPropagation();
        showContextMenu(audioElement, id, e.clientX, e.clientY);
    });

    // Для добавления других нужных слушателей, таких как изменение размера
    addResizeListeners(audioElement, id, 'audio');
}

document.getElementById('add-audio').addEventListener('click', () => {
    const formHtml = `
        <div id="audio-form" style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); background:#f9f9f9; padding:20px; border-radius:12px; box-shadow:0 15px 30px rgba(0,0,0,0.2); z-index: 1000; max-width:400px; width:100%; text-align:center; font-family:'Arial', sans-serif;">
            <label for="audio-file" style="display:block; margin-bottom:16px; font-size:16px; color:#333;">🎵 Upload Your Audio File</label>
            <input type="file" id="audio-file" accept="audio/*" style="margin-bottom:20px; width:calc(100% - 32px); padding:10px; border-radius:8px; background-color:#fff; border:1px solid #ddd; color:#333; cursor:pointer;">
            <br>
            <button id="add-audio-submit" style="padding:12px 24px; background-color:#28a745; color:white; border:none; border-radius:6px; cursor:pointer; margin-right:12px; box-shadow:0 4px 8px rgba(40,167,69,0.2); transition:background-color 0.3s;">Add Audio</button>
            <button id="cancel-audio" style="padding:12px 24px; background-color:#ff3860; color:white; border:none; border-radius:6px; cursor:pointer; box-shadow:0 4px 8px rgba(255,56,96,0.2); transition:background-color 0.3s;">Cancel</button>
        </div>
    `;
    document.body.insertAdjacentHTML('beforeend', formHtml);

    document.getElementById('add-audio-submit').addEventListener('click', async () => {
        const fileInput = document.getElementById('audio-file');
        if (fileInput.files.length > 0) {
            const file = fileInput.files[0];
            const audioData = await uploadFile(file, '/upload_audio');
            addAudioToCenter(audioData);
            closeModal();
        }
    });

    document.getElementById('cancel-audio').addEventListener('click', closeModal);

    function closeModal() {
        const form = document.getElementById('audio-form');
        if (form) {
            form.remove();
        }
    }
});

function addAudioToCenter(audioData) {
    const rect = boardWrapper.getBoundingClientRect();
    const width = 200;
    const height = 80;

    const centerX = ((rect.width / 2 - translateX) / scale) - width / 2;
    const centerY = ((rect.height / 2 - translateY) / scale) - height / 2;

    socket.emit('create_audio', {
        file_name: audioData.file_name,
        url: audioData.url,
        x: centerX,
        y: centerY,
        width: 500,
        height: 100
    });
}

function handleExistingAudioElements() {
    // Находим все существующие элементы с классом 'audio'
    document.querySelectorAll('.audio').forEach(audioElement => {
        // Получаем audioTag и элементы управления, которые нам необходимы для привязки обработчиков
        const audioTag = audioElement.querySelector('audio');
        const playPauseButton = audioElement.querySelector('.play-pause');
        const stopButton = audioElement.querySelector('.stop');
        const progressBar = audioElement.querySelector('.progress-bar');

        const id = audioElement.dataset.id; // Получаем идентификатор элемента

        // Привязываем события, которые отвечают за взаимодействие с непосредственно аудио
        attachAudioEventListeners(audioElement, audioTag, playPauseButton, stopButton, progressBar);

        // Привязываем другие события, такие как перетаскивание и изменение размера
        addAudioEventListeners(audioElement, id);
    });
}

# static\js\board.js
// board.js

let scale = 0.05; // 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.05; // 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;

    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();

        if (element.classList.contains('note')) {
            socket.emit('delete_element', { id: id, type: 'note' });
        } else if (element.classList.contains('image')) {
            socket.emit('delete_element', { id: id, type: 'image' });
        } else if (element.classList.contains('audio')) {
            socket.emit('delete_audio', { id: id });
        }

        hideContextMenu();
    });

    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\fullscreen.js
// Ensure the document is fully loaded before accessing DOM elements
document.addEventListener('DOMContentLoaded', function() {
    const toggleFullscreenButton = document.getElementById('toggle-fullscreen');

    toggleFullscreenButton.addEventListener('click', () => {
        if (!document.fullscreenElement) {
            document.documentElement.requestFullscreen().catch(err => {
                console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
            });
        } else {
            document.exitFullscreen();
        }
    });
});

# 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 addImageToCenter(imageData) {
    const maxWidth = 720;
    const maxHeight = 720;
    const img = new Image();
    img.src = imageData.url;
    img.onload = () => {
        // Рассчитываем размеры, с учетом допустимых максимальных
        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;
        }

        // Получаем размеры видимой области
        const rect = boardWrapper.getBoundingClientRect();

        // Рассчитываем координаты центра видимой области с учётом текущего масштаба и смещения
        // и учитываем размеры изображения
        const centerX = ((rect.width / 2 - translateX) / scale) - width / 2;
        const centerY = ((rect.height / 2 - translateY) / scale) - height / 2;

        socket.emit('create_image', { url: imageData.url, x: centerX, y: centerY, width, height });
    };
}

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!');
}

document.addEventListener('DOMContentLoaded', function() {
    const menuButton = document.getElementById('menu-button');
    const dropdownMenu = document.getElementById('dropdown-menu');

    menuButton.addEventListener('click', () => {
        dropdownMenu.classList.toggle('hidden');
        dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
    });

    // Optional: Close the dropdown menu if the user clicks outside of it
    document.addEventListener('click', (event) => {
        if (!menuButton.contains(event.target) && !dropdownMenu.contains(event.target)) {
            dropdownMenu.style.display = 'none';
        }
    });
});

document.getElementById('toggle-vr').addEventListener('click', () => {
    const vrWrapper = document.getElementById('vr-wrapper');

    if (vrWrapper) {
        vrWrapper.remove();
        boardWrapper.style.display = 'block'; // Показать оригинальную доску
    } else {
        // Создание контейнера VR
        const newVrWrapper = document.createElement('div');
        newVrWrapper.id = 'vr-wrapper';

        const leftHalf = document.createElement('div');
        leftHalf.className = 'half-screen';
        const rightHalf = document.createElement('div');
        rightHalf.className = 'half-screen';

        const boardWrapperCloneLeft = boardWrapper.cloneNode(true);
        const boardLeft = boardWrapperCloneLeft.firstElementChild;

        const boardWrapperCloneRight = boardWrapper.cloneNode(true);
        const boardRight = boardWrapperCloneRight.firstElementChild;

        // Функция для обновления трансформации
        const applyTransformShift = (element, shiftX) => {
            const currentTransform = element.style.transform || '';

            let newTransform;
            let translateXMatch = currentTransform.match(/translateX\(([-\d.]+)px\)/);
            let translateMatch = currentTransform.match(/translate\(([-\d.]+)px,\s*([-\d.]+)px\)/);

            if (translateXMatch) {
                const currentTranslateX = parseFloat(translateXMatch[1]);
                const newTranslateX = currentTranslateX + shiftX;
                newTransform = currentTransform.replace(/translateX\(([-\d.]+)px\)/, `translateX(${newTranslateX}px)`);
            } else if (translateMatch) {
                const currentTranslateX = parseFloat(translateMatch[1]);
                const currentTranslateY = parseFloat(translateMatch[2]);
                const newTranslateX = currentTranslateX + shiftX;
                newTransform = currentTransform.replace(/translate\(([-\d.]+)px,\s*([-\d.]+)px\)/, `translate(${newTranslateX}px, ${currentTranslateY}px)`);
            } else {
                newTransform = `translateX(${shiftX}px) ${currentTransform}`.trim();
            }

            element.style.transform = newTransform;
        };

        if (boardLeft) {
            boardLeft.id = 'board-left';
            applyTransformShift(boardLeft, 30); // Смещаем board-left на 30 пикселей вправо
        }

        if (boardRight) {
            boardRight.id = 'board-right';
            applyTransformShift(boardRight, -30); // Смещаем board-right на 30 пикселей влево
        }

        leftHalf.appendChild(boardWrapperCloneLeft);
        rightHalf.appendChild(boardWrapperCloneRight);

        newVrWrapper.appendChild(leftHalf);
        newVrWrapper.appendChild(rightHalf);
        document.body.appendChild(newVrWrapper);

        boardWrapper.style.display = 'none'; // Скрыть оригинальную доску
    }
});

// Event listener для кнопки 'add-note'
document.getElementById('add-note').addEventListener('click', () => {
    // Получаем размеры видимой области
    const rect = boardWrapper.getBoundingClientRect();

    const width = 200; // Ширина по умолчанию
    const height = 40; // Высота по умолчанию

    // Рассчитываем координаты центра видимой области с учётом текущего масштаба и смещения
    const centerX = ((rect.width / 2 - translateX) / scale) - width / 2;
    const centerY = ((rect.height / 2 - translateY) / scale) - height / 2;


    socket.emit('create_note', { text: 'New Comment', x: centerX, y: centerY, 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) {
            imageData = {"url": url};
        } else if (fileInput.files.length > 0) {
            const file = fileInput.files[0];
            imageData = await uploadFile(file, '/upload');
        }

        if (imageData) {
            addImageToCenter(imageData);
            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();
    handleExistingAudioElements();
});

# 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' || type === 'audio') {
                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 });
            } else if (type === 'audio') {
                socket.emit('update_audio', { id, file_name: element.querySelector('.audio-file-name').textContent, url: element.querySelector('audio').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' || type === 'audio') {
                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 });
            } else if (type === 'audio') {
                socket.emit('update_audio', { id, file_name: element.querySelector('.audio-file-name').textContent, url: element.querySelector('audio').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, path) {
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch(path, {
        method: 'POST',
        body: formData
    });

    return await response.json();
}

# static\js\vr.js
const moveStepVr = 10;  // Шаг для перемещения
const zoomStepVr = 0.01;  // Шаг для увеличения-уменьшения
const MIN_SCALEVr = 0.05; // Минимальный масштаб
const MAX_SCALEVr = 5;   // Максимальный масштаб
const DEAD_ZONE = 0.1; // Мертвая зона для стиков

let translateVrXLeft = 0;
let translateVrYLeft = 0;
let scaleVrLeft = 0.05; // Начальный масштаб для левой части

let translateVrXRight = 0;
let translateVrYRight = 0;
let scaleVrRight = 0.05; // Начальный масштаб для правой части

function getInitialTransformValues(board) {
    const transform = board.style.transform || '';
    const translateMatch = transform.match(/translate\((-?\d*\.?\d*)px,\s*(-?\d*\.?\d*)px\)/);
    const scaleMatch = transform.match(/scale\((-?\d*\.?\d*)\)/);

    const translateX = translateMatch ? parseFloat(translateMatch[1]) : 0;
    const translateY = translateMatch ? parseFloat(translateMatch[2]) : 0;
    const scale = scaleMatch ? parseFloat(scaleMatch[1]) : 0.05;

    return { translateX, translateY, scale };
}

window.addEventListener('gamepadconnected', function(e) {
    console.log('Gamepad connected at index %d: %s. %d buttons, %d axes.',
        e.gamepad.index, e.gamepad.id,
        e.gamepad.buttons.length, e.gamepad.axes.length);
    requestAnimationFrame(handleGamepadInput);
});

function handleGamepadInput() {
    const gamepads = navigator.getGamepads();
    if (!gamepads) return;

    const gamepad = gamepads[0];
    if (!gamepad) return;

    const boardLeft = document.getElementById('board-left');
    const boardRight = document.getElementById('board-right');

    if (boardLeft && boardRight) {
        if (translateVrXLeft === 0 && translateVrYLeft === 0 && scaleVrLeft === 0.05) {
            const initialLeft = getInitialTransformValues(boardLeft);
            translateVrXLeft = initialLeft.translateX;
            translateVrYLeft = initialLeft.translateY;
            scaleVrLeft = initialLeft.scale;
        }

        if (translateVrXRight === 0 && translateVrYRight === 0 && scaleVrRight === 0.05) {
            const initialRight = getInitialTransformValues(boardRight);
            translateVrXRight = initialRight.translateX;
            translateVrYRight = initialRight.translateY;
            scaleVrRight = initialRight.scale;
        }

        // Проверьте кнопки увеличения/уменьшения масштаба
        if (gamepad.buttons[5].pressed) {
            const prevScaleLeft = scaleVrLeft;
            const prevScaleRight = scaleVrRight;
            scaleVrLeft = Math.max(MIN_SCALEVr, scaleVrLeft - zoomStepVr);
            scaleVrRight = Math.max(MIN_SCALEVr, scaleVrRight - zoomStepVr);

            // Adjust the translation to keep the center point of boards at the same screen position
            adjustTranslationForScaling(boardLeft, prevScaleLeft, scaleVrLeft, translateVrXLeft, translateVrYLeft);
            adjustTranslationForScaling(boardRight, prevScaleRight, scaleVrRight, translateVrXRight, translateVrYRight);
        }
        if (gamepad.buttons[3].pressed) {
            const prevScaleLeft = scaleVrLeft;
            const prevScaleRight = scaleVrRight;
            scaleVrLeft = Math.min(MAX_SCALEVr, scaleVrLeft + zoomStepVr);
            scaleVrRight = Math.min(MAX_SCALEVr, scaleVrRight + zoomStepVr);

            // Adjust the translation to keep the center point of boards at the same screen position
            adjustTranslationForScaling(boardLeft, prevScaleLeft, scaleVrLeft, translateVrXLeft, translateVrYLeft);
            adjustTranslationForScaling(boardRight, prevScaleRight, scaleVrRight, translateVrXRight, translateVrYRight);
        }

        // Проверка осей стиков для перемещения с учетом мертвой зоны
        const moveLeftX = Math.abs(gamepad.axes[0]) > DEAD_ZONE ? gamepad.axes[0] * moveStepVr : 0;
        const moveLeftY = Math.abs(gamepad.axes[1]) > DEAD_ZONE ? gamepad.axes[1] * moveStepVr : 0;
        const moveRightX = Math.abs(gamepad.axes[0]) > DEAD_ZONE ? gamepad.axes[0] * moveStepVr : 0;
        const moveRightY = Math.abs(gamepad.axes[1]) > DEAD_ZONE ? gamepad.axes[1] * moveStepVr : 0;

        translateVrXLeft -= moveLeftX;
        translateVrYLeft -= moveLeftY;
        translateVrXRight -= moveRightX;
        translateVrYRight -= moveRightY;

        updateVRTransform(boardLeft, translateVrXLeft, translateVrYLeft, scaleVrLeft);
        updateVRTransform(boardRight, translateVrXRight, translateVrYRight, scaleVrRight);
    }

    requestAnimationFrame(handleGamepadInput);
}

function adjustTranslationForScaling(board, previousScale, newScale, translateX, translateY) {
    const rect = board.getBoundingClientRect();
    const centerX = rect.width / 2;
    const centerY = rect.height / 2;

    const offsetX = centerX / previousScale;
    const offsetY = centerY / previousScale;

    translateX -= offsetX * (newScale - previousScale);
    translateY -= offsetY * (newScale - previousScale);
}

function updateVRTransform(board, translateX, translateY, scale) {
    board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
    board.style.backgroundPosition = `${translateX}px ${translateY}px`;
}

# 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 %}
            {% for audio in audios %}
            <div class="audio" data-id="{{ audio.id }}" style="left: {{ audio.x }}px; top: {{ audio.y }}px; width: {{ audio.width }}px; height: {{ audio.height }}px;">
                <div class="audio-file-name">{{ audio.file_name }}</div>
                <audio src="{{ audio.url }}"></audio>
                <div class="controls">
                    <button class="play-pause">▶️</button>
                    <button class="stop">⏹️</button>
                    <input class="progress-bar" type="range" min="0" step="0.1" max="32.078333">
                </div>
                <div class="resize-handle"></div>
            </div>
            {% endfor %}
        </div>
    </div>
<div id="menu-container">
    <button id="menu-button">Menu</button>
    <div id="dropdown-menu" class="hidden">
        <button id="add-note">Add Comment</button>
        <button id="add-image">Add Image</button>
        <button id="add-audio">Add Audio</button>
        <button id="toggle-fullscreen">Toggle Fullscreen</button>
        <button id="toggle-vr">Toggle VR</button>
    </div>
</div>

    <!-- Подключение 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/fullscreen.js') }}"></script>
    <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/audio.js') }}"></script>
    <script src="{{ url_for('static', filename='js/board.js') }}"></script>
    <script src="{{ url_for('static', filename='js/vr.js') }}"></script>
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>

У меня при зуме в vr доска смещается, а я хочу чтобы увеличение шло ровно по центру