auth modals

This commit is contained in:
aneuhmanh 2025-04-18 17:03:53 +03:00
parent bf5aeb677f
commit c9e8e5386f
31 changed files with 4105 additions and 3639 deletions

24
.gitignore vendored
View File

@ -1,13 +1,13 @@
/instance/ /instance/
/static/arts/ /static/arts/
/static/comics/ /static/comics/
/static/comicthumbs/ /static/comicthumbs/
/static/items/ /static/items/
/static/posts/ /static/posts/
/static/thumbnails/ /static/thumbnails/
/static/videos/ /static/videos/
/venv/ /venv/
/__pycache__/ /__pycache__/
static/css/*.css static/css/*.css
static/css/*.css.map static/css/*.css.map
.env .env

120
README.md
View File

@ -1,61 +1,61 @@
[![](https://artberry.xyz/static/Logo.svg "test")](https://artberry.xyz/static/Logo.svg "test") [![](https://artberry.xyz/static/Logo.svg "test")](https://artberry.xyz/static/Logo.svg "test")
### Booru сайт нового поколения ### Booru сайт нового поколения
**Открытый репозиторий веб-приложения [Artberry](https://artberry.xyz/ "Artberry")** **Открытый репозиторий веб-приложения [Artberry](https://artberry.xyz/ "Artberry")**
------------ ------------
### Используемые технологии ### Используемые технологии
#### Backend: #### Backend:
- ##### ЯП: [Python](http://https://www.python.org/ "Python") - ##### ЯП: [Python](http://https://www.python.org/ "Python")
- ##### ФРЕЙМВОРК: [Flask](https://https://flask.palletsprojects.com/en/stable/ "Flask") - ##### ФРЕЙМВОРК: [Flask](https://https://flask.palletsprojects.com/en/stable/ "Flask")
- ##### ШАБЛОНИЗАЦИЯ: [JINJA](https:/https://jinja.palletsprojects.com/en/stable// "JINJA") - ##### ШАБЛОНИЗАЦИЯ: [JINJA](https:/https://jinja.palletsprojects.com/en/stable// "JINJA")
#### Frontend: #### Frontend:
- ##### HTML - ##### HTML
- ##### CSS | [SASS](https://sass-lang.com/ "SASS") - ##### CSS | [SASS](https://sass-lang.com/ "SASS")
- ##### JAVASCRIPT - ##### JAVASCRIPT
------------ ------------
#### ЗАПУСК И ТЕСТИРОВАНИЕ #### ЗАПУСК И ТЕСТИРОВАНИЕ
#### Для удобства и скорости разработки выполните следующие шаги: #### Для удобства и скорости разработки выполните следующие шаги:
#### Создание виртуального окружения: #### Создание виртуального окружения:
- `python -m venv venv` - `python -m venv venv`
#### Запуск виртуального окружения: #### Запуск виртуального окружения:
*WINDOWS*: *WINDOWS*:
- **powershell:** ` .\venv\Scripts\Activate` - **powershell:** ` .\venv\Scripts\Activate`
- **CMD:** `venv\Scripts\activate.bat` - **CMD:** `venv\Scripts\activate.bat`
*LINUX* | *MAC*: *LINUX* | *MAC*:
- `source venv/bin/activate` - `source venv/bin/activate`
#### Установка зависимостей: #### Установка зависимостей:
- `pip install -r requirements.txt` - `pip install -r requirements.txt`
#### Запуск проекта: #### Запуск проекта:
- `python app.py` или `flask run` - `python app.py` или `flask run`
#### Для отладки в конце файла `app.py` измените: #### Для отладки в конце файла `app.py` измените:
- `app.run(debug=False)` **на:** `app.run(debug=True)` - `app.run(debug=False)` **на:** `app.run(debug=True)`
------------ ------------
#### КОМПИЛЯЦИЯ CSS ИЗ SASS #### КОМПИЛЯЦИЯ CSS ИЗ SASS
#### Для компиляции в реальном времени: #### Для компиляции в реальном времени:
- `sass --watch static/css/styles.scss:static/css/styles.css` - `sass --watch static/css/styles.scss:static/css/styles.css`
*Эта команда будет отслеживать изменения в файле `styles.scss` и автоматически компилировать его в `styles.css`* *Эта команда будет отслеживать изменения в файле `styles.scss` и автоматически компилировать его в `styles.css`*
#### Для одноразовой компиляции: #### Для одноразовой компиляции:
- `sass static/css/styles.scss:static/css/styles.css` - `sass static/css/styles.scss:static/css/styles.css`
*Эта команда выполнит одноразовую компиляцию из файла `styles.scss` в `styles.css`* *Эта команда выполнит одноразовую компиляцию из файла `styles.scss` в `styles.css`*
------------ ------------

412
admin.py
View File

@ -1,207 +1,207 @@
from flask import render_template, redirect, url_for, request, abort from flask import render_template, redirect, url_for, request, abort
from flask_login import login_required, current_user from flask_login import login_required, current_user
from wtforms import StringField, SubmitField from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from models import db, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicVotes, Cookies, UpdateCookiesForm from models import db, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicVotes, Cookies, UpdateCookiesForm
import os import os
import shutil import shutil
import uuid import uuid
import aiofiles import aiofiles
import bcrypt import bcrypt
from utils import update_related_tables from utils import update_related_tables
def register_admin_routes(app): def register_admin_routes(app):
@app.route('/admin', methods=['GET', 'POST']) @app.route('/admin', methods=['GET', 'POST'])
@login_required @login_required
def admin(): def admin():
if current_user.username != 'naturefie': if current_user.username != 'naturefie':
return redirect(url_for('index')) return redirect(url_for('index'))
form = UpdateCookiesForm() form = UpdateCookiesForm()
user_cookies = { user_cookies = {
user.id: Cookies.query.filter_by(username=user.username).first().cookies if Cookies.query.filter_by(username=user.username).first() else 0 user.id: Cookies.query.filter_by(username=user.username).first().cookies if Cookies.query.filter_by(username=user.username).first() else 0
for user in User.query.all() for user in User.query.all()
} }
comments = Comments.query.order_by(Comments.comment_date.desc()).all() comments = Comments.query.order_by(Comments.comment_date.desc()).all()
return render_template( return render_template(
'panel.html', 'panel.html',
arts=Image.query.all(), arts=Image.query.all(),
comics=Comic.query.all(), comics=Comic.query.all(),
videos=Video.query.all(), videos=Video.query.all(),
users=User.query.all(), users=User.query.all(),
comments=comments, comments=comments,
form=form, form=form,
user_cookies=user_cookies user_cookies=user_cookies
) )
@app.route('/admin/delete/<content_type>/<int:content_id>', methods=['POST']) @app.route('/admin/delete/<content_type>/<int:content_id>', methods=['POST'])
@login_required @login_required
def admin_delete_content(content_type, content_id): def admin_delete_content(content_type, content_id):
models = { models = {
'art': (Image, 'arts', 'image_file', Votes, 'image_id'), 'art': (Image, 'arts', 'image_file', Votes, 'image_id'),
'video': (Video, 'videos', 'video_file', VideoVotes, 'video_id'), 'video': (Video, 'videos', 'video_file', VideoVotes, 'video_id'),
'comic': (Comic, 'comics', 'comic_folder', ComicVotes, 'comic_id') 'comic': (Comic, 'comics', 'comic_folder', ComicVotes, 'comic_id')
} }
if content_type not in models: if content_type not in models:
abort(404) abort(404)
model, folder, file_field, vote_model, foreign_key = models[content_type] model, folder, file_field, vote_model, foreign_key = models[content_type]
content = model.query.get_or_404(content_id) content = model.query.get_or_404(content_id)
vote_model.query.filter(getattr(vote_model, foreign_key) == content_id).delete() vote_model.query.filter(getattr(vote_model, foreign_key) == content_id).delete()
Comments.query.filter(getattr(Comments, foreign_key) == content_id).delete() Comments.query.filter(getattr(Comments, foreign_key) == content_id).delete()
file_path = os.path.join(app.config['UPLOAD_FOLDER'][folder], getattr(content, file_field)) file_path = os.path.join(app.config['UPLOAD_FOLDER'][folder], getattr(content, file_field))
if os.path.exists(file_path): if os.path.exists(file_path):
if os.path.isfile(file_path): if os.path.isfile(file_path):
os.remove(file_path) os.remove(file_path)
else: else:
shutil.rmtree(file_path) shutil.rmtree(file_path)
db.session.delete(content) db.session.delete(content)
db.session.commit() db.session.commit()
return redirect(url_for('admin')) return redirect(url_for('admin'))
@app.route('/admin/delete/user/<int:user_id>', methods=['POST']) @app.route('/admin/delete/user/<int:user_id>', methods=['POST'])
@login_required @login_required
def admin_delete_user(user_id): def admin_delete_user(user_id):
user = User.query.get_or_404(user_id) user = User.query.get_or_404(user_id)
if current_user.username != 'naturefie': if current_user.username != 'naturefie':
return redirect(url_for('admin')) return redirect(url_for('admin'))
db.session.delete(user) db.session.delete(user)
db.session.commit() db.session.commit()
return redirect(url_for('admin')) return redirect(url_for('admin'))
@app.route('/admin/update_comment/<int:comment_id>', methods=['POST']) @app.route('/admin/update_comment/<int:comment_id>', methods=['POST'])
@login_required @login_required
def admin_update_comment(comment_id): def admin_update_comment(comment_id):
comment = Comments.query.get_or_404(comment_id) comment = Comments.query.get_or_404(comment_id)
if current_user.username != 'naturefie': if current_user.username != 'naturefie':
abort(403) abort(403)
new_text = request.form.get('comment_text', '').strip() new_text = request.form.get('comment_text', '').strip()
if not new_text: if not new_text:
return redirect(url_for('admin')) return redirect(url_for('admin'))
comment.comment_text = new_text comment.comment_text = new_text
try: try:
db.session.commit() db.session.commit()
print(f"Updated comment ID {comment_id}: {comment.comment_text}") print(f"Updated comment ID {comment_id}: {comment.comment_text}")
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
print(f"Error updating comment: {e}") print(f"Error updating comment: {e}")
return redirect(url_for('admin')) return redirect(url_for('admin'))
@app.route('/admin/delete_comment/<int:comment_id>', methods=['POST']) @app.route('/admin/delete_comment/<int:comment_id>', methods=['POST'])
@login_required @login_required
def admin_delete_comment(comment_id): def admin_delete_comment(comment_id):
comment = Comments.query.get_or_404(comment_id) comment = Comments.query.get_or_404(comment_id)
if current_user.username != 'naturefie': if current_user.username != 'naturefie':
abort(403) abort(403)
db.session.delete(comment) db.session.delete(comment)
db.session.commit() db.session.commit()
return redirect(url_for('admin')) return redirect(url_for('admin'))
@app.route('/admin/update_cookies/<int:user_id>', methods=['POST']) @app.route('/admin/update_cookies/<int:user_id>', methods=['POST'])
@login_required @login_required
def admin_update_cookies(user_id): def admin_update_cookies(user_id):
user = User.query.get_or_404(user_id) user = User.query.get_or_404(user_id)
if request.method == 'POST': if request.method == 'POST':
new_cookie_count = request.form.get('cookies', type=int) new_cookie_count = request.form.get('cookies', type=int)
if new_cookie_count is not None and new_cookie_count >= 0: if new_cookie_count is not None and new_cookie_count >= 0:
user_cookies = Cookies.query.filter_by(username=user.username).first() user_cookies = Cookies.query.filter_by(username=user.username).first()
if not user_cookies: if not user_cookies:
user_cookies = Cookies(username=user.username, cookies=new_cookie_count) user_cookies = Cookies(username=user.username, cookies=new_cookie_count)
db.session.add(user_cookies) db.session.add(user_cookies)
else: else:
user_cookies.cookies = new_cookie_count user_cookies.cookies = new_cookie_count
db.session.commit() db.session.commit()
return redirect(url_for('admin')) return redirect(url_for('admin'))
@app.route('/admin/update_video/<int:content_id>', methods=['POST']) @app.route('/admin/update_video/<int:content_id>', methods=['POST'])
@login_required @login_required
def admin_update_video(content_id): def admin_update_video(content_id):
video = Video.query.get_or_404(content_id) video = Video.query.get_or_404(content_id)
if current_user.username != 'naturefie': if current_user.username != 'naturefie':
return redirect(url_for('admin')) return redirect(url_for('admin'))
new_video_name = request.form.get('video_name') new_video_name = request.form.get('video_name')
new_description = request.form.get('description') new_description = request.form.get('description')
new_tags = request.form.get('tags') new_tags = request.form.get('tags')
if new_video_name and new_video_name != video.video_name: if new_video_name and new_video_name != video.video_name:
if len(new_video_name) < 3 or len(new_video_name) > 100: if len(new_video_name) < 3 or len(new_video_name) > 100:
return redirect(url_for('admin')) return redirect(url_for('admin'))
video.video_name = new_video_name video.video_name = new_video_name
if new_description: if new_description:
video.description = new_description video.description = new_description
if new_tags: if new_tags:
video.tags = new_tags video.tags = new_tags
db.session.commit() db.session.commit()
return redirect(url_for('admin')) return redirect(url_for('admin'))
@app.route('/admin/update_user/<int:user_id>', methods=['POST']) @app.route('/admin/update_user/<int:user_id>', methods=['POST'])
@login_required @login_required
def admin_update_user(user_id): def admin_update_user(user_id):
user = User.query.get_or_404(user_id) user = User.query.get_or_404(user_id)
if current_user.username != 'naturefie': if current_user.username != 'naturefie':
return redirect(url_for('admin')) return redirect(url_for('admin'))
new_username = request.form.get('username') new_username = request.form.get('username')
new_password = request.form.get('password') new_password = request.form.get('password')
if new_username and new_username != user.username: if new_username and new_username != user.username:
if len(new_username) < 3 or len(new_username) > 20: if len(new_username) < 3 or len(new_username) > 20:
return redirect(url_for('admin')) return redirect(url_for('admin'))
if User.query.filter_by(username=new_username).first(): if User.query.filter_by(username=new_username).first():
return redirect(url_for('admin')) return redirect(url_for('admin'))
old_username = user.username old_username = user.username
user.username = new_username user.username = new_username
update_related_tables(old_username, new_username) update_related_tables(old_username, new_username)
if new_password: if new_password:
if len(new_password) < 6: if len(new_password) < 6:
return redirect(url_for('admin')) return redirect(url_for('admin'))
hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8') hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8')
user.encrypted_password = hashed_password user.encrypted_password = hashed_password
db.session.commit() db.session.commit()
return redirect(url_for('admin')) return redirect(url_for('admin'))
@app.route('/admin/update_tags/<content_type>/<int:content_id>', methods=['POST']) @app.route('/admin/update_tags/<content_type>/<int:content_id>', methods=['POST'])
@login_required @login_required
def admin_update_tags(content_type, content_id): def admin_update_tags(content_type, content_id):
models = { models = {
'art': Image, 'art': Image,
'video': Video, 'video': Video,
'comic': Comic 'comic': Comic
} }
if content_type not in models: if content_type not in models:
abort(404) abort(404)
model = models[content_type] model = models[content_type]
content = model.query.get_or_404(content_id) content = model.query.get_or_404(content_id)
new_tags = request.form.get('tags', '').strip() new_tags = request.form.get('tags', '').strip()
content.tags = new_tags content.tags = new_tags
db.session.commit() db.session.commit()
return redirect(url_for('admin')) return redirect(url_for('admin'))

1910
app.py

File diff suppressed because it is too large Load Diff

142
auth.py
View File

@ -1,66 +1,78 @@
from flask import Blueprint, render_template, redirect, url_for, request from flask import Blueprint, render_template, redirect, url_for, request
from flask_login import login_user, logout_user, login_required, current_user from flask_login import login_user, logout_user, login_required, current_user
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from models import db, User from models import db, User
from utils import get_client_ip from utils import get_client_ip
from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from wtforms.validators import DataRequired, Length, EqualTo from wtforms.validators import DataRequired, Length, EqualTo
from config import Config
auth_bp = Blueprint('auth', __name__) auth_bp = Blueprint('auth', __name__)
bcrypt = Bcrypt() bcrypt = Bcrypt()
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)]) password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
recaptcha = RecaptchaField() recaptcha = RecaptchaField()
submit = SubmitField('Register') submit = SubmitField('Register')
@auth_bp.route('/register', methods=['GET', 'POST']) @auth_bp.route('/register', methods=['GET', 'POST'])
def register(): def register():
form = RegistrationForm() form = RegistrationForm()
if form.validate_on_submit(): if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
ip_address = get_client_ip()
ip_address = get_client_ip() username = form.username.data.lower()
existing_user = User.query.filter_by(ip_address=ip_address).first() existing_user = User.query.filter_by(ip_address=ip_address).first()
if existing_user: user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address)
return render_template('register.html', form=form)
try:
username = form.username.data.lower() db.session.add(user)
db.session.commit()
user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address) return redirect(url_for('auth.login'))
except IntegrityError:
try: db.session.rollback()
db.session.add(user)
db.session.commit() if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return redirect(url_for('auth.login')) return render_template('register-modal.html', form=form)
except IntegrityError:
db.session.rollback() return render_template('register.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
return render_template('register.html', form=form)
@auth_bp.route('/login', methods=['GET', 'POST'])
@auth_bp.route('/login', methods=['GET', 'POST']) def login():
def login(): form = LoginForm()
form = LoginForm()
if form.validate_on_submit():
if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data).first()
user = User.query.filter_by(username=form.username.data).first() if user and user.check_password(form.password.data):
login_user(user)
if user and user.check_password(form.password.data): if user.ip_address is None:
login_user(user) ip_address = get_client_ip()
user.ip_address = ip_address
if user.ip_address is None: db.session.commit()
ip_address = get_client_ip() return redirect(url_for('profile', username=user.username))
user.ip_address = ip_address
db.session.commit() if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return render_template('login-modal.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
return redirect(url_for('profile', username=user.username))
return render_template('login.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
return render_template('login.html', form=form)
@auth_bp.route('/logout')
def logout(): @auth_bp.route('/register-modal')
logout_user() def register_modal():
form = RegistrationForm()
return render_template('register-modal.html', form=form)
@auth_bp.route('/login-modal')
def login_modal():
form = LoginForm()
return render_template('login-modal.html', form=form)
@auth_bp.route('/logout')
def logout():
logout_user()
return redirect(url_for('index')) return redirect(url_for('index'))

View File

@ -1,27 +1,27 @@
import os import os
from dotenv import load_dotenv, find_dotenv from dotenv import load_dotenv, find_dotenv
dotenv_path = find_dotenv() dotenv_path = find_dotenv()
load_dotenv(dotenv_path, override=True) load_dotenv(dotenv_path, override=True)
class Config: class Config:
SECRET_KEY = os.getenv('SECRET_KEY') SECRET_KEY = os.getenv('SECRET_KEY')
WTF_CSRF_ENABLED = True WTF_CSRF_ENABLED = True
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY') RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY') RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI') SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
UPLOAD_FOLDER = { UPLOAD_FOLDER = {
'images': 'static/arts/', 'images': 'static/arts/',
'arts': 'static/arts/', 'arts': 'static/arts/',
'videos': 'static/videos/', 'videos': 'static/videos/',
'thumbnails': 'static/thumbnails/', 'thumbnails': 'static/thumbnails/',
'avatars': 'static/avatars/', 'avatars': 'static/avatars/',
'banners': 'static/banners/', 'banners': 'static/banners/',
'comics': 'static/comics', 'comics': 'static/comics',
'comicthumbs': 'static/comicthumbs/', 'comicthumbs': 'static/comicthumbs/',
'posts': 'static/posts/' 'posts': 'static/posts/'
} }
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'avi', 'mov'} ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'avi', 'mov'}
MAX_IMAGE_SIZE = 15 * 1024 * 1024 MAX_IMAGE_SIZE = 15 * 1024 * 1024
MAX_VIDEO_SIZE = 10 * 1024 * 1024 * 1024 MAX_VIDEO_SIZE = 10 * 1024 * 1024 * 1024

16
license
View File

@ -1,9 +1,9 @@
Copyright (c) 2025 artberry.xyz Copyright (c) 2025 artberry.xyz
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The "artberry.xyz" brand, logo, and trademarks are protected and may not be used without explicit permission from the copyright holder. This license does not grant any rights to use the brand or trademarks associated with "artberry.xyz". The "artberry.xyz" brand, logo, and trademarks are protected and may not be used without explicit permission from the copyright holder. This license does not grant any rights to use the brand or trademarks associated with "artberry.xyz".

528
models.py
View File

@ -1,266 +1,264 @@
import os import os
import io import io
import re import re
import uuid import uuid
import shutil import shutil
import random import random
import asyncio import asyncio
from datetime import datetime from datetime import datetime
import requests import requests
from PIL import Image as PILImage from PIL import Image as PILImage
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from flask_wtf import FlaskForm, RecaptchaField, CSRFProtect from flask_wtf import FlaskForm, RecaptchaField, CSRFProtect
from flask_wtf.file import FileAllowed from flask_wtf.file import FileAllowed
from flask_wtf.csrf import validate_csrf from flask_wtf.csrf import validate_csrf
from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField, RadioField, SelectField, TextAreaField from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField, RadioField, SelectField, TextAreaField
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Regexp from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Regexp
from dotenv import load_dotenv, find_dotenv from dotenv import load_dotenv, find_dotenv
import aiofiles.os import aiofiles.os
from sqlalchemy import func, or_ from sqlalchemy import func, or_
import magic import magic
from config import Config from config import Config
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
from flask_wtf.file import FileAllowed from flask_wtf.file import FileAllowed
db = SQLAlchemy() db = SQLAlchemy()
bcrypt = Bcrypt() bcrypt = Bcrypt()
class Comments(db.Model): class Comments(db.Model):
__tablename__ = 'comments' __tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, nullable=False) username = db.Column(db.String, nullable=False)
image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True) image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True)
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True) video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True)
comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=True) comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=True)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=True) post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=True)
comment_text = db.Column(db.Text, nullable=False) comment_text = db.Column(db.Text, nullable=False)
comment_date = db.Column(db.DateTime, default=datetime.utcnow) comment_date = db.Column(db.DateTime, default=datetime.utcnow)
image = db.relationship('Image', back_populates='comments') image = db.relationship('Image', back_populates='comments')
video = db.relationship('Video', back_populates='comments') video = db.relationship('Video', back_populates='comments')
comic = db.relationship('Comic', back_populates='comments', overlaps="comic_link") comic = db.relationship('Comic', back_populates='comments', overlaps="comic_link")
post = db.relationship('Post', backref='comments') post = db.relationship('Post', backref='comments')
class User(db.Model, UserMixin): class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False) username = db.Column(db.String(20), unique=True, nullable=False)
encrypted_password = db.Column(db.String(60), nullable=False) encrypted_password = db.Column(db.String(60), nullable=False)
ip_address = db.Column(db.String(15), nullable=True) ip_address = db.Column(db.String(15), nullable=True)
avatar_file = db.Column(db.String(50), nullable=True) avatar_file = db.Column(db.String(50), nullable=True)
banner_file = db.Column(db.String(50), nullable=True) banner_file = db.Column(db.String(50), nullable=True)
bio = db.Column(db.Text, nullable=True) bio = db.Column(db.Text, nullable=True)
current_item = db.Column(db.String(30), nullable=True) current_item = db.Column(db.String(30), nullable=True)
def __repr__(self): def __repr__(self):
return f'<User {self.username}>' return f'<User {self.username}>'
def check_password(self, password): def check_password(self, password):
return bcrypt.check_password_hash(self.encrypted_password, password) return bcrypt.check_password_hash(self.encrypted_password, password)
class Image(db.Model): class Image(db.Model):
__tablename__ = 'image' __tablename__ = 'image'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
image_file = db.Column(db.String(40), nullable=False) image_file = db.Column(db.String(40), nullable=False)
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
publication_date = db.Column(db.DateTime, default=datetime.utcnow) publication_date = db.Column(db.DateTime, default=datetime.utcnow)
tags = db.Column(db.String(100), nullable=True) tags = db.Column(db.String(100), nullable=True)
cookie_votes = db.Column(db.Integer, default=0) cookie_votes = db.Column(db.Integer, default=0)
comments = db.relationship('Comments', back_populates='image', cascade='all, delete-orphan') comments = db.relationship('Comments', back_populates='image', cascade='all, delete-orphan')
class Votes(db.Model): class Votes(db.Model):
__tablename__ = 'votes' __tablename__ = 'votes'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), nullable=False) username = db.Column(db.String(100), nullable=False)
image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=False) image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=False)
image = db.relationship('Image', backref=db.backref('votes', lazy=True)) image = db.relationship('Image', backref=db.backref('votes', lazy=True))
def __repr__(self): def __repr__(self):
return f'<Vote {self.username} for image {self.image_id}>' return f'<Vote {self.username} for image {self.image_id}>'
class VideoVotes(db.Model): class VideoVotes(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(120), nullable=False) username = db.Column(db.String(120), nullable=False)
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False) video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False)
class Video(db.Model): class Video(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
video_file = db.Column(db.String(100), nullable=False) video_file = db.Column(db.String(100), nullable=False)
video_name = db.Column(db.String(100), nullable=False) video_name = db.Column(db.String(100), nullable=False)
video_thumbnail_file = db.Column(db.String(100), nullable=False) video_thumbnail_file = db.Column(db.String(100), nullable=False)
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
publication_date = db.Column(db.DateTime, default=datetime.utcnow) publication_date = db.Column(db.DateTime, default=datetime.utcnow)
tags = db.Column(db.String(100), nullable=True) tags = db.Column(db.String(100), nullable=True)
description = db.Column(db.Text, nullable=True) description = db.Column(db.Text, nullable=True)
cookie_votes = db.Column(db.Integer, default=0) cookie_votes = db.Column(db.Integer, default=0)
comments = db.relationship('Comments', back_populates='video') comments = db.relationship('Comments', back_populates='video')
class Comic(db.Model): class Comic(db.Model):
__tablename__ = 'comics' __tablename__ = 'comics'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
comic_folder = db.Column(db.String(100), nullable=False) comic_folder = db.Column(db.String(100), nullable=False)
comic_thumbnail_file = db.Column(db.String(100), nullable=False) comic_thumbnail_file = db.Column(db.String(100), nullable=False)
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
name = db.Column(db.String(100), nullable=False, unique=True) name = db.Column(db.String(100), nullable=False, unique=True)
publication_date = db.Column(db.DateTime, default=datetime.utcnow) publication_date = db.Column(db.DateTime, default=datetime.utcnow)
tags = db.Column(db.String(100), nullable=True) tags = db.Column(db.String(100), nullable=True)
cookie_votes = db.Column(db.Integer, default=0) cookie_votes = db.Column(db.Integer, default=0)
comments = db.relationship('Comments', back_populates='comic', overlaps="comic_link") comments = db.relationship('Comments', back_populates='comic', overlaps="comic_link")
pages = db.relationship('ComicPage', back_populates='comic', cascade="all, delete-orphan") pages = db.relationship('ComicPage', back_populates='comic', cascade="all, delete-orphan")
class ComicPage(db.Model): class ComicPage(db.Model):
__tablename__ = 'comic_pages' __tablename__ = 'comic_pages'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
comic_id = db.Column(db.Integer, db.ForeignKey('comics.id', ondelete='CASCADE'), nullable=False) comic_id = db.Column(db.Integer, db.ForeignKey('comics.id', ondelete='CASCADE'), nullable=False)
page_number = db.Column(db.Integer, nullable=False) page_number = db.Column(db.Integer, nullable=False)
file_path = db.Column(db.String(200), nullable=False) file_path = db.Column(db.String(200), nullable=False)
comic = db.relationship('Comic', back_populates='pages') comic = db.relationship('Comic', back_populates='pages')
class ComicVotes(db.Model): class ComicVotes(db.Model):
__tablename__ = 'comic_votes' __tablename__ = 'comic_votes'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), nullable=False) username = db.Column(db.String(100), nullable=False)
comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=False) comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=False)
vote = db.Column(db.Integer) vote = db.Column(db.Integer)
comic = db.relationship('Comic', backref='votes', lazy=True) comic = db.relationship('Comic', backref='votes', lazy=True)
class Cookies(db.Model): class Cookies(db.Model):
username = db.Column(db.String(20), primary_key=True, nullable=False) username = db.Column(db.String(20), primary_key=True, nullable=False)
cookies = db.Column(db.Integer, default=0) cookies = db.Column(db.Integer, default=0)
class Views(db.Model): class Views(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True) image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True)
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True) video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True)
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
view_date = db.Column(db.DateTime, default=datetime.utcnow) view_date = db.Column(db.DateTime, default=datetime.utcnow)
__table_args__ = ( __table_args__ = (
db.UniqueConstraint('image_id', 'username', name='unique_image_view'), db.UniqueConstraint('image_id', 'username', name='unique_image_view'),
db.UniqueConstraint('video_id', 'username', name='unique_video_view') db.UniqueConstraint('video_id', 'username', name='unique_video_view')
) )
class Item(db.Model): class Item(db.Model):
__tablename__ = 'items' __tablename__ = 'items'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
item_path = db.Column(db.String, nullable=False) item_path = db.Column(db.String, nullable=False)
price = db.Column(db.Integer, nullable=False, default=0) price = db.Column(db.Integer, nullable=False, default=0)
visible = db.Column(db.Boolean, default=True) visible = db.Column(db.Boolean, default=True)
def __repr__(self): def __repr__(self):
return f'<Item {self.id}: {self.item_path}, Price: {self.price}, Visible: {self.visible}>' return f'<Item {self.id}: {self.item_path}, Price: {self.price}, Visible: {self.visible}>'
class UserItem(db.Model): class UserItem(db.Model):
__tablename__ = 'user_items' __tablename__ = 'user_items'
username = db.Column(db.String, db.ForeignKey('user.username'), primary_key=True) username = db.Column(db.String, db.ForeignKey('user.username'), primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey('items.id'), primary_key=True) item_id = db.Column(db.Integer, db.ForeignKey('items.id'), primary_key=True)
item = db.relationship('Item', backref=db.backref('user_items', lazy=True)) item = db.relationship('Item', backref=db.backref('user_items', lazy=True))
def __repr__(self): def __repr__(self):
return f'<UserItem {self.username}, Item {self.item_id}>' return f'<UserItem {self.username}, Item {self.item_id}>'
class Post(db.Model): class Post(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
post_date = db.Column(db.DateTime, default=datetime.utcnow) post_date = db.Column(db.DateTime, default=datetime.utcnow)
text = db.Column(db.Text, nullable=False) text = db.Column(db.Text, nullable=False)
media_file = db.Column(db.String(100), nullable=True) media_file = db.Column(db.String(100), nullable=True)
user = db.relationship('User', backref=db.backref('posts', lazy=True)) user = db.relationship('User', backref=db.backref('posts', lazy=True))
class Subscription(db.Model): class Subscription(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('subscriptions', lazy=True)) user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('subscriptions', lazy=True))
author = db.relationship('User', foreign_keys=[author_id], backref=db.backref('followers', lazy=True)) author = db.relationship('User', foreign_keys=[author_id], backref=db.backref('followers', lazy=True))
def __repr__(self): def __repr__(self):
return f'<Subscription user_id={self.user_id} author_id={self.author_id}>' return f'<Subscription user_id={self.user_id} author_id={self.author_id}>'
class UploadForm(FlaskForm): class UploadForm(FlaskForm):
image_file = FileField('Choose File', validators=[DataRequired()]) image_file = FileField('Choose File', validators=[DataRequired()])
tags = StringField('Tags (comma-separated)', validators=[DataRequired()]) tags = StringField('Tags (comma-separated)', validators=[DataRequired()])
recaptcha = RecaptchaField() recaptcha = RecaptchaField()
agree_with_rules = BooleanField('I agree with the publication rules', agree_with_rules = BooleanField('I agree with the publication rules',
validators=[DataRequired(message="You must agree with the publication rules.")]) validators=[DataRequired(message="You must agree with the publication rules.")])
submit = SubmitField('Upload') submit = SubmitField('Upload')
class UploadVideoForm(FlaskForm): class UploadVideoForm(FlaskForm):
video_file = FileField('Video File', validators=[DataRequired()]) video_file = FileField('Video File', validators=[DataRequired()])
thumbnail = FileField('Thumbnail', validators=[DataRequired(), FileAllowed(['jpg', 'png', 'jpeg'])]) thumbnail = FileField('Thumbnail', validators=[DataRequired(), FileAllowed(['jpg', 'png', 'jpeg'])])
name = StringField('Video Name', validators=[DataRequired()]) name = StringField('Video Name', validators=[DataRequired()])
tags = StringField('Tags', validators=[DataRequired()]) tags = StringField('Tags', validators=[DataRequired()])
description = StringField('Description', validators=[DataRequired()]) description = StringField('Description', validators=[DataRequired()])
recaptcha = RecaptchaField() recaptcha = RecaptchaField()
submit = SubmitField('Upload') submit = SubmitField('Upload')
agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired()]) agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired()])
class UploadComicForm(FlaskForm): class UploadComicForm(FlaskForm):
title = StringField('Comic Name', validators=[DataRequired()]) title = StringField('Comic Name', validators=[DataRequired()])
thumbnail = FileField('Thumbnail', validators=[DataRequired()]) thumbnail = FileField('Thumbnail', validators=[DataRequired()])
tags = StringField('Tags (comma-separated)') tags = StringField('Tags (comma-separated)')
recaptcha = RecaptchaField() recaptcha = RecaptchaField()
agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired(message="You must agree with the publication rules.")]) agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired(message="You must agree with the publication rules.")])
submit = SubmitField('Upload') submit = SubmitField('Upload')
class EditTagsForm(FlaskForm): class EditTagsForm(FlaskForm):
tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter tags"}) tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter tags"})
submit = SubmitField('Save') submit = SubmitField('Save')
class EditVideoForm(FlaskForm): class EditVideoForm(FlaskForm):
video_name = StringField('Title', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter video title"}) video_name = StringField('Title', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter video title"})
video_thumbnail = FileField('Thumbnail', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Only images!')]) video_thumbnail = FileField('Thumbnail', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Only images!')])
description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)], render_kw={"placeholder": "Enter video description"}) description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)], render_kw={"placeholder": "Enter video description"})
tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Tags"}) tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Tags"})
submit = SubmitField('Save') submit = SubmitField('Save')
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()]) username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()])
recaptcha = RecaptchaField() recaptcha = RecaptchaField()
submit = SubmitField('Login') submit = SubmitField('Login')
class RegistrationForm(FlaskForm):
username = StringField( class RegistrationForm(FlaskForm):
'Username', username = StringField('Username',
validators=[ validators=[DataRequired(), Length(3,20),
DataRequired(), Regexp('^[a-zA-Z0-9_]+$')])
Length(min=3, max=20), password = PasswordField('Password',
Regexp(r'^[a-zA-Z0-9_]+$', message="Username can contain only letters, numbers, and underscores.") validators=[DataRequired(), Length(min=6)])
] confirm_password = PasswordField('Confirm Password',
) validators=[DataRequired(), EqualTo('password')])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)]) recaptcha = RecaptchaField()
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) submit = SubmitField('Register')
recaptcha = RecaptchaField()
submit = SubmitField('Register') class EmptyForm(FlaskForm):
pass
class EmptyForm(FlaskForm):
pass class UpdateCookiesForm(FlaskForm):
cookies = StringField('Количество печенек', validators=[DataRequired()])
class UpdateCookiesForm(FlaskForm):
cookies = StringField('Количество печенек', validators=[DataRequired()])
submit = SubmitField('Применить') submit = SubmitField('Применить')

Binary file not shown.

File diff suppressed because it is too large Load Diff

23
static/favicon.svg Normal file
View File

@ -0,0 +1,23 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2_365)">
<path d="M32.9817 30.0844C38.9837 20.3425 32.56 14.1926 30.7762 12.702C30.3873 13.8164 30.2317 15.462 30.2317 15.462C25.2408 9.77372 16.2013 10.9507 16.2013 10.9507C15.3941 17.4137 17.9782 22.2645 17.9782 22.2645C17.9782 22.2645 16.1052 22.1668 14.8166 22.7903C17.9055 27.6744 24.0397 32.7986 32.9817 30.0844Z" fill="#9CCB3B"/>
<path d="M29.088 15.3974C29.3517 15.6008 29.7035 15.6655 30.0336 15.5612C30.4296 15.439 30.7081 15.0939 30.7427 14.6899C30.7402 14.6806 30.7791 14.3104 30.859 13.8142C33.2206 16.3673 35.9452 21.3051 31.7847 28.3506C23.4789 30.6779 18.3202 25.7344 15.9467 22.5C16.5575 22.4047 17.1478 22.3998 17.4096 22.4139C17.7793 22.4338 18.135 22.2576 18.3314 21.9447C18.5338 21.6352 18.5462 21.2422 18.3756 20.9186C18.3533 20.8747 16.1695 16.6649 16.6252 11.0314C18.8589 10.9166 25.2022 11.0136 28.9321 15.2547C28.9784 15.3122 29.0315 15.3577 29.088 15.3974ZM30.8952 11.0268C30.6435 10.8302 30.3071 10.7663 29.9907 10.847C29.6614 10.9361 29.4007 11.1814 29.2907 11.496C29.1841 11.8046 29.0912 12.1444 29.0137 12.4851C23.6144 8.09621 15.9077 9.02754 15.5501 9.06934C15.0864 9.12985 14.7172 9.49451 14.6578 9.95519C14.0821 14.627 15.1713 18.4636 15.9213 20.4387C15.2652 20.5164 14.5056 20.6769 13.842 21.0014C13.5787 21.1276 13.3898 21.3585 13.3063 21.6409C13.2262 21.9173 13.2684 22.2156 13.4219 22.4589C15.1095 25.1344 21.5041 33.6052 32.7613 30.1945C33.0033 30.119 33.2128 29.9625 33.3357 29.7493C39.629 19.544 33.0773 12.8593 30.9183 11.0556C30.9192 11.0403 30.9072 11.0336 30.8952 11.0268Z" fill="#1F1D44"/>
<path d="M29.5863 27.73C22.374 16.414 29.7945 9.09085 31.8591 7.31192C32.3351 8.61155 32.5471 10.5382 32.5471 10.5382C38.2961 3.78147 48.9154 5.00264 48.9154 5.00264C49.9782 12.5657 47.0359 18.2983 47.0359 18.2983C47.0359 18.2983 49.23 18.1509 50.7521 18.8592C47.2184 24.6397 40.1189 30.7552 29.5863 27.73Z" fill="#9CCB3B"/>
<path d="M33.8871 10.4423C33.5816 10.6854 33.1704 10.7674 32.7814 10.6509C32.315 10.5147 31.9822 10.115 31.9343 9.6419C31.9372 9.63095 31.8849 9.19759 31.7823 8.61725C29.0594 11.6521 25.954 17.4891 30.9586 25.6763C40.7385 28.259 46.6975 22.3724 49.4221 18.5387C48.7043 18.4377 48.0121 18.4424 47.7054 18.4635C47.2724 18.4933 46.8522 18.293 46.6162 17.9295C46.3733 17.5702 46.3517 17.1096 46.5459 16.7273C46.5713 16.6755 49.0559 11.7014 48.4202 5.10466C45.7992 5.00925 38.364 5.23444 34.0673 10.2723C34.0141 10.3405 33.9527 10.3948 33.8871 10.4423ZM31.6896 5.34995C31.9813 5.11497 32.3744 5.03419 32.7469 5.12317C33.1346 5.22185 33.4446 5.50495 33.5793 5.87177C33.7098 6.2317 33.8248 6.62851 33.9218 7.02657C40.173 1.78611 49.2253 2.74263 49.6453 2.78536C50.19 2.84816 50.6294 3.26921 50.7073 3.80827C51.4664 9.27543 50.2585 13.7927 49.4148 16.1216C50.1854 16.2011 51.0789 16.376 51.8627 16.7447C52.1737 16.888 52.3993 17.1555 52.5023 17.485C52.6012 17.8078 52.557 18.1582 52.3815 18.4461C50.4511 21.6126 43.1064 31.6562 29.8469 27.8553C29.5618 27.771 29.3134 27.5911 29.1654 27.3434C21.6033 15.4891 29.1644 7.53666 31.663 5.38401C31.6618 5.36619 31.6757 5.35807 31.6896 5.34995Z" fill="#1F1D44"/>
<path d="M34.1094 36.1705C34.1094 44.0866 27.6163 50.5007 19.6026 50.5007C11.5889 50.5007 5.0957 44.0866 5.0957 36.1705C5.0957 28.2544 11.5889 21.8403 19.6026 21.8403C27.6163 21.8403 34.1094 28.2544 34.1094 36.1705Z" fill="#5751B3"/>
<path d="M19.0931 22.4492C11.8109 22.4492 5.88486 28.3031 5.88486 35.4966C5.88486 42.6901 11.8109 48.544 19.0931 48.544C26.3753 48.544 32.3013 42.6901 32.3013 35.4966C32.3013 28.3031 26.3753 22.4492 19.0931 22.4492ZM19.0931 51.1096C10.3807 51.1096 3.2876 44.1109 3.2876 35.5047C3.2876 26.8985 10.3725 19.8998 19.0931 19.8998C27.8054 19.8998 34.8986 26.8985 34.8986 35.5047C34.8903 44.1109 27.8054 51.1096 19.0931 51.1096Z" fill="#1F1D44"/>
<path d="M15.0837 24.8519C15.7967 24.9052 16.3352 25.5154 16.2806 26.2279C16.2266 26.9322 15.7036 26.9264 14.8535 27.3185C12.931 28.2051 12.9009 28.5974 11.729 30.0161C11.2961 30.7166 11.0573 31.2524 10.336 31.1985C9.62302 31.1452 9.08515 30.5269 9.13911 29.8226C9.71701 28.1966 12.4524 25.307 15.0837 24.8519Z" fill="#8784C9"/>
<path d="M17.2603 25.0068C17.2603 25.3082 17.3815 25.5974 17.5973 25.8105C17.8131 26.0237 18.1058 26.1434 18.4109 26.1434C18.7161 26.1434 19.0088 26.0237 19.2246 25.8105C19.4404 25.5974 19.5616 25.3082 19.5616 25.0068C19.5616 24.7053 19.4404 24.4162 19.2246 24.203C19.0088 23.9899 18.7161 23.8701 18.4109 23.8701C18.1058 23.8701 17.8131 23.9899 17.5973 24.203C17.3815 24.4162 17.2603 24.7053 17.2603 25.0068Z" fill="#8784C9"/>
<path d="M58.4385 35.6022C58.4385 43.5183 51.9454 49.9323 43.9317 49.9323C35.918 49.9323 29.4248 43.5183 29.4248 35.6022C29.4248 27.6861 35.918 21.272 43.9317 21.272C51.9454 21.272 58.4385 27.6861 58.4385 35.6022Z" fill="#5751B3"/>
<path d="M40.152 25.0955C40.865 25.1488 41.4035 25.759 41.349 26.4714C41.295 27.1758 40.772 27.17 39.9219 27.562C37.9994 28.4487 37.9693 28.841 36.7973 30.2597C36.3645 30.9602 36.1256 31.496 35.4044 31.4421C34.6914 31.3888 34.1535 30.7705 34.2075 30.0661C34.7854 28.4402 37.5207 25.5505 40.152 25.0955Z" fill="#8784C9"/>
<path d="M43.3397 22.4492C36.0575 22.4492 30.1314 28.3031 30.1314 35.4966C30.1314 42.6901 36.0575 48.544 43.3397 48.544C50.6219 48.544 56.5479 42.6901 56.5479 35.4966C56.5479 28.3031 50.6219 22.4492 43.3397 22.4492ZM43.3397 51.1096C34.6273 51.1096 27.5342 44.1109 27.5342 35.5047C27.5342 26.8985 34.6191 19.8998 43.3397 19.8998C52.052 19.8998 59.1451 26.8985 59.1451 35.5047C59.1369 44.1109 52.052 51.1096 43.3397 51.1096Z" fill="#1F1D44"/>
<path d="M42.3286 25.2503C42.3286 25.5518 42.4498 25.8409 42.6656 26.0541C42.8814 26.2672 43.1741 26.387 43.4793 26.387C43.7845 26.387 44.0772 26.2672 44.293 26.0541C44.5088 25.8409 44.63 25.5518 44.63 25.2503C44.63 24.9489 44.5088 24.6598 44.293 24.4466C44.0772 24.2334 43.7845 24.1137 43.4793 24.1137C43.1741 24.1137 42.8814 24.2334 42.6656 24.4466C42.4498 24.6598 42.3286 24.9489 42.3286 25.2503Z" fill="#8784C9"/>
<path d="M43.4119 36.6171L41.7834 39.813M41.7834 39.813L40.1548 43.0092M41.7834 39.813L45.0188 41.4217M41.7834 39.813L38.5479 38.2042" stroke="#1F1D44" stroke-width="2" stroke-linecap="round"/>
<path d="M17.1107 36.6171L15.4821 39.813M15.4821 39.813L13.8535 43.0092M15.4821 39.813L18.7175 41.4217M15.4821 39.813L12.2466 38.2042" stroke="#1F1D44" stroke-width="2" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_2_365">
<rect width="60" height="60" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -1,3 +1,3 @@
<svg width="15" height="25" viewBox="0 0 15 25" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="15" height="25" viewBox="0 0 15 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0607 13.5607C14.6464 12.9749 14.6464 12.0251 14.0607 11.4393L4.51472 1.8934C3.92893 1.30761 2.97919 1.30761 2.3934 1.8934C1.80761 2.47919 1.80761 3.42893 2.3934 4.01472L10.8787 12.5L2.3934 20.9853C1.80761 21.5711 1.80761 22.5208 2.3934 23.1066C2.97919 23.6924 3.92893 23.6924 4.51472 23.1066L14.0607 13.5607ZM12 14H13V11H12V14Z" fill="#8784C9"/> <path d="M14.0607 13.5607C14.6464 12.9749 14.6464 12.0251 14.0607 11.4393L4.51472 1.8934C3.92893 1.30761 2.97919 1.30761 2.3934 1.8934C1.80761 2.47919 1.80761 3.42893 2.3934 4.01472L10.8787 12.5L2.3934 20.9853C1.80761 21.5711 1.80761 22.5208 2.3934 23.1066C2.97919 23.6924 3.92893 23.6924 4.51472 23.1066L14.0607 13.5607ZM12 14H13V11H12V14Z" fill="#8784C9"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 465 B

View File

@ -1,15 +1,15 @@
let pageCount = 1; let pageCount = 1;
const maxPages = 64; const maxPages = 64;
function addPage() { function addPage() {
if (pageCount >= maxPages) { if (pageCount >= maxPages) {
alert(`You can't add more than ${maxPages} pages!`); alert(`You can't add more than ${maxPages} pages!`);
return; return;
} }
pageCount++; pageCount++;
const newPage = document.createElement("div"); const newPage = document.createElement("div");
newPage.classList.add("form-group"); newPage.classList.add("form-group");
newPage.innerHTML = `<label>Page ${pageCount}:</label><input type="file" name="pages[]" class="file-input" accept="image/*" required>`; newPage.innerHTML = `<label>Page ${pageCount}:</label><input type="file" name="pages[]" class="file-input" accept="image/*" required>`;
document.getElementById("pages").appendChild(newPage); document.getElementById("pages").appendChild(newPage);
} }

View File

@ -1,64 +1,63 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const cardCovers = document.querySelectorAll('.small-card-cover, .card-cover'); const cardCovers = document.querySelectorAll('.small-card-cover, .card-cover');
cardCovers.forEach(cardCover => { cardCovers.forEach(cardCover => {
const thumbnail = cardCover.querySelector('.thumbnail'); const thumbnail = cardCover.querySelector('.thumbnail');
const video = cardCover.querySelector('.preview-video'); const video = cardCover.querySelector('.preview-video');
const videoSource = video.querySelector('source'); const videoSource = video.querySelector('source');
let hoverTimeout;
const showPreview = () => {
cardCover.addEventListener('mouseenter', () => { thumbnail.style.display = 'none';
hoverTimeout = setTimeout(() => { video.style.display = 'block';
if (!videoSource.src) { if (video.duration > 15) {
videoSource.src = video.dataset.src; video.currentTime = 10;
video.load(); const loopSegment = () => {
} if (video.currentTime >= 15) {
video.currentTime = 10;
thumbnail.style.display = 'none'; video.play();
video.style.display = 'block'; }
};
video.addEventListener('loadedmetadata', () => { video.addEventListener('timeupdate', loopSegment);
cardCover.loopSegment = loopSegment;
if (video.duration > 15) { } else {
video.currentTime = 0;
video.currentTime = 10; const loopEntireVideo = () => {
const loopSegment = () => { if (video.currentTime >= video.duration) {
if (video.currentTime >= 15) { video.currentTime = 0;
video.currentTime = 10; video.play();
video.play(); }
} };
}; video.addEventListener('timeupdate', loopEntireVideo);
video.addEventListener('timeupdate', loopSegment); cardCover.loopSegment = loopEntireVideo;
cardCover.loopSegment = loopSegment; }
} else {
video.play();
video.currentTime = 0; };
const loopEntireVideo = () => {
if (video.currentTime >= video.duration) { cardCover.addEventListener('mouseenter', () => {
video.currentTime = 0; video.pause();
video.play(); videoSource.src = video.dataset.src;
} video.load();
}; video.addEventListener('canplay', showPreview, { once: true });
video.addEventListener('timeupdate', loopEntireVideo); });
cardCover.loopSegment = loopEntireVideo;
} cardCover.addEventListener('mouseleave', (e) => {
const toElement = e.relatedTarget;
video.play();
}); if (!cardCover.contains(toElement)) {
}, 2000); video.pause();
}); video.currentTime = 0;
video.style.display = 'none';
cardCover.addEventListener('mouseleave', () => { thumbnail.style.display = 'block';
clearTimeout(hoverTimeout);
video.pause(); if (cardCover.loopSegment) {
video.style.display = 'none'; video.removeEventListener('timeupdate', cardCover.loopSegment);
thumbnail.style.display = 'block'; delete cardCover.loopSegment;
}
if (cardCover.loopSegment) {
video.removeEventListener('timeupdate', cardCover.loopSegment); videoSource.src = '';
delete cardCover.loopSegment; }
} });
}); });
}); });
});

View File

@ -1,17 +1,92 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const iconContainer = document.querySelector('.icon-container'); const iconContainer = document.querySelector('.icon-container');
const dropdownMenu = document.querySelector('.dropdown-menu'); const dropdownMenu = document.querySelector('.dropdown-menu');
iconContainer.addEventListener('click', function (event) { iconContainer.addEventListener('click', function (event) {
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block'; dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
event.stopPropagation(); event.stopPropagation();
}); });
document.addEventListener('click', function () { document.addEventListener('click', function () {
dropdownMenu.style.display = 'none'; dropdownMenu.style.display = 'none';
}); });
dropdownMenu.addEventListener('click', function (event) { dropdownMenu.addEventListener('click', function (event) {
event.stopPropagation(); event.stopPropagation();
}); });
});
// Делегируем клики для открытия логина/регистрации внутри любых модалок
document.addEventListener('click', function (e) {
const target = e.target.closest('[data-action]');
if (!target) return;
e.preventDefault();
const action = target.dataset.action;
if (action === 'open-register') {
openRegisterModal();
} else if (action === 'open-login') {
openLoginModal();
}
});
});
function closeAllModals() {
const modalContainer = document.getElementById('modal-container');
if (modalContainer) modalContainer.remove();
}
function openRegisterModal() {
closeAllModals();
const modalContainer = document.createElement('div');
modalContainer.id = 'modal-container';
document.body.appendChild(modalContainer);
fetch('/register-modal')
.then(response => response.text())
.then(html => {
modalContainer.innerHTML = html;
reloadRecaptcha(modalContainer);
});
}
function openLoginModal() {
closeAllModals();
const modalContainer = document.createElement('div');
modalContainer.id = 'modal-container';
document.body.appendChild(modalContainer);
fetch('/login-modal')
.then(response => response.text())
.then(html => {
modalContainer.innerHTML = html;
reloadRecaptcha(modalContainer);
});
}
function reloadRecaptcha(modalContainer) {
const recaptchaScript = modalContainer.querySelector('script[src*="recaptcha"]');
if (recaptchaScript) recaptchaScript.remove();
const script = document.createElement('script');
script.src = 'https://www.google.com/recaptcha/api.js?render=explicit&onload=recaptchaCallback';
document.head.appendChild(script);
}
window.recaptchaCallback = function () {
const recaptchaElement = document.querySelector('.g-recaptcha');
if (recaptchaElement && typeof grecaptcha !== 'undefined') {
grecaptcha.render(recaptchaElement, {
theme: "dark",
size: "default"
});
}
};
document.addEventListener('click', function (e) {
const modal = document.querySelector('#modal-container .modal');
if (modal && !modal.contains(e.target)) {
closeAllModals();
}
});

View File

@ -1,22 +1,22 @@
window.addEventListener('DOMContentLoaded', function() { window.addEventListener('DOMContentLoaded', function() {
const listButton = document.querySelector('.list-button'); const listButton = document.querySelector('.list-button');
const tags = document.querySelectorAll('.tag'); const tags = document.querySelectorAll('.tag');
const buttonRect = listButton.getBoundingClientRect(); const buttonRect = listButton.getBoundingClientRect();
function checkTagVisibility() { function checkTagVisibility() {
tags.forEach(tag => { tags.forEach(tag => {
const tagRect = tag.getBoundingClientRect(); const tagRect = tag.getBoundingClientRect();
if (tagRect.right > buttonRect.left) { if (tagRect.right > buttonRect.left) {
tag.style.visibility = 'hidden'; tag.style.visibility = 'hidden';
} else { } else {
tag.style.visibility = 'visible'; tag.style.visibility = 'visible';
} }
}); });
} }
document.querySelector('.tags-container').addEventListener('scroll', checkTagVisibility); document.querySelector('.tags-container').addEventListener('scroll', checkTagVisibility);
checkTagVisibility(); checkTagVisibility();
}); });

View File

@ -1,40 +1,40 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head> </head>
<body> <body>
{% include 'navbar.html' %} {% include 'navbar.html' %}
{% include 'subnav.html' %} {% include 'subnav.html' %}
<div class="container img-new-content"> <div class="container img-new-content">
<a class="new-content-text">ПОПУЛЯРНОЕ</a> <a class="new-content-text">ПОПУЛЯРНОЕ</a>
<div class="ac-img-small-cards-grid"> <div class="ac-img-small-cards-grid">
{% for i in range(36) %} {% for i in range(36) %}
<div class="ct-img-card"> <div class="ct-img-card">
<div class="ct-img-card-cover"></div> <div class="ct-img-card-cover"></div>
<div class="ct-small-card-info"> <div class="ct-small-card-info">
<div class="ct-small-card-header"> <div class="ct-small-card-header">
<span class="ct-small-card-text">Totoka</span> <span class="ct-small-card-text">Totoka</span>
<div class="ct-small-card-stats"> <div class="ct-small-card-stats">
<div class="ct-small-stat"> <div class="ct-small-stat">
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20"> <img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
<span class="ct-small-card-text">134</span> <span class="ct-small-card-text">134</span>
</div> </div>
<div class="ct-small-stat"> <div class="ct-small-stat">
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20"> <img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
<span class="ct-small-card-text">32113</span> <span class="ct-small-card-text">32113</span>
</div> </div>
</div> </div>
</div> </div>
<p class="ct-small-card-text">Big Brother Keep Hugging Me</p> <p class="ct-small-card-text">Big Brother Keep Hugging Me</p>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<button class="most-new-button"> <button class="most-new-button">
<span class="most-new-button-text">Смотреть Больше</span> <span class="most-new-button-text">Смотреть Больше</span>
</button> </button>
</div> </div>
{% include 'pagination.html' %} {% include 'pagination.html' %}
</body> </body>
</html> </html>

View File

@ -1,113 +1,113 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head> </head>
<body> <body>
{% include 'navbar.html' %} {% include 'navbar.html' %}
{% include 'subnav.html' %} {% include 'subnav.html' %}
{% include 'tags_list.html' %} {% include 'tags_list.html' %}
<div class="container img-new-content"> <div class="container img-new-content">
<a class="new-content-text">НОВИНКИ</a> <a class="new-content-text">НОВИНКИ</a>
<div class="img-small-cards-grid"> <div class="img-small-cards-grid">
{% for i in range(7) %} {% for i in range(7) %}
<div class="img-small-card"> <div class="img-small-card">
<div class="img-small-card-cover"></div> <div class="img-small-card-cover"></div>
<div class="small-card-info"> <div class="small-card-info">
<div class="small-card-header"> <div class="small-card-header">
<span class="small-card-text">Totoka</span> <span class="small-card-text">Totoka</span>
<div class="small-card-stats"> <div class="small-card-stats">
<div class="small-stat"> <div class="small-stat">
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20"> <img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
<span class="small-card-text">134</span> <span class="small-card-text">134</span>
</div> </div>
<div class="small-stat"> <div class="small-stat">
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20"> <img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
<span class="small-card-text">32113</span> <span class="small-card-text">32113</span>
</div> </div>
</div> </div>
</div> </div>
<p class="small-card-text">Big Brother Keep Hugging Me</p> <p class="small-card-text">Big Brother Keep Hugging Me</p>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<button class="view-more-button" style="width: 1503px; height: 40px; gap: 10px; padding: 10px 86px;"> <button class="view-more-button" style="width: 1503px; height: 40px; gap: 10px; padding: 10px 86px;">
<span class="new-context-button-text">Смотреть Больше</span> <span class="new-context-button-text">Смотреть Больше</span>
</button> </button>
</div> </div>
<div class="container img-popular-content"> <div class="container img-popular-content">
<a class="popular-content-text">ПОПУЛЯРНОЕ</a> <a class="popular-content-text">ПОПУЛЯРНОЕ</a>
<div class="img-cards-grid"> <div class="img-cards-grid">
{% for i in range(12) %} {% for i in range(12) %}
<div class="img-card"> <div class="img-card">
<div class="img-card-cover"></div> <div class="img-card-cover"></div>
<div class="card-info"> <div class="card-info">
<div class="card-header"> <div class="card-header">
<span class="card-title" style="color: #3C3882;">Totoka</span> <span class="card-title" style="color: #3C3882;">Totoka</span>
<div class="card-stats"> <div class="card-stats">
<div class="stat"> <div class="stat">
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20"> <img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
<span style="color: #8784C9;">134</span> <span style="color: #8784C9;">134</span>
</div> </div>
<div class="stat"> <div class="stat">
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20"> <img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
<span style="color: #8784C9;">32113</span> <span style="color: #8784C9;">32113</span>
</div> </div>
</div> </div>
</div> </div>
<p class="card-text" style="font-family: Nunito, sans-serif;">Big Brother Keep Hugging Me </p> <p class="card-text" style="font-family: Nunito, sans-serif;">Big Brother Keep Hugging Me </p>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<button class="view-more-button" style="width: 1500px; height: 40px; padding: 10px 86px;"> <button class="view-more-button" style="width: 1500px; height: 40px; padding: 10px 86px;">
<span class="new-context-button-text">Смотреть Больше</span> <span class="new-context-button-text">Смотреть Больше</span>
</button> </button>
</div> </div>
<div class="container img-viewed-content"> <div class="container img-viewed-content">
<a class="viewed-content-text">ПРОСМАТРИВАЕМОЕ</a> <a class="viewed-content-text">ПРОСМАТРИВАЕМОЕ</a>
<div class="img-cards-grid"> <div class="img-cards-grid">
{% for i in range(12) %} {% for i in range(12) %}
<div class="img-card"> <div class="img-card">
<div class="img-card-cover"></div> <div class="img-card-cover"></div>
<div class="card-info"> <div class="card-info">
<div class="card-header"> <div class="card-header">
<span class="card-title" style="color: #3C3882;">Totoka</span> <span class="card-title" style="color: #3C3882;">Totoka</span>
<div class="card-stats"> <div class="card-stats">
<div class="stat"> <div class="stat">
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20"> <img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
<span style="color: #8784C9;">134</span> <span style="color: #8784C9;">134</span>
</div> </div>
<div class="stat"> <div class="stat">
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20"> <img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
<span style="color: #8784C9;">32113</span> <span style="color: #8784C9;">32113</span>
</div> </div>
</div> </div>
</div> </div>
<p class="card-text" style="font-family: Nunito, sans-serif;">Big Brother Keep Hugging Me </p> <p class="card-text" style="font-family: Nunito, sans-serif;">Big Brother Keep Hugging Me </p>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<button class="view-more-button" style="width: 1500px; height: 40px; padding: 10px 86px;margin-bottom: -170px;"> <button class="view-more-button" style="width: 1500px; height: 40px; padding: 10px 86px;margin-bottom: -170px;">
<span class="new-context-button-text">Смотреть Больше</span> <span class="new-context-button-text">Смотреть Больше</span>
</button> </button>
</div> </div>
<div class="container img-popular-categories"> <div class="container img-popular-categories">
<a class="popular-categories-text">ПОПУЛЯРНЫЕ КАТЕГОРИИ</a> <a class="popular-categories-text">ПОПУЛЯРНЫЕ КАТЕГОРИИ</a>
{% for i in range(6) %} {% for i in range(6) %}
<div class="pc-card"> <div class="pc-card">
<div class="pc-card-cover"></div> <div class="pc-card-cover"></div>
<div class="pc-card-info"> <div class="pc-card-info">
<div class="pc-card-stats"> <div class="pc-card-stats">
<span class="category-name-text">Category</span> <span class="category-name-text">Category</span>
<span class="categories-number" style="--length: 4;">14655</span> <span class="categories-number" style="--length: 4;">14655</span>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button> <button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,14 +1,14 @@
{% extends "content.html" %} {% extends "content.html" %}
{% include 'subnav.html' %} {% include 'subnav.html' %}
{% block title %}🫐videos - artberry🫐{% endblock %} {% block title %}🫐videos - artberry🫐{% endblock %}
{% block new_content %} {% block new_content %}
<div class="container new-content"> <div class="container new-content">
<a class="new-content-text">НОВИНКИ (Updated for Videos)</a> <a class="new-content-text">НОВИНКИ (Updated for Videos)</a>
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button> <button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
</div> </div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include 'tags_list.html' %} {% include 'tags_list.html' %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,47 @@
<div id="loginModal" class="modal active">
<form method="POST" action="{{ url_for('auth.login') }}">
{{ form.hidden_tag() }}
<div class="form-inner-container">
<p class="login-form-title">ВХОД</p>
<div class="modal-login-input-container">
{{ form.username(class_="modal-login-text-input", placeholder="Имя пользователя") }}
</div>
<div class="modal-login-input-container">
{{ form.password(class_="modal-login-text-input password-input", placeholder="Пароль") }}
</div>
<div class="login-link-container">
<span class="login-prompt">
<a href="#" class="login-link" data-action="forgot-password">Забыли пароль?</a>
</span>
</div>
<div class="login-link-container">
<span class="login-prompt">
Нет аккаунта?
<a href="#" class="login-link" data-action="open-register">Зарегистрируйся тут!</a>
</span>
</div>
<div class="recaptcha-wrapper">
{{ form.recaptcha() }}
</div>
<button type="submit" class="modal-login-button">
<span class="modal-login-button-text">Войти</span>
</button>
</div>
</form>
</div>
<script>
document.addEventListener('click', function(e) {
if (e.target.matches('[data-action="open-register"]')) {
e.preventDefault();
closeAllModals();
openRegisterModal();
}
});
</script>

View File

@ -1,35 +1,60 @@
{% extends "auth.html" %} <!DOCTYPE html>
{% block title %}🫐login - artberry🫐{% endblock %} <html lang="ru">
{% set action = 'auth.login' %} <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@500&display=swap" rel="stylesheet">
</head>
<body>
<div id="loginModal" class="modal active">
<form method="POST" action="{{ url_for('auth.register') }}">
{{ form.hidden_tag() }}
<div class="form-inner-container">
<p class="login-form-title">ВХОД</p>
<div class="modal-login-input-container">
{{ form.username(class_="modal-login-text-input", placeholder="Имя пользователя") }}
</div>
<div class="modal-login-input-container">
{{ form.password(class_="modal-login-text-input password-input", placeholder="Пароль") }}
</div>
{% block content %} <div class="login-link-container">
<form method="POST" action="{{ url_for(action) }}"> <span class="login-prompt">
{{ form.hidden_tag() }} <a href="#" class="login-link" data-action="forgot-password">Забыли пароль?</a>
</span>
{% for field, errors in form.errors.items() %} </div>
<ul>
{% for error in errors %} <div class="login-link-container">
<li><strong>{{ field.label }}:</strong> {{ error }}</li> <span class="login-prompt">
{% endfor %} Нет аккаунта?
</ul> <a href="#" class="login-link" data-action="open-register">Зарегистрируйся тут!</a>
{% endfor %} </span>
</div>
<label for="{{ form.username.id }}">{{ form.username.label.text }}</label> <div class="recaptcha-wrapper">
{{ form.username(class="input-field", placeholder="Enter username") }}<br> {{ form.recaptcha() }}
</div>
<label for="{{ form.password.id }}">{{ form.password.label.text }}</label>
{{ form.password(class="input-field", placeholder="Enter password") }}<br> <button type="submit" class="modal-login-button">
<span class="modal-login-button-text">Войти</span>
<div class="recaptcha-container"> </button>
{{ form.recaptcha.label }} </div>
{{ form.recaptcha() }}<br> </form>
</div> </div>
<script>
document.addEventListener('click', function(e) {
if (e.target.matches('[data-action="open-register"]')) {
e.preventDefault();
closeAllModals();
openRegisterModal();
}
});
</script>
{{ form.submit(class="login-button button", value="Login") }} </body>
</form> </html>
<div class="link-container">
<span class="link-text">Don't have an account?</span>
<a href="{{ url_for('auth.register') }}" class="button">Register</a>
</div>
{% endblock %}

View File

@ -1,55 +1,59 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head> </head>
<body> <body>
<div class="navbar-wrapper"> <div class="navbar-wrapper">
<div class="navbar"> <div class="navbar">
<img src="{{ url_for('static', filename='navbar/logo.svg') }}" alt="Логотип" class="logo"> <img src="{{ url_for('static', filename='navbar/logo.svg') }}" alt="Логотип" class="logo">
<div class="search-container"> <div class="search-container">
<div class="search-icon-container"> <div class="search-icon-container">
<img src="{{ url_for('static', filename='navbar/search-icon.svg') }}" alt="Поиск" class="search-icon"> <img src="{{ url_for('static', filename='navbar/search-icon.svg') }}" alt="Поиск" class="search-icon">
<img src="{{ url_for('static', filename='navbar/search-hover.svg') }}" alt="Поиск (hover)" <img src="{{ url_for('static', filename='navbar/search-hover.svg') }}" alt="Поиск (hover)"
class="search-hover-icon"> class="search-hover-icon">
</div> </div>
<input type="text" placeholder="Поиск" class="search-input" onfocus="this.placeholder=''" onblur="this.placeholder='Поиск'"> <input type="text" placeholder="Поиск" class="search-input" onfocus="this.placeholder=''" onblur="this.placeholder='Поиск'">
<div class="icon-container"> <div class="icon-container">
<img src="{{ url_for('static', filename='navbar/video-icon.svg') }}" alt="Видео" class="video-icon"> <img src="{{ url_for('static', filename='navbar/video-icon.svg') }}" alt="Видео" class="video-icon">
<img src="{{ url_for('static', filename='navbar/tray-icon.svg') }}" alt="Поднос" class="tray-icon"> <img src="{{ url_for('static', filename='navbar/tray-icon.svg') }}" alt="Поднос" class="tray-icon">
<div class="dropdown-menu"> <div class="dropdown-menu">
<div class="dropdown-item">Пункт 1</div> <div class="dropdown-item">Пункт 1</div>
<div class="dropdown-item">Пункт 2</div> <div class="dropdown-item">Пункт 2</div>
<div class="dropdown-item">Пункт 3</div> <div class="dropdown-item">Пункт 3</div>
</div> </div>
</div> </div>
</div> </div>
<div class="translate-btn"> <div class="translate-btn">
<img src="{{ url_for('static', filename='navbar/translate-icon.svg') }}" alt="Перевод" <img src="{{ url_for('static', filename='navbar/translate-icon.svg') }}" alt="Перевод"
class="translate-icon"> class="translate-icon">
<img src="{{ url_for('static', filename='navbar/translate-hover.svg') }}" alt="Перевод (hover)" <img src="{{ url_for('static', filename='navbar/translate-hover.svg') }}" alt="Перевод (hover)"
class="translate-hover-icon"> class="translate-hover-icon">
</div> </div>
<nav class="menu"> <nav class="menu">
<a href="/videos">ВИДЕО</a> <a href="/videos">ВИДЕО</a>
<a href="/">АРТЫ</a> <a href="/">АРТЫ</a>
<a href="/comics">МАНГА</a> <a href="/comics">МАНГА</a>
<a href="/gifs">ГИФКИ</a> <a href="/gifs">ГИФКИ</a>
</nav> </nav>
<div class="auth-container"> <div class="auth-container">
<div class="discord-icon-container"> <div class="discord-icon-container">
<img src="{{ url_for('static', filename='navbar/discord-icon.svg') }}" alt="Discord" <img src="{{ url_for('static', filename='navbar/discord-icon.svg') }}" alt="Discord"
class="discord-icon"> class="discord-icon">
<img src="{{ url_for('static', filename='navbar/discord-hover.svg') }}" alt="Discord Hover" <img src="{{ url_for('static', filename='navbar/discord-hover.svg') }}" alt="Discord Hover"
class="discord-hover-icon"> class="discord-hover-icon">
</div> </div>
<a href="{{ url_for('auth.login') }}"><button class="login-btn">ВОЙТИ</button></a> <button class="login-btn" onclick="openRegisterModal()">ВОЙТИ</button>
</div> </div>
</div> </div>
</div> </div>
<script src="{{ url_for('static', filename='js/navbar.js') }}"></script> <script src="{{ url_for('static', filename='js/navbar.js') }}"></script>
</body> <script>
window.recaptchaSiteKey = "{{ recaptcha_key }}";
</script>
</body>
</html> </html>

View File

@ -1,13 +1,13 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<div class="pagination-container"> <div class="pagination-container">
<button class="swipe-button left-swipe-button"><img src="{{ url_for('static', filename='/icons/left-arrow.svg') }}"></button> <button class="swipe-button left-swipe-button"><img src="{{ url_for('static', filename='/icons/left-arrow.svg') }}"></button>
<div class="page-buttons-container"> <div class="page-buttons-container">
<button class="page-button">1</button> <button class="page-button">1</button>
<button class="page-button">2</button> <button class="page-button">2</button>
<button class="page-button">3</button> <button class="page-button">3</button>
<button class="page-button">4</button> <button class="page-button">4</button>
<button class="page-button">5</button> <button class="page-button">5</button>
<button class="page-button">10</button> <button class="page-button">10</button>
</div> </div>
<button class="swipe-button right-swipe-button"><img src="{{ url_for('static', filename='/icons/right-arrow.svg') }}"></button> <button class="swipe-button right-swipe-button"><img src="{{ url_for('static', filename='/icons/right-arrow.svg') }}"></button>
</div> </div>

View File

@ -0,0 +1,35 @@
<div id="registerModal" class="modal active">
<form method="POST" action="{{ url_for('auth.register') }}">
{{ form.hidden_tag() }}
<div class="form-inner-container">
<p class="reg-form-title">РЕГИСТРАЦИЯ</p>
<div class="modal-register-input-container">
{{ form.username(class_="modal-register-text-input", placeholder="Имя пользователя") }}
</div>
<div class="modal-register-input-container">
{{ form.password(class_="modal-register-text-input password-input", placeholder="Пароль") }}
</div>
<div class="modal-register-input-container">
{{ form.confirm_password(class_="modal-register-text-input password-input", placeholder="Повтори пароль") }}
</div>
<div class="login-link-container">
<span class="login-prompt">
Уже есть аккаунт?
<a href="#" class="login-link" data-action="open-login">Войди тут!</a>
</span>
</div>
<div class="recaptcha-wrapper">
{{ form.recaptcha() }}
</div>
<button type="submit" class="modal-register-button">
<span class="modal-register-button-text">Зарегистрироваться</span>
</button>
</div>
</form>
</div>

View File

@ -1,28 +1,54 @@
{% extends "auth.html" %} <!DOCTYPE html>
{% block title %}🫐register - artberry🫐{% endblock %} <html lang="ru">
{% set action = 'auth.register' %} <head>
{% block content %} <meta charset="UTF-8">
<form method="POST" action="{{ url_for(action) }}"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{{ form.hidden_tag() }} <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@500&display=swap" rel="stylesheet">
<label for="{{ form.username.id }}">{{ form.username.label.text }}</label> </head>
{{ form.username(class="input-field", placeholder="Enter username") }}<br> <body>
<div id="registerModal" class="modal active">
<label for="{{ form.password.id }}">{{ form.password.label.text }}</label> <form method="POST" action="{{ url_for('auth.register') }}">
{{ form.password(class="input-field", placeholder="Enter password") }}<br> {{ form.hidden_tag() }}
<div class="form-inner-container">
<label for="{{ form.confirm_password.id }}">{{ form.confirm_password.label.text }}</label> <p class="reg-form-title">РЕГИСТРАЦИЯ</p>
{{ form.confirm_password(class="input-field", placeholder="Repeat password") }}<br>
<div class="modal-register-input-container">
<div class="recaptcha-container"> {{ form.username(class_="modal-register-text-input", placeholder="Имя пользователя") }}
{{ form.recaptcha.label }} </div>
{{ form.recaptcha() }}<br>
<div class="modal-register-input-container">
{{ form.password(class_="modal-register-text-input password-input", placeholder="Пароль") }}
</div>
<div class="modal-register-input-container">
{{ form.confirm_password(class_="modal-register-text-input password-input", placeholder="Повтори пароль") }}
</div>
<div class="login-link-container">
<span class="login-prompt">
Уже есть аккаунт?
<a href="#" class="login-link" data-action="open-login">Войди тут!</a>
</span>
</div>
<div class="recaptcha-wrapper">
{{ form.recaptcha() }}
</div>
<button type="submit" class="modal-register-button">
<span class="modal-register-button-text">Зарегестрироваться</span>
</button>
</div>
</form>
</div> </div>
</body>
{{ form.submit(class="login-button button", value="Register") }} <script>
</form> function openLoginModal() {
<div class="link-container"> document.getElementById('registerModal').classList.remove('active');
<span class="link-text">Already have an account?</span> document.getElementById('loginModal').classList.add('active');
<a href="{{ url_for('auth.login') }}" class="button">Login</a> }
</div>
{% endblock %} </script>
</html>

View File

@ -1,51 +1,51 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style> <style>
body { body {
margin: 0; margin: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.subnav-container { .subnav-container {
width: 100%; width: 100%;
height: 50px; height: 50px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background-color: #3C3882; background-color: #3C3882;
padding: 0 20px; padding: 0 20px;
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
top: 112px; top: 112px;
} }
.button { .button {
flex: 1; flex: 1;
height: 50px; height: 50px;
background-color: #3C3882; background-color: #3C3882;
border: none; border: none;
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
color: #8784C9; color: #8784C9;
box-sizing: border-box; box-sizing: border-box;
padding: 15px 0; padding: 15px 0;
} }
.button:not(:last-child) { .button:not(:last-child) {
margin-right: 20px; margin-right: 20px;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="subnav-container"> <div class="subnav-container">
<button class="button">ТЕГИ</button> <button class="button">ТЕГИ</button>
<button class="button">КАТЕГОРИИ</button> <button class="button">КАТЕГОРИИ</button>
<button class="button">ПЕРСОНАЖИ</button> <button class="button">ПЕРСОНАЖИ</button>
<button class="button">КОЛЛЕКЦИИ</button> <button class="button">КОЛЛЕКЦИИ</button>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,21 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<body> <body>
<div class="tags-container"> <div class="tags-container">
{% for tag in tags %} {% for tag in tags %}
<button class="tag">{{ tag }}</button> <button class="tag">{{ tag }}</button>
{% endfor %} {% endfor %}
<button class="list-button"> <button class="list-button">
<img src="{{ url_for('static', filename='icons/list-icon.svg') }}" alt="List Icon"> <img src="{{ url_for('static', filename='icons/list-icon.svg') }}" alt="List Icon">
</button> </button>
<div class="taglist-shadow"></div> <div class="taglist-shadow"></div>
</div> </div>
<script src="{{ url_for('static', filename='js/taglist.js') }}"></script> <script src="{{ url_for('static', filename='js/taglist.js') }}"></script>
</body> </body>
</head> </head>
</html> </html>

View File

@ -1,129 +1,129 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head> <link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}" type="image/svg+xml">
<body> </head>
{% include 'navbar.html' %} <body>
{% include 'subnav.html' %} {% include 'navbar.html' %}
{% include 'tags_list.html' %} {% include 'subnav.html' %}
<div class="container new-content"> {% include 'tags_list.html' %}
<a class="new-content-text">НОВИНКИ</a> <div class="container new-content">
<div class="small-cards-grid"> <a class="new-content-text">НОВИНКИ</a>
{% for video_data in videos[:5] %} <div class="small-cards-grid">
<div class="small-card"> {% for video_data in videos[:5] %}
<div class="small-card-cover"> <div class="small-card">
<img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail"> <div class="small-card-cover">
<video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}"> <img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail">
<source type="video/mp4"> <video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}">
Your browser does not support the video tag. <source type="video/mp4">
</video> Your browser does not support the video tag.
</div> </video>
<div class="small-card-info"> </div>
<div class="small-card-header"> <div class="small-card-info">
<span class="small-card-text">{{ video_data.video.username }}</span> <div class="small-card-header">
<div class="small-card-stats"> <span class="small-card-text">{{ video_data.video.username }}</span>
<div class="small-stat"> <div class="small-card-stats">
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20"> <div class="small-stat">
<span class="small-card-text">{{ video_data.video.cookie_votes }}</span> <img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
</div> <span class="small-card-text">{{ video_data.video.cookie_votes }}</span>
<div class="small-stat"> </div>
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20"> <div class="small-stat">
<span class="small-card-text">{{ video_data.views_count }}</span> <img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
</div> <span class="small-card-text">{{ video_data.views_count }}</span>
</div> </div>
</div> </div>
<p class="small-card-text">{{ video_data.video.video_name }}</p> </div>
</div> <p class="small-card-text">{{ video_data.video.video_name }}</p>
</div> </div>
{% endfor %} </div>
</div> {% endfor %}
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button> </div>
</div> <button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
<div class="container popular-content"> </div>
<a class="popular-content-text">ПОПУЛЯРНОЕ</a> <div class="container popular-content">
<div class="cards-grid"> <a class="popular-content-text">ПОПУЛЯРНОЕ</a>
{% for video_data in popular_videos[:8] %} <div class="cards-grid">
<div class="card"> {% for video_data in popular_videos[:8] %}
<div class="card-cover"> <div class="card">
<img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail"> <div class="card-cover">
<video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}"> <img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail">
<source type="video/mp4"> <video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}">
Your browser does not support the video tag. <source type="video/mp4">
</video> Your browser does not support the video tag.
</div> </video>
<div class="card-info"> </div>
<div class="card-header"> <div class="card-info">
<span class="card-title" style="color: #3C3882;">{{ video_data.video.username }}</span> <div class="card-header">
<div class="card-stats"> <span class="card-title" style="color: #3C3882;">{{ video_data.video.username }}</span>
<div class="stat"> <div class="card-stats">
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20"> <div class="stat">
<span style="color: #8784C9;">{{ video_data.video.cookie_votes }}</span> <img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
</div> <span style="color: #8784C9;">{{ video_data.video.cookie_votes }}</span>
<div class="stat"> </div>
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20"> <div class="stat">
<span style="color: #8784C9;">{{ video_data.views_count }}</span> <img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
</div> <span style="color: #8784C9;">{{ video_data.views_count }}</span>
</div> </div>
</div> </div>
<p class="card-text" style="font-family: Nunito, sans-serif;">{{ video_data.video.video_name }}</p> </div>
</div> <p class="card-text" style="font-family: Nunito, sans-serif;">{{ video_data.video.video_name }}</p>
</div> </div>
{% endfor %} </div>
</div> {% endfor %}
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button> </div>
</div> <button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
</div>
<div class="container viewed-content">
<a class="viewed-content-text">ПРОСМАТРИВАЕМОЕ</a> <div class="container viewed-content">
<div class="cards-grid"> <a class="viewed-content-text">ПРОСМАТРИВАЕМОЕ</a>
{% for video_data in most_viewed_videos[:8] %} <div class="cards-grid">
<div class="card"> {% for video_data in most_viewed_videos[:8] %}
<div class="card-cover"> <div class="card">
<img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail"> <div class="card-cover">
<video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}"> <img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail">
<source type="video/mp4"> <video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}">
Your browser does not support the video tag. <source type="video/mp4">
</video> Your browser does not support the video tag.
</div> </video>
<div class="card-info"> </div>
<div class="card-header"> <div class="card-info">
<span class="card-title" style="color: #3C3882;">{{ video_data.video.username }}</span> <div class="card-header">
<div class="card-stats"> <span class="card-title" style="color: #3C3882;">{{ video_data.video.username }}</span>
<div class="stat"> <div class="card-stats">
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20"> <div class="stat">
<span style="color: #8784C9;">{{ video_data.video.cookie_votes }}</span> <img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
</div> <span style="color: #8784C9;">{{ video_data.video.cookie_votes }}</span>
<div class="stat"> </div>
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20"> <div class="stat">
<span style="color: #8784C9;">{{ video_data.views_count }}</span> <img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
</div> <span style="color: #8784C9;">{{ video_data.views_count }}</span>
</div> </div>
</div> </div>
<p class="card-text" style="font-family: Nunito, sans-serif;">{{ video_data.video.video_name }}</p> </div>
</div> <p class="card-text" style="font-family: Nunito, sans-serif;">{{ video_data.video.video_name }}</p>
</div> </div>
{% endfor %} </div>
</div> {% endfor %}
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button> </div>
</div> <button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
</div>
<div class="container popular-categories">
<a class="popular-categories-text">ПОПУЛЯРНЫЕ КАТЕГОРИИ</a> <div class="container popular-categories">
{% for i in range(6) %} <a class="popular-categories-text">ПОПУЛЯРНЫЕ КАТЕГОРИИ</a>
<div class="pc-card"> {% for i in range(6) %}
<div class="pc-card-cover"></div> <div class="pc-card">
<div class="pc-card-info"> <div class="pc-card-cover"></div>
<div class="pc-card-stats"> <div class="pc-card-info">
<span class="category-name-text">Category</span> <div class="pc-card-stats">
<span class="categories-number" style="--length: 4;">14655</span> <span class="category-name-text">Category</span>
</div> <span class="categories-number" style="--length: 4;">14655</span>
</div> </div>
</div> </div>
{% endfor %} </div>
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button> {% endfor %}
</div> <button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
<script src="{{ url_for('static', filename='js/hoverPreview.js') }}"></script> </div>
<script src="{{ url_for('static', filename='js/adjustScrollbar.js') }}"></script> <script src="{{ url_for('static', filename='js/hoverPreview.js') }}"></script>
</body> </body>
</html> </html>

View File

@ -1,132 +1,132 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>🫐Content View - Artberry🫐</title> <title>🫐Content View - Artberry🫐</title>
<link href="https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="icon" href="{{ url_for('static', filename='artberry.ico') }}" type="image/x-icon"> <link rel="icon" href="{{ url_for('static', filename='artberry.ico') }}" type="image/x-icon">
</head> </head>
<body> <body>
{% include 'navbar.html' %} {% include 'navbar.html' %}
{% if content_type == 'art' %} {% if content_type == 'art' %}
<h1>Image</h1> <h1>Image</h1>
<div class="details"> <div class="details">
<img src="{{ url_for('static', filename='arts/' + content.image_file) }}" alt="Art Image"> <img src="{{ url_for('static', filename='arts/' + content.image_file) }}" alt="Art Image">
<p><strong>Author:</strong> <a href="{{ url_for('profile', username=content.username) }}">{{ content.username }}</a></p> <p><strong>Author:</strong> <a href="{{ url_for('profile', username=content.username) }}">{{ content.username }}</a></p>
<p><strong>Publication Date:</strong> {{ content.publication_date }}</p> <p><strong>Publication Date:</strong> {{ content.publication_date }}</p>
<p><strong>Tags:</strong> <p><strong>Tags:</strong>
{% for tag in content.tags.split(',') %} {% for tag in content.tags.split(',') %}
<a href="{{ url_for('index', search=tag.strip()) }}" class="tag-link">{{ tag.strip() }}</a>{% if not loop.last %}, {% endif %} <a href="{{ url_for('index', search=tag.strip()) }}" class="tag-link">{{ tag.strip() }}</a>{% if not loop.last %}, {% endif %}
{% endfor %} {% endfor %}
</p> </p>
</div> </div>
{% elif content_type == 'video' %} {% elif content_type == 'video' %}
<h1>Video</h1> <h1>Video</h1>
<div class="video-details"> <div class="video-details">
<video controls> <video controls>
<source src="{{ url_for('static', filename='videos/' + content.video_file) }}" type="video/mp4"> <source src="{{ url_for('static', filename='videos/' + content.video_file) }}" type="video/mp4">
</video> </video>
<p><strong>Author:</strong> <a href="{{ url_for('profile', username=content.username) }}">{{ content.username }}</a></p> <p><strong>Author:</strong> <a href="{{ url_for('profile', username=content.username) }}">{{ content.username }}</a></p>
<p><strong>Publication Date:</strong> {{ content.publication_date }}</p> <p><strong>Publication Date:</strong> {{ content.publication_date }}</p>
<p>Description: {{ content.description }}</p> <p>Description: {{ content.description }}</p>
<p><strong>Tags:</strong> <p><strong>Tags:</strong>
{% for tag in content.tags.split(',') %} {% for tag in content.tags.split(',') %}
<a href="{{ url_for('videos', search=tag.strip()) }}" class="tag-link">{{ tag.strip() }}</a>{% if not loop.last %}, {% endif %} <a href="{{ url_for('videos', search=tag.strip()) }}" class="tag-link">{{ tag.strip() }}</a>{% if not loop.last %}, {% endif %}
{% endfor %} {% endfor %}
</p> </p>
</div> </div>
{% elif content_type == 'comic' %} {% elif content_type == 'comic' %}
<h1>{{ content.name }}</h1> <h1>{{ content.name }}</h1>
<div class="comic-pages"> <div class="comic-pages">
{% if comic_pages %} {% if comic_pages %}
{% for page in comic_pages %} {% for page in comic_pages %}
<img src="{{ url_for('static', filename=page.file_path.replace('static/', '').replace('\\', '/')) }}" alt="Page {{ page.page_number }}"> <img src="{{ url_for('static', filename=page.file_path.replace('static/', '').replace('\\', '/')) }}" alt="Page {{ page.page_number }}">
{% endfor %} {% endfor %}
{% else %} {% else %}
<p>No pages available for this comic.</p> <p>No pages available for this comic.</p>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% if current_user.is_authenticated and current_user.username == content.username %} {% if current_user.is_authenticated and current_user.username == content.username %}
<form method="POST" action="{{ url_for('delete', content_type=content_type, content_id=content.id) }}"> <form method="POST" action="{{ url_for('delete', content_type=content_type, content_id=content.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="button" onclick="return confirm('Are you sure you want to delete this content?');">Delete</button> <button type="submit" class="button" onclick="return confirm('Are you sure you want to delete this content?');">Delete</button>
</form> </form>
{% endif %} {% endif %}
{% if content_type != 'comic' %} {% if content_type != 'comic' %}
<section class="comments"> <section class="comments">
<h2>Comments</h2> <h2>Comments</h2>
<div class="comments-list"> <div class="comments-list">
{% for comment in comments %} {% for comment in comments %}
<div class="comment"> <div class="comment">
<a href="{{ url_for('profile', username=comment.username) }}"> <a href="{{ url_for('profile', username=comment.username) }}">
<img src="{{ url_for('static', filename='avatars/' + (avatars[comment.username] if avatars.get(comment.username) else 'default_avatar.png')) }}" <img src="{{ url_for('static', filename='avatars/' + (avatars[comment.username] if avatars.get(comment.username) else 'default_avatar.png')) }}"
alt="Avatar of {{ comment.username }}" class="avatar"> alt="Avatar of {{ comment.username }}" class="avatar">
{% if current_user.is_authenticated and comment.username == current_user.username %} {% if current_user.is_authenticated and comment.username == current_user.username %}
<form class="button" action="{{ url_for('delete_comment', comment_id=comment.id) }}" method="POST" style="display:inline;"> <form class="button" action="{{ url_for('delete_comment', comment_id=comment.id) }}" method="POST" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="delete-button" onclick="return confirm('Are you sure you want to delete this comment?')">Delete</button> <button type="submit" class="delete-button" onclick="return confirm('Are you sure you want to delete this comment?')">Delete</button>
</form> </form>
{% endif %} {% endif %}
</a> </a>
<div class="content"> <div class="content">
<p> <p>
<a href="{{ url_for('profile', username=comment.username) }}" class="username-link"> <a href="{{ url_for('profile', username=comment.username) }}" class="username-link">
<strong>{{ comment.username }}</strong> <strong>{{ comment.username }}</strong>
</a> </a>
({{ comment.comment_date.strftime('%Y-%m-%d %H:%M') }}): ({{ comment.comment_date.strftime('%Y-%m-%d %H:%M') }}):
</p> </p>
<p style="margin-top: 10px;">{{ comment.comment_text }}</p> <p style="margin-top: 10px;">{{ comment.comment_text }}</p>
</div> </div>
</div> </div>
{% else %} {% else %}
<p>No comments yet. Be the first to comment!</p> <p>No comments yet. Be the first to comment!</p>
{% endfor %} {% endfor %}
</div> </div>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<form method="POST" action="{{ url_for('view', content_type=content_type, id=content.id) }}" class="comment-form"> <form method="POST" action="{{ url_for('view', content_type=content_type, id=content.id) }}" class="comment-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<textarea name="comment" class="input-field" placeholder="Add a comment..." rows="3" maxlength="44" required></textarea> <textarea name="comment" class="input-field" placeholder="Add a comment..." rows="3" maxlength="44" required></textarea>
<button type="submit" class="button">Post Comment</button> <button type="submit" class="button">Post Comment</button>
</form> </form>
{% else %} {% else %}
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to post a comment.</p> <p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to post a comment.</p>
{% endif %} {% endif %}
</section> </section>
{% endif %} {% endif %}
{% if content_type != 'comic' %} {% if content_type != 'comic' %}
<div class="navigation"> <div class="navigation">
<a href="{{ url_for('view', content_type=content_type, id=prev_content.id, page=request.args.get('page', 1)) }}" class="button">&larr; Prev</a> <a href="{{ url_for('view', content_type=content_type, id=prev_content.id, page=request.args.get('page', 1)) }}" class="button">&larr; Prev</a>
<a href="{{ url_for('view', content_type=content_type, id=random_content.id, page=request.args.get('page', 1)) }}" class="button">Random</a> <a href="{{ url_for('view', content_type=content_type, id=random_content.id, page=request.args.get('page', 1)) }}" class="button">Random</a>
<a href="{{ url_for('view', content_type=content_type, id=next_content.id, page=request.args.get('page', 1)) }}" class="button">Next &rarr;</a> <a href="{{ url_for('view', content_type=content_type, id=next_content.id, page=request.args.get('page', 1)) }}" class="button">Next &rarr;</a>
{% if current_user.is_authenticated and content.username == current_user.username %} {% if current_user.is_authenticated and content.username == current_user.username %}
{% if content_type == 'art' %} {% if content_type == 'art' %}
<a href="{{ url_for('image_edit', id=content.id) }}" class="button">Edit Art</a> <a href="{{ url_for('image_edit', id=content.id) }}" class="button">Edit Art</a>
{% elif content_type == 'video' %} {% elif content_type == 'video' %}
<a href="{{ url_for('video_edit', id=content.id) }}" class="button">Edit Video</a> <a href="{{ url_for('video_edit', id=content.id) }}" class="button">Edit Video</a>
{% elif content_type == 'comic' %} {% elif content_type == 'comic' %}
<a href="{{ url_for('comic_edit', id=content.id) }}" class="button">Edit Comic</a> <a href="{{ url_for('comic_edit', id=content.id) }}" class="button">Edit Comic</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div class="vote-section"> <div class="vote-section">
<p>Votes: {{ content.cookie_votes }} 🍪</p> <p>Votes: {{ content.cookie_votes }} 🍪</p>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<form action="{{ url_for('vote_' + content_type, **({'image_id': content.id} if content_type == 'art' else {'video_id': content.id} if content_type == 'video' else {'comic_id': content.id})) }}" method="POST"> <form action="{{ url_for('vote_' + content_type, **({'image_id': content.id} if content_type == 'art' else {'video_id': content.id} if content_type == 'video' else {'comic_id': content.id})) }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="button">Vote</button> <button type="submit" class="button">Vote</button>
</form> </form>
{% else %} {% else %}
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to vote.</p> <p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to vote.</p>
{% endif %} {% endif %}
</div> </div>
</body> </body>
</html> </html>

396
upload.py
View File

@ -1,199 +1,199 @@
import os import os
import uuid import uuid
import aiofiles import aiofiles
from flask import Blueprint, render_template, redirect, url_for, request, current_app from flask import Blueprint, render_template, redirect, url_for, request, current_app
from flask_login import login_required, current_user from flask_login import login_required, current_user
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from models import db, Image, Video, Comic, ComicPage, Post, Cookies, UploadForm, UploadVideoForm, UploadComicForm from models import db, Image, Video, Comic, ComicPage, Post, Cookies, UploadForm, UploadVideoForm, UploadComicForm
from utils import allowed_file, check_file_content, check_file_size, convert_to_webp, generate_unique_filename from utils import allowed_file, check_file_content, check_file_size, convert_to_webp, generate_unique_filename
upload_bp = Blueprint('upload', __name__) upload_bp = Blueprint('upload', __name__)
@upload_bp.route('/upload', methods=['GET', 'POST']) @upload_bp.route('/upload', methods=['GET', 'POST'])
@login_required @login_required
async def upload(): async def upload():
form = UploadForm() form = UploadForm()
if form.validate_on_submit(): if form.validate_on_submit():
image_file = form.image_file.data image_file = form.image_file.data
tags = form.tags.data tags = form.tags.data
allowed_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'} allowed_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
if not (allowed_file(image_file.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and if not (allowed_file(image_file.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
check_file_content(image_file, allowed_mime_types) and check_file_content(image_file, allowed_mime_types) and
await check_file_size(image_file, current_app.config['MAX_IMAGE_SIZE'])): await check_file_size(image_file, current_app.config['MAX_IMAGE_SIZE'])):
return redirect(url_for('upload.upload')) return redirect(url_for('upload.upload'))
unique_filename = f"{uuid.uuid4().hex}.webp" unique_filename = f"{uuid.uuid4().hex}.webp"
filepath = os.path.join(current_app.config['UPLOAD_FOLDER']['images'], unique_filename) filepath = os.path.join(current_app.config['UPLOAD_FOLDER']['images'], unique_filename)
if os.path.exists(filepath): if os.path.exists(filepath):
return redirect(url_for('upload.upload')) return redirect(url_for('upload.upload'))
webp_image = await convert_to_webp(image_file) webp_image = await convert_to_webp(image_file)
async with aiofiles.open(filepath, 'wb') as f: async with aiofiles.open(filepath, 'wb') as f:
await f.write(webp_image.read()) await f.write(webp_image.read())
img = Image(image_file=unique_filename, username=current_user.username, tags=tags, cookie_votes=0) img = Image(image_file=unique_filename, username=current_user.username, tags=tags, cookie_votes=0)
db.session.add(img) db.session.add(img)
user_cookie = Cookies.query.filter_by(username=current_user.username).first() user_cookie = Cookies.query.filter_by(username=current_user.username).first()
if user_cookie: if user_cookie:
user_cookie.cookies += 1 user_cookie.cookies += 1
else: else:
user_cookie = Cookies(username=current_user.username, cookies=1) user_cookie = Cookies(username=current_user.username, cookies=1)
db.session.add(user_cookie) db.session.add(user_cookie)
db.session.commit() db.session.commit()
return redirect(url_for('index')) return redirect(url_for('index'))
return render_template('upload.html', form=form) return render_template('upload.html', form=form)
@upload_bp.route('/upload_video', methods=['GET', 'POST']) @upload_bp.route('/upload_video', methods=['GET', 'POST'])
@login_required @login_required
async def upload_video(): async def upload_video():
form = UploadVideoForm() form = UploadVideoForm()
if form.validate_on_submit(): if form.validate_on_submit():
video_file = form.video_file.data video_file = form.video_file.data
video_thumbnail = form.thumbnail.data video_thumbnail = form.thumbnail.data
video_name = form.name.data video_name = form.name.data
tags = form.tags.data tags = form.tags.data
description = form.description.data description = form.description.data
allowed_video_mime_types = {'video/mp4', 'video/x-msvideo', 'video/quicktime'} allowed_video_mime_types = {'video/mp4', 'video/x-msvideo', 'video/quicktime'}
allowed_image_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'} allowed_image_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
if video_file and video_thumbnail: if video_file and video_thumbnail:
if not (allowed_file(video_file.filename, current_app.config['ALLOWED_VIDEO_EXTENSIONS']) and if not (allowed_file(video_file.filename, current_app.config['ALLOWED_VIDEO_EXTENSIONS']) and
check_file_content(video_file, allowed_video_mime_types)): check_file_content(video_file, allowed_video_mime_types)):
return redirect(url_for('upload.upload_video')) return redirect(url_for('upload.upload_video'))
if not await check_file_size(video_file, current_app.config['MAX_VIDEO_SIZE']): if not await check_file_size(video_file, current_app.config['MAX_VIDEO_SIZE']):
return redirect(url_for('upload.upload_video')) return redirect(url_for('upload.upload_video'))
if not (allowed_file(video_thumbnail.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and if not (allowed_file(video_thumbnail.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
check_file_content(video_thumbnail, allowed_image_mime_types)): check_file_content(video_thumbnail, allowed_image_mime_types)):
return redirect(url_for('upload.upload_video')) return redirect(url_for('upload.upload_video'))
video_filename = await generate_unique_filename(current_app.config['UPLOAD_FOLDER']['videos'], 'mp4') video_filename = await generate_unique_filename(current_app.config['UPLOAD_FOLDER']['videos'], 'mp4')
thumbnail_filename = f"{uuid.uuid4().hex}.webp" thumbnail_filename = f"{uuid.uuid4().hex}.webp"
video_path = os.path.join(current_app.config['UPLOAD_FOLDER']['videos'], video_filename) video_path = os.path.join(current_app.config['UPLOAD_FOLDER']['videos'], video_filename)
thumbnail_path = os.path.join(current_app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename) thumbnail_path = os.path.join(current_app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename)
async with aiofiles.open(video_path, 'wb') as f: async with aiofiles.open(video_path, 'wb') as f:
await f.write(video_file.read()) await f.write(video_file.read())
webp_thumbnail = await convert_to_webp(video_thumbnail) webp_thumbnail = await convert_to_webp(video_thumbnail)
async with aiofiles.open(thumbnail_path, 'wb') as f: async with aiofiles.open(thumbnail_path, 'wb') as f:
await f.write(webp_thumbnail.read()) await f.write(webp_thumbnail.read())
video = Video( video = Video(
video_file=video_filename, video_file=video_filename,
video_name=video_name, video_name=video_name,
video_thumbnail_file=thumbnail_filename, video_thumbnail_file=thumbnail_filename,
username=current_user.username, username=current_user.username,
tags=tags, tags=tags,
description=description description=description
) )
db.session.add(video) db.session.add(video)
db.session.commit() db.session.commit()
return redirect(url_for('videos')) return redirect(url_for('videos'))
return render_template('upload_video.html', form=form) return render_template('upload_video.html', form=form)
@upload_bp.route('/comic_upload', methods=['GET', 'POST']) @upload_bp.route('/comic_upload', methods=['GET', 'POST'])
@login_required @login_required
async def comic_upload(): async def comic_upload():
form = UploadComicForm() form = UploadComicForm()
if request.method == 'POST': if request.method == 'POST':
if form.validate_on_submit(): if form.validate_on_submit():
ct = form.thumbnail.data ct = form.thumbnail.data
n = form.title.data n = form.title.data
tags = form.tags.data tags = form.tags.data
allowed_image_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'} allowed_image_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
if db.session.execute(db.select(Comic).filter_by(name=n)).scalar(): if db.session.execute(db.select(Comic).filter_by(name=n)).scalar():
return render_template('comic_upload.html', form=form) return render_template('comic_upload.html', form=form)
if ct: if ct:
if not (allowed_file(ct.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and if not (allowed_file(ct.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
check_file_content(ct, allowed_image_mime_types)): check_file_content(ct, allowed_image_mime_types)):
return redirect(url_for('upload.comic_upload')) return redirect(url_for('upload.comic_upload'))
tf = f"{uuid.uuid4().hex}.webp" tf = f"{uuid.uuid4().hex}.webp"
tp = os.path.join(current_app.config['UPLOAD_FOLDER']['comicthumbs'], tf) tp = os.path.join(current_app.config['UPLOAD_FOLDER']['comicthumbs'], tf)
webp_thumbnail = await convert_to_webp(ct) webp_thumbnail = await convert_to_webp(ct)
async with aiofiles.open(tp, 'wb') as f: async with aiofiles.open(tp, 'wb') as f:
await f.write(webp_thumbnail.read()) await f.write(webp_thumbnail.read())
cf = os.path.join(current_app.config['UPLOAD_FOLDER']['comics'], n) cf = os.path.join(current_app.config['UPLOAD_FOLDER']['comics'], n)
os.makedirs(cf, exist_ok=True) os.makedirs(cf, exist_ok=True)
new_comic = Comic( new_comic = Comic(
comic_folder=n, comic_folder=n,
comic_thumbnail_file=tf, comic_thumbnail_file=tf,
username=current_user.username, username=current_user.username,
name=n, name=n,
tags=tags tags=tags
) )
db.session.add(new_comic) db.session.add(new_comic)
db.session.flush() db.session.flush()
async def save_pages(): async def save_pages():
pages = request.files.getlist('pages[]') pages = request.files.getlist('pages[]')
for i, p in enumerate(sorted(pages, key=lambda x: x.filename), start=1): for i, p in enumerate(sorted(pages, key=lambda x: x.filename), start=1):
if p: if p:
if not (allowed_file(p.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and if not (allowed_file(p.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
check_file_content(p, allowed_image_mime_types)): check_file_content(p, allowed_image_mime_types)):
return redirect(url_for('upload.comic_upload')) return redirect(url_for('upload.comic_upload'))
filename = f"{uuid.uuid4().hex}.webp" filename = f"{uuid.uuid4().hex}.webp"
file_path = os.path.join(cf, filename) file_path = os.path.join(cf, filename)
webp_page = await convert_to_webp(p) webp_page = await convert_to_webp(p)
async with aiofiles.open(file_path, 'wb') as f: async with aiofiles.open(file_path, 'wb') as f:
await f.write(webp_page.read()) await f.write(webp_page.read())
new_page = ComicPage(comic_id=new_comic.id, page_number=i, file_path=file_path) new_page = ComicPage(comic_id=new_comic.id, page_number=i, file_path=file_path)
db.session.add(new_page) db.session.add(new_page)
db.session.commit() db.session.commit()
await save_pages() await save_pages()
return redirect(url_for('comics')) return redirect(url_for('comics'))
else: else:
for field, errors in form.errors.items(): for field, errors in form.errors.items():
for error in errors: for error in errors:
pass pass
return render_template('comic_upload.html', form=form) return render_template('comic_upload.html', form=form)
@upload_bp.route('/upload_post', methods=['GET', 'POST']) @upload_bp.route('/upload_post', methods=['GET', 'POST'])
@login_required @login_required
async def upload_post(): async def upload_post():
if request.method == 'POST': if request.method == 'POST':
post_text = request.form.get('post_text') post_text = request.form.get('post_text')
post_media = request.files.get('post_media') post_media = request.files.get('post_media')
if post_text: if post_text:
new_post = Post( new_post = Post(
username=current_user.username, username=current_user.username,
text=post_text text=post_text
) )
db.session.add(new_post) db.session.add(new_post)
if post_media and allowed_file(post_media.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']): if post_media and allowed_file(post_media.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']):
if await check_file_size(post_media, current_app.config['MAX_IMAGE_SIZE']): if await check_file_size(post_media, current_app.config['MAX_IMAGE_SIZE']):
unique_filename = f"{uuid.uuid4().hex}.webp" unique_filename = f"{uuid.uuid4().hex}.webp"
media_path = os.path.join(current_app.config['UPLOAD_FOLDER']['posts'], unique_filename) media_path = os.path.join(current_app.config['UPLOAD_FOLDER']['posts'], unique_filename)
webp_image = await convert_to_webp(post_media) webp_image = await convert_to_webp(post_media)
async with aiofiles.open(media_path, 'wb') as f: async with aiofiles.open(media_path, 'wb') as f:
await f.write(webp_image.read()) await f.write(webp_image.read())
new_post.media_file = unique_filename new_post.media_file = unique_filename
else: else:
return redirect(url_for('upload.upload_post')) return redirect(url_for('upload.upload_post'))
db.session.commit() db.session.commit()
return redirect(url_for('user_posts', username=current_user.username)) return redirect(url_for('user_posts', username=current_user.username))
else: else:
return redirect(url_for('upload.upload_post')) return redirect(url_for('upload.upload_post'))
return render_template('upload_post.html') return render_template('upload_post.html')

266
utils.py
View File

@ -1,134 +1,134 @@
import os import os
import io import io
import uuid import uuid
import aiofiles import aiofiles
import asyncio import asyncio
import magic import magic
import re import re
from PIL import Image as PILImage from PIL import Image as PILImage
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from sqlalchemy import func, or_ from sqlalchemy import func, or_
from flask import request from flask import request
from models import db, Comments, Image, Video, Comic, Post, User from models import db, Comments, Image, Video, Comic, Post, User
def allowed_file(filename, allowed_extensions): def allowed_file(filename, allowed_extensions):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
async def check_file_size(file, max_size): async def check_file_size(file, max_size):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _sync_check_file_size, file, max_size) return await loop.run_in_executor(None, _sync_check_file_size, file, max_size)
def _sync_check_file_size(file, max_size): def _sync_check_file_size(file, max_size):
file.seek(0, os.SEEK_END) file.seek(0, os.SEEK_END)
file_size = file.tell() file_size = file.tell()
file.seek(0) file.seek(0)
return file_size <= max_size return file_size <= max_size
def check_file_content(file, allowed_mime_types): def check_file_content(file, allowed_mime_types):
mime = magic.Magic(mime=True) mime = magic.Magic(mime=True)
file_mime_type = mime.from_buffer(file.read(1024)) file_mime_type = mime.from_buffer(file.read(1024))
file.seek(0) file.seek(0)
return file_mime_type in allowed_mime_types return file_mime_type in allowed_mime_types
async def convert_to_webp(image_file): async def convert_to_webp(image_file):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _sync_convert_to_webp, image_file) return await loop.run_in_executor(None, _sync_convert_to_webp, image_file)
def _sync_convert_to_webp(image_file): def _sync_convert_to_webp(image_file):
with PILImage.open(image_file) as img: with PILImage.open(image_file) as img:
output = io.BytesIO() output = io.BytesIO()
img.convert("RGB").save(output, format="WEBP", quality=90, optimize=True) img.convert("RGB").save(output, format="WEBP", quality=90, optimize=True)
output.seek(0) output.seek(0)
return output return output
async def generate_unique_filename(upload_folder, extension): async def generate_unique_filename(upload_folder, extension):
while True: while True:
unique_filename = f"{uuid.uuid4().hex}.{extension}" unique_filename = f"{uuid.uuid4().hex}.{extension}"
file_path = os.path.join(upload_folder, unique_filename) file_path = os.path.join(upload_folder, unique_filename)
if not await aiofiles.os.path.exists(file_path): if not await aiofiles.os.path.exists(file_path):
return unique_filename return unique_filename
def update_related_tables(old_username, new_username): def update_related_tables(old_username, new_username):
models_to_update = [Comments, Image, Video, Comic, Post] models_to_update = [Comments, Image, Video, Comic, Post]
for model in models_to_update: for model in models_to_update:
for record in model.query.filter_by(username=old_username).all(): for record in model.query.filter_by(username=old_username).all():
record.username = new_username record.username = new_username
db.session.commit() db.session.commit()
def get_content_query(model, subscriptions, search_query): def get_content_query(model, subscriptions, search_query):
query = model.query query = model.query
if search_query: if search_query:
tags = [tag.strip().lower() for tag in search_query.replace(',', ' ').split()] tags = [tag.strip().lower() for tag in search_query.replace(',', ' ').split()]
filter_condition = [ filter_condition = [
model.tags.like(f'%{tag}%') for tag in tags model.tags.like(f'%{tag}%') for tag in tags
] ]
query = query.filter( query = query.filter(
or_(*filter_condition) or_(*filter_condition)
) )
if subscriptions: if subscriptions:
query = query.filter(or_( query = query.filter(or_(
model.username.in_(subscriptions), model.username.in_(subscriptions),
model.username.notin_(subscriptions) model.username.notin_(subscriptions)
)) ))
query = query.order_by( query = query.order_by(
func.coalesce(model.cookie_votes, 0).desc(), func.coalesce(model.cookie_votes, 0).desc(),
model.publication_date.desc() model.publication_date.desc()
) )
return query return query
def _sync_check_file_size(file, max_size): def _sync_check_file_size(file, max_size):
file.seek(0, os.SEEK_END) file.seek(0, os.SEEK_END)
file_size = file.tell() file_size = file.tell()
file.seek(0) file.seek(0)
return file_size <= max_size return file_size <= max_size
async def generate_unique_filename(filename, upload_folder): async def generate_unique_filename(filename, upload_folder):
base, ext = os.path.splitext(secure_filename(filename)) base, ext = os.path.splitext(secure_filename(filename))
while True: while True:
unique_filename = f"{base}_{uuid.uuid4().hex}{ext}" unique_filename = f"{base}_{uuid.uuid4().hex}{ext}"
file_path = os.path.join(upload_folder, unique_filename) file_path = os.path.join(upload_folder, unique_filename)
if not await aiofiles.os.path.exists(file_path): if not await aiofiles.os.path.exists(file_path):
return unique_filename return unique_filename
def get_client_ip(): def get_client_ip():
if 'X-Forwarded-For' in request.headers: if 'X-Forwarded-For' in request.headers:
forwarded_for = request.headers['X-Forwarded-For'] forwarded_for = request.headers['X-Forwarded-For']
ip_address = forwarded_for.split(',')[0] ip_address = forwarded_for.split(',')[0]
else: else:
ip_address = request.remote_addr ip_address = request.remote_addr
return ip_address return ip_address
def validate_username(self, username): def validate_username(self, username):
username.data = username.data.lower() username.data = username.data.lower()
user = User.query.filter_by(username=username.data).first() user = User.query.filter_by(username=username.data).first()
if user: if user:
return return
if not re.match(r'^[a-z0-9]+$', username.data): if not re.match(r'^[a-z0-9]+$', username.data):
return return
def validate_ip(self): def validate_ip(self):
ip_address = get_client_ip() ip_address = get_client_ip()
user_with_ip = User.query.filter_by(ip_address=ip_address).first() user_with_ip = User.query.filter_by(ip_address=ip_address).first()
if user_with_ip: if user_with_ip:
return return
def get_autocomplete_suggestions(query): def get_autocomplete_suggestions(query):
last_tag = query.split(',')[-1].strip() last_tag = query.split(',')[-1].strip()
all_tags = Image.query.with_entities(Image.tags).all() all_tags = Image.query.with_entities(Image.tags).all()
unique_tags = set(tag.strip() for tags in all_tags if tags.tags for tag in tags.tags.split(',')) unique_tags = set(tag.strip() for tags in all_tags if tags.tags for tag in tags.tags.split(','))
filtered_tags = [tag for tag in unique_tags if last_tag.lower() in tag.lower()] filtered_tags = [tag for tag in unique_tags if last_tag.lower() in tag.lower()]
return filtered_tags[:5] return filtered_tags[:5]