}
{% block title %}Board View{% endblock %}
{% block content %}

{% for note in notes %}
{{ note.text }}
{% endfor %} {% for image in images %}
{% endfor %} {% for audio in audios %}
{{ audio.file_name }}
▶️ ⏹️ 
{% endfor %}
Menu
Add Comment Add Image Add Audio Toggle Fullscreen Toggle VR
{% endblock %}

// 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, 54); // Смещаем board-left
    }

    if (boardRight) {
        boardRight.id = 'board-right';
        applyTransformShift(boardRight, -54);

        const images = boardRight.getElementsByTagName('img');
        Array.from(images).forEach(img => {
            const originalSrc = img.src;
            const imageName = originalSrc.split('/').pop();
            const newSrc = `/static/uploads/images/vr/${imageName}`;

            fetch(newSrc, { method: 'HEAD' })
                .then(response => {
                    if (response.ok) {
                        img.src = newSrc; // Изображение существует, обновляем источник
                    }
                });
        });
    }

    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, board_id: currentBoardId });
});

// 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 imageData;

    if (url) {
        imageData = await fetchLocalImageUrl(url);
    } else if (fileInput.files.length > 0) {
        const file = fileInput.files[0];
        imageData = await uploadFile(file, '/upload');
    }

    if (imageData) {
        addImageToCenter(imageData);
        closeModal();
    }
});

async function fetchLocalImageUrl(imageUrl) {
const response = await fetch('/fetch_image_from_url', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ url: imageUrl })
});

return response.json();
}

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

// 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, board_id: currentBoardId });
};
}

function handleExistingImages() {
document.querySelectorAll('.image').forEach(image => {
const id = image.dataset.id;
addImageEventListeners(image, id);
});
}

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

import uuid
import io
import requests
from flask import Flask, render_template, request, jsonify, flash, redirect, url_for
from flask_socketio import SocketIO, emit
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin, LoginManager, login_user, login_required, logout_user, current_user
from PIL import Image as PILImage
from werkzeug.security import generate_password_hash, check_password_hash

from forms import RegistrationForm, LoginForm
import os

app = Flask(name)
app.config['SECRET_KEY'] = 'secret!'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db'
db = SQLAlchemy(app)
socketio = SocketIO(app)

login_manager = LoginManager(app)
login_manager.login_view = 'login'

UPLOAD_IMAGE_FOLDER = 'static/uploads/images'
UPLOAD_IMAGE_VR_FOLDER = 'static/uploads/images/vr'
UPLOAD_AUDIO_FOLDER = 'static/uploads/audio'
app.config['UPLOAD_IMAGE_FOLDER'] = UPLOAD_IMAGE_FOLDER
app.config['UPLOAD_IMAGE_VR_FOLDER'] = UPLOAD_IMAGE_VR_FOLDER
app.config['UPLOAD_AUDIO_FOLDER'] = UPLOAD_AUDIO_FOLDER

class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False)
email = db.Column(db.String(150), unique=True, nullable=False)
password = db.Column(db.String(150), nullable=False)
boards = db.relationship('Board', backref='owner', lazy=True)

class Board(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(150), nullable=False)
is_public = db.Column(db.Boolean, default=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

class Note(db.Model):
board_id = db.Column(db.Integer, db.ForeignKey('board.id'), nullable=False)
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):
board_id = db.Column(db.Integer, db.ForeignKey('board.id'), nullable=False)
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):
board_id = db.Column(db.Integer, db.ForeignKey('board.id'), nullable=False)
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)

@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))

@app.route("/register", methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# Используйте pbkdf2:sha256 для хеширования пароля
hashed_password = generate_password_hash(form.password.data, method='pbkdf2:sha256', salt_length=16)
user = User(username=form.username.data, email=form.email.data, password=hashed_password)
db.session.add(user)
db.session.commit()
flash('Your account has been created!', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)

@app.route("/login", methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and check_password_hash(user.password, form.password.data):
login_user(user, remember=form.remember.data)
return redirect(url_for('boards'))
else:
flash('Login Unsuccessful. Please check email and password', 'danger')
return render_template('login.html', form=form)

@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for('login'))

@app.route('/')
@login_required
def boards():
user_boards = Board.query.filter_by(user_id=current_user.id).all()
public_boards = Board.query.filter_by(is_public=True).all()
return render_template('boards.html', user_boards=user_boards, public_boards=public_boards)

@app.route('/create_board', methods=['GET', 'POST'])
@login_required
def create_board():
if request.method == 'POST':
board_name = request.form.get('board_name')
is_public = 'is_public' in request.form
new_board = Board(name=board_name, is_public=is_public, owner=current_user)
db.session.add(new_board)
db.session.commit()
return redirect(url_for('boards'))
return render_template('create_board.html')

@app.route('/board/int:board_id')
@login_required
def board_view(board_id):
board = Board.query.get_or_404(board_id)
if board.is_public or board.owner == current_user:
notes = Note.query.filter_by(board_id=board_id)
images = Image.query.filter_by(board_id=board_id)
audios = Audio.query.filter_by(board_id=board_id)
return render_template('index.html', board=board, notes=notes, images=images, audios=audios)
else:
flash('Access denied', 'danger')
return redirect(url_for('boards'))

@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

# Open the image file
image = PILImage.open(file.stream)

if image.mode != 'RGB':
    image = image.convert('RGB')

unique_filename = f"{uuid.uuid4().hex}.jpg"
filepath = os.path.join(app.config['UPLOAD_IMAGE_FOLDER'], unique_filename)
os.makedirs(app.config['UPLOAD_IMAGE_FOLDER'], exist_ok=True)

image.save(filepath, "JPEG", quality=100)

return jsonify({'url': os.path.join('/', app.config['UPLOAD_IMAGE_FOLDER'], unique_filename)})
@app.route('/fetch_image_from_url', methods=['POST'])
def fetch_image_from_url():
data = request.get_json()
image_url = data.get('url')
if not image_url:
return jsonify({'error': 'No URL provided'}), 400


try:
    # Скачивание изображения с переданного URL
    response = requests.get(image_url)
    response.raise_for_status()

    # Генерация уникального имени файла
    filename = f"{uuid.uuid4().hex}.jpg"
    filepath = os.path.join(app.config['UPLOAD_IMAGE_FOLDER'], filename)
    os.makedirs(app.config['UPLOAD_IMAGE_FOLDER'], exist_ok=True)

    # Сохранение изображения в jpg формате
    image = PILImage.open(io.BytesIO(response.content))
    if image.mode != 'RGB':
        image = image.convert('RGB')
    image.save(filepath, "JPEG", quality=100)

    # Возвращаем путь к сохраненному изображению
    return jsonify({'url': os.path.join('/', app.config['UPLOAD_IMAGE_FOLDER'], filename)})

except requests.RequestException as e:
    return jsonify({'error': str(e)}), 500
@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']
board_id = data.get('board_id')


if not board_id:
    emit('error', {'error': 'Board ID is missing'}, broadcast=False)
    return

new_note = Note(text=text, x=x, y=y, width=width, height=height, board_id=board_id)
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')
board_id = data.get('board_id')


if not board_id:
    emit('error', {'error': 'Board ID is missing'}, broadcast=False)
    return

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, board_id=board_id)
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))
        image_vr_path = os.path.join(app.config['UPLOAD_IMAGE_VR_FOLDER'], os.path.basename(element.url))
        if os.path.exists(image_path):
            os.remove(image_path)

        if os.path.exists(image_vr_path):
            os.remove(image_vr_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('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)

@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']
board_id = data.get('board_id')


if not board_id:
    emit('error', {'error': 'Board ID is missing'}, broadcast=False)
    return

new_audio = Audio(file_name=file_name, url=url, x=x, y=y, width=width, height=height, board_id=board_id)
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)

if name == 'main':
with app.app_context():
if not os.path.exists('data.db'):
db.create_all()
socketio.run(app, host='192.168.0.105', port=7846, debug=True, allow_unsafe_werkzeug=True)

Почему когда я открываю порт и захожу на доску по открытому ip у меня не загружаются изображения? В консоли возникает ошибка например:
1:1 Access to image at 'http://192.168.0.105:7846/static/uploads/images/3b6aca63a7be435b804b369ba42e3be9.jpg' from origin 'http://188.68.180.137:7846' has been blocked by CORS policy: The request client is not a secure context and the resource is in more-private address space private.