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:///notes.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):
    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)


@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('index'))
        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']
    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))
            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']

    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)


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


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Miro Clone{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <nav>
        {% if current_user.is_authenticated %}
            <a href="{{ url_for('boards') }}">Boards</a>
            <a href="{{ url_for('logout') }}">Logout</a>
        {% else %}
            <a href="{{ url_for('login') }}">Login</a>
            <a href="{{ url_for('register') }}">Register</a>
        {% endif %}
    </nav>

    <div class="content">
        {% block content %}{% endblock %}
    </div>

    <footer>
        <p>&copy; 2023 Miro Clone</p>
    </footer>
</body>
</html>

{% extends "base.html" %}
{% block title %}Boards{% endblock %}
{% block content %}
<h2>Your Boards</h2>
<ul>
    {% for board in user_boards %}
    <li><a href="{{ url_for('board_view', board_id=board.id) }}">{{ board.name }}</a> {% if board.is_public %}[Public]{% else %}[Private]{% endif %}</li>
    {% endfor %}
</ul>
<h3>Create New Board</h3>
<form action="{{ url_for('create_board') }}" method="POST">
    <input type="text" name="board_name" placeholder="Board Name"><br>
    <label><input type="checkbox" name="is_public"> Public</label><br>
    <button type="submit">Create Board</button>
</form>
{% endblock %}

{% extends "base.html" %}
{% block title %}Board View{% endblock %}
{% block content %}
<h2>{{ board.name }}</h2>
<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>

{% endblock %}

{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<h2>Login</h2>
<form method="POST">
    {{ form.hidden_tag() }}
    <div>
        {{ form.email.label }} {{ form.email(size=32) }}
        {% for error in form.email.errors %}
        <span style="color: red;">[{{ error }}]</span>
        {% endfor %}
    </div>
    <div>
        {{ form.password.label }} {{ form.password(size=32) }}
        {% for error in form.password.errors %}
        <span style="color: red;">[{{ error }}]</span>
        {% endfor %}
    </div>
    <div>
        {{ form.remember() }} {{ form.remember.label }}
    </div>
    <button type="submit">{{ form.submit() }}</button>
</form>
{% endblock %}

{% extends "base.html" %}
{% block title %}Register{% endblock %}
{% block content %}
<h2>Register</h2>
<form method="POST">
    {{ form.hidden_tag() }}
    <div>
        {{ form.username.label }} {{ form.username(size=32) }}
        {% for error in form.username.errors %}
        <span style="color: red;">[{{ error }}]</span>
        {% endfor %}
    </div>
    <div>
        {{ form.email.label }} {{ form.email(size=32) }}
        {% for error in form.email.errors %}
        <span style="color: red;">[{{ error }}]</span>
        {% endfor %}
    </div>
    <div>
        {{ form.password.label }} {{ form.password(size=32) }}
        {% for error in form.password.errors %}
        <span style="color: red;">[{{ error }}]</span>
        {% endfor %}
    </div>
    <div>
        {{ form.confirm_password.label }} {{ form.confirm_password(size=32) }}
        {% for error in form.confirm_password.errors %}
        <span style="color: red;">[{{ error }}]</span>
        {% endfor %}
    </div>
    <button type="submit">{{ form.submit() }}</button>
</form>
{% endblock %}

После того как я логинюсь, получаю ошибку:

BuildError
werkzeug.routing.exceptions.BuildError: Could not build url for endpoint 'index'. Did you mean 'login' instead?

Traceback (most recent call last)
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask\app.py", line 1498, in __call__
return self.wsgi_app(environ, start_response)
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask_socketio\__init__.py", line 43, in __call__
return super(_SocketIOMiddleware, self).__call__(environ,
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\engineio\middleware.py", line 74, in __call__
return self.wsgi_app(environ, start_response)
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask\app.py", line 1476, in wsgi_app
response = self.handle_exception(e)
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask\app.py", line 1473, in wsgi_app
response = self.full_dispatch_request()
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask\app.py", line 882, in full_dispatch_request
rv = self.handle_user_exception(e)
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask\app.py", line 880, in full_dispatch_request
rv = self.dispatch_request()
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask\app.py", line 865, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
File "C:\Developing\Python\Network\app.py", line 100, in login
return redirect(url_for('index'))
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask\helpers.py", line 220, in url_for
return current_app.url_for(
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask\app.py", line 1084, in url_for
return self.handle_url_build_error(error, endpoint, values)
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\flask\app.py", line 1073, in url_for
rv = url_adapter.build(  # type: ignore[union-attr]
File "C:\Users\crazy\.conda\envs\testing_env\lib\site-packages\werkzeug\routing\map.py", line 924, in build
raise BuildError(endpoint, values, method, self)
werkzeug.routing.exceptions.BuildError: Could not build url for endpoint 'index'. Did you mean 'login' instead?