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/
/static/arts/
/static/comics/
/static/comicthumbs/
/static/items/
/static/posts/
/static/thumbnails/
/static/videos/
/venv/
/__pycache__/
static/css/*.css
static/css/*.css.map
/instance/
/static/arts/
/static/comics/
/static/comicthumbs/
/static/items/
/static/posts/
/static/thumbnails/
/static/videos/
/venv/
/__pycache__/
static/css/*.css
static/css/*.css.map
.env

120
README.md
View File

@ -1,61 +1,61 @@
[![](https://artberry.xyz/static/Logo.svg "test")](https://artberry.xyz/static/Logo.svg "test")
### Booru сайт нового поколения
**Открытый репозиторий веб-приложения [Artberry](https://artberry.xyz/ "Artberry")**
------------
### Используемые технологии
#### Backend:
- ##### ЯП: [Python](http://https://www.python.org/ "Python")
- ##### ФРЕЙМВОРК: [Flask](https://https://flask.palletsprojects.com/en/stable/ "Flask")
- ##### ШАБЛОНИЗАЦИЯ: [JINJA](https:/https://jinja.palletsprojects.com/en/stable// "JINJA")
#### Frontend:
- ##### HTML
- ##### CSS | [SASS](https://sass-lang.com/ "SASS")
- ##### JAVASCRIPT
------------
#### ЗАПУСК И ТЕСТИРОВАНИЕ
#### Для удобства и скорости разработки выполните следующие шаги:
#### Создание виртуального окружения:
- `python -m venv venv`
#### Запуск виртуального окружения:
*WINDOWS*:
- **powershell:** ` .\venv\Scripts\Activate`
- **CMD:** `venv\Scripts\activate.bat`
*LINUX* | *MAC*:
- `source venv/bin/activate`
#### Установка зависимостей:
- `pip install -r requirements.txt`
#### Запуск проекта:
- `python app.py` или `flask run`
#### Для отладки в конце файла `app.py` измените:
- `app.run(debug=False)` **на:** `app.run(debug=True)`
------------
#### КОМПИЛЯЦИЯ CSS ИЗ SASS
#### Для компиляции в реальном времени:
- `sass --watch static/css/styles.scss:static/css/styles.css`
*Эта команда будет отслеживать изменения в файле `styles.scss` и автоматически компилировать его в `styles.css`*
#### Для одноразовой компиляции:
- `sass static/css/styles.scss:static/css/styles.css`
*Эта команда выполнит одноразовую компиляцию из файла `styles.scss` в `styles.css`*
[![](https://artberry.xyz/static/Logo.svg "test")](https://artberry.xyz/static/Logo.svg "test")
### Booru сайт нового поколения
**Открытый репозиторий веб-приложения [Artberry](https://artberry.xyz/ "Artberry")**
------------
### Используемые технологии
#### Backend:
- ##### ЯП: [Python](http://https://www.python.org/ "Python")
- ##### ФРЕЙМВОРК: [Flask](https://https://flask.palletsprojects.com/en/stable/ "Flask")
- ##### ШАБЛОНИЗАЦИЯ: [JINJA](https:/https://jinja.palletsprojects.com/en/stable// "JINJA")
#### Frontend:
- ##### HTML
- ##### CSS | [SASS](https://sass-lang.com/ "SASS")
- ##### JAVASCRIPT
------------
#### ЗАПУСК И ТЕСТИРОВАНИЕ
#### Для удобства и скорости разработки выполните следующие шаги:
#### Создание виртуального окружения:
- `python -m venv venv`
#### Запуск виртуального окружения:
*WINDOWS*:
- **powershell:** ` .\venv\Scripts\Activate`
- **CMD:** `venv\Scripts\activate.bat`
*LINUX* | *MAC*:
- `source venv/bin/activate`
#### Установка зависимостей:
- `pip install -r requirements.txt`
#### Запуск проекта:
- `python app.py` или `flask run`
#### Для отладки в конце файла `app.py` измените:
- `app.run(debug=False)` **на:** `app.run(debug=True)`
------------
#### КОМПИЛЯЦИЯ CSS ИЗ SASS
#### Для компиляции в реальном времени:
- `sass --watch static/css/styles.scss:static/css/styles.css`
*Эта команда будет отслеживать изменения в файле `styles.scss` и автоматически компилировать его в `styles.css`*
#### Для одноразовой компиляции:
- `sass static/css/styles.scss:static/css/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_login import login_required, current_user
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_wtf import FlaskForm
from models import db, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicVotes, Cookies, UpdateCookiesForm
import os
import shutil
import uuid
import aiofiles
import bcrypt
from utils import update_related_tables
def register_admin_routes(app):
@app.route('/admin', methods=['GET', 'POST'])
@login_required
def admin():
if current_user.username != 'naturefie':
return redirect(url_for('index'))
form = UpdateCookiesForm()
user_cookies = {
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()
}
comments = Comments.query.order_by(Comments.comment_date.desc()).all()
return render_template(
'panel.html',
arts=Image.query.all(),
comics=Comic.query.all(),
videos=Video.query.all(),
users=User.query.all(),
comments=comments,
form=form,
user_cookies=user_cookies
)
@app.route('/admin/delete/<content_type>/<int:content_id>', methods=['POST'])
@login_required
def admin_delete_content(content_type, content_id):
models = {
'art': (Image, 'arts', 'image_file', Votes, 'image_id'),
'video': (Video, 'videos', 'video_file', VideoVotes, 'video_id'),
'comic': (Comic, 'comics', 'comic_folder', ComicVotes, 'comic_id')
}
if content_type not in models:
abort(404)
model, folder, file_field, vote_model, foreign_key = models[content_type]
content = model.query.get_or_404(content_id)
vote_model.query.filter(getattr(vote_model, 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))
if os.path.exists(file_path):
if os.path.isfile(file_path):
os.remove(file_path)
else:
shutil.rmtree(file_path)
db.session.delete(content)
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/delete/user/<int:user_id>', methods=['POST'])
@login_required
def admin_delete_user(user_id):
user = User.query.get_or_404(user_id)
if current_user.username != 'naturefie':
return redirect(url_for('admin'))
db.session.delete(user)
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_comment/<int:comment_id>', methods=['POST'])
@login_required
def admin_update_comment(comment_id):
comment = Comments.query.get_or_404(comment_id)
if current_user.username != 'naturefie':
abort(403)
new_text = request.form.get('comment_text', '').strip()
if not new_text:
return redirect(url_for('admin'))
comment.comment_text = new_text
try:
db.session.commit()
print(f"Updated comment ID {comment_id}: {comment.comment_text}")
except Exception as e:
db.session.rollback()
print(f"Error updating comment: {e}")
return redirect(url_for('admin'))
@app.route('/admin/delete_comment/<int:comment_id>', methods=['POST'])
@login_required
def admin_delete_comment(comment_id):
comment = Comments.query.get_or_404(comment_id)
if current_user.username != 'naturefie':
abort(403)
db.session.delete(comment)
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_cookies/<int:user_id>', methods=['POST'])
@login_required
def admin_update_cookies(user_id):
user = User.query.get_or_404(user_id)
if request.method == 'POST':
new_cookie_count = request.form.get('cookies', type=int)
if new_cookie_count is not None and new_cookie_count >= 0:
user_cookies = Cookies.query.filter_by(username=user.username).first()
if not user_cookies:
user_cookies = Cookies(username=user.username, cookies=new_cookie_count)
db.session.add(user_cookies)
else:
user_cookies.cookies = new_cookie_count
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_video/<int:content_id>', methods=['POST'])
@login_required
def admin_update_video(content_id):
video = Video.query.get_or_404(content_id)
if current_user.username != 'naturefie':
return redirect(url_for('admin'))
new_video_name = request.form.get('video_name')
new_description = request.form.get('description')
new_tags = request.form.get('tags')
if new_video_name and new_video_name != video.video_name:
if len(new_video_name) < 3 or len(new_video_name) > 100:
return redirect(url_for('admin'))
video.video_name = new_video_name
if new_description:
video.description = new_description
if new_tags:
video.tags = new_tags
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_user/<int:user_id>', methods=['POST'])
@login_required
def admin_update_user(user_id):
user = User.query.get_or_404(user_id)
if current_user.username != 'naturefie':
return redirect(url_for('admin'))
new_username = request.form.get('username')
new_password = request.form.get('password')
if new_username and new_username != user.username:
if len(new_username) < 3 or len(new_username) > 20:
return redirect(url_for('admin'))
if User.query.filter_by(username=new_username).first():
return redirect(url_for('admin'))
old_username = user.username
user.username = new_username
update_related_tables(old_username, new_username)
if new_password:
if len(new_password) < 6:
return redirect(url_for('admin'))
hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8')
user.encrypted_password = hashed_password
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_tags/<content_type>/<int:content_id>', methods=['POST'])
@login_required
def admin_update_tags(content_type, content_id):
models = {
'art': Image,
'video': Video,
'comic': Comic
}
if content_type not in models:
abort(404)
model = models[content_type]
content = model.query.get_or_404(content_id)
new_tags = request.form.get('tags', '').strip()
content.tags = new_tags
db.session.commit()
from flask import render_template, redirect, url_for, request, abort
from flask_login import login_required, current_user
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_wtf import FlaskForm
from models import db, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicVotes, Cookies, UpdateCookiesForm
import os
import shutil
import uuid
import aiofiles
import bcrypt
from utils import update_related_tables
def register_admin_routes(app):
@app.route('/admin', methods=['GET', 'POST'])
@login_required
def admin():
if current_user.username != 'naturefie':
return redirect(url_for('index'))
form = UpdateCookiesForm()
user_cookies = {
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()
}
comments = Comments.query.order_by(Comments.comment_date.desc()).all()
return render_template(
'panel.html',
arts=Image.query.all(),
comics=Comic.query.all(),
videos=Video.query.all(),
users=User.query.all(),
comments=comments,
form=form,
user_cookies=user_cookies
)
@app.route('/admin/delete/<content_type>/<int:content_id>', methods=['POST'])
@login_required
def admin_delete_content(content_type, content_id):
models = {
'art': (Image, 'arts', 'image_file', Votes, 'image_id'),
'video': (Video, 'videos', 'video_file', VideoVotes, 'video_id'),
'comic': (Comic, 'comics', 'comic_folder', ComicVotes, 'comic_id')
}
if content_type not in models:
abort(404)
model, folder, file_field, vote_model, foreign_key = models[content_type]
content = model.query.get_or_404(content_id)
vote_model.query.filter(getattr(vote_model, 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))
if os.path.exists(file_path):
if os.path.isfile(file_path):
os.remove(file_path)
else:
shutil.rmtree(file_path)
db.session.delete(content)
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/delete/user/<int:user_id>', methods=['POST'])
@login_required
def admin_delete_user(user_id):
user = User.query.get_or_404(user_id)
if current_user.username != 'naturefie':
return redirect(url_for('admin'))
db.session.delete(user)
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_comment/<int:comment_id>', methods=['POST'])
@login_required
def admin_update_comment(comment_id):
comment = Comments.query.get_or_404(comment_id)
if current_user.username != 'naturefie':
abort(403)
new_text = request.form.get('comment_text', '').strip()
if not new_text:
return redirect(url_for('admin'))
comment.comment_text = new_text
try:
db.session.commit()
print(f"Updated comment ID {comment_id}: {comment.comment_text}")
except Exception as e:
db.session.rollback()
print(f"Error updating comment: {e}")
return redirect(url_for('admin'))
@app.route('/admin/delete_comment/<int:comment_id>', methods=['POST'])
@login_required
def admin_delete_comment(comment_id):
comment = Comments.query.get_or_404(comment_id)
if current_user.username != 'naturefie':
abort(403)
db.session.delete(comment)
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_cookies/<int:user_id>', methods=['POST'])
@login_required
def admin_update_cookies(user_id):
user = User.query.get_or_404(user_id)
if request.method == 'POST':
new_cookie_count = request.form.get('cookies', type=int)
if new_cookie_count is not None and new_cookie_count >= 0:
user_cookies = Cookies.query.filter_by(username=user.username).first()
if not user_cookies:
user_cookies = Cookies(username=user.username, cookies=new_cookie_count)
db.session.add(user_cookies)
else:
user_cookies.cookies = new_cookie_count
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_video/<int:content_id>', methods=['POST'])
@login_required
def admin_update_video(content_id):
video = Video.query.get_or_404(content_id)
if current_user.username != 'naturefie':
return redirect(url_for('admin'))
new_video_name = request.form.get('video_name')
new_description = request.form.get('description')
new_tags = request.form.get('tags')
if new_video_name and new_video_name != video.video_name:
if len(new_video_name) < 3 or len(new_video_name) > 100:
return redirect(url_for('admin'))
video.video_name = new_video_name
if new_description:
video.description = new_description
if new_tags:
video.tags = new_tags
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_user/<int:user_id>', methods=['POST'])
@login_required
def admin_update_user(user_id):
user = User.query.get_or_404(user_id)
if current_user.username != 'naturefie':
return redirect(url_for('admin'))
new_username = request.form.get('username')
new_password = request.form.get('password')
if new_username and new_username != user.username:
if len(new_username) < 3 or len(new_username) > 20:
return redirect(url_for('admin'))
if User.query.filter_by(username=new_username).first():
return redirect(url_for('admin'))
old_username = user.username
user.username = new_username
update_related_tables(old_username, new_username)
if new_password:
if len(new_password) < 6:
return redirect(url_for('admin'))
hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8')
user.encrypted_password = hashed_password
db.session.commit()
return redirect(url_for('admin'))
@app.route('/admin/update_tags/<content_type>/<int:content_id>', methods=['POST'])
@login_required
def admin_update_tags(content_type, content_id):
models = {
'art': Image,
'video': Video,
'comic': Comic
}
if content_type not in models:
abort(404)
model = models[content_type]
content = model.query.get_or_404(content_id)
new_tags = request.form.get('tags', '').strip()
content.tags = new_tags
db.session.commit()
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_login import login_user, logout_user, login_required, current_user
from sqlalchemy.exc import IntegrityError
from models import db, User
from utils import get_client_ip
from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField
from flask_bcrypt import Bcrypt
from wtforms.validators import DataRequired, Length, EqualTo
auth_bp = Blueprint('auth', __name__)
bcrypt = Bcrypt()
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
recaptcha = RecaptchaField()
submit = SubmitField('Register')
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
ip_address = get_client_ip()
existing_user = User.query.filter_by(ip_address=ip_address).first()
if existing_user:
return render_template('register.html', form=form)
username = form.username.data.lower()
user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address)
try:
db.session.add(user)
db.session.commit()
return redirect(url_for('auth.login'))
except IntegrityError:
db.session.rollback()
return render_template('register.html', form=form)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data):
login_user(user)
if user.ip_address is None:
ip_address = get_client_ip()
user.ip_address = ip_address
db.session.commit()
return redirect(url_for('profile', username=user.username))
return render_template('login.html', form=form)
@auth_bp.route('/logout')
def logout():
logout_user()
from flask import Blueprint, render_template, redirect, url_for, request
from flask_login import login_user, logout_user, login_required, current_user
from sqlalchemy.exc import IntegrityError
from models import db, User
from utils import get_client_ip
from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField
from flask_bcrypt import Bcrypt
from wtforms.validators import DataRequired, Length, EqualTo
from config import Config
auth_bp = Blueprint('auth', __name__)
bcrypt = Bcrypt()
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
recaptcha = RecaptchaField()
submit = SubmitField('Register')
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
ip_address = get_client_ip()
username = form.username.data.lower()
existing_user = User.query.filter_by(ip_address=ip_address).first()
user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address)
try:
db.session.add(user)
db.session.commit()
return redirect(url_for('auth.login'))
except IntegrityError:
db.session.rollback()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return render_template('register-modal.html', form=form)
return render_template('register.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data):
login_user(user)
if user.ip_address is None:
ip_address = get_client_ip()
user.ip_address = ip_address
db.session.commit()
return redirect(url_for('profile', username=user.username))
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return render_template('login-modal.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
return render_template('login.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
@auth_bp.route('/register-modal')
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'))

View File

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

16
license
View File

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

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 465 B

View File

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

View File

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

View File

@ -1,17 +1,92 @@
document.addEventListener('DOMContentLoaded', function () {
const iconContainer = document.querySelector('.icon-container');
const dropdownMenu = document.querySelector('.dropdown-menu');
iconContainer.addEventListener('click', function (event) {
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
event.stopPropagation();
});
document.addEventListener('click', function () {
dropdownMenu.style.display = 'none';
});
dropdownMenu.addEventListener('click', function (event) {
event.stopPropagation();
});
});
document.addEventListener('DOMContentLoaded', function () {
const iconContainer = document.querySelector('.icon-container');
const dropdownMenu = document.querySelector('.dropdown-menu');
iconContainer.addEventListener('click', function (event) {
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
event.stopPropagation();
});
document.addEventListener('click', function () {
dropdownMenu.style.display = 'none';
});
dropdownMenu.addEventListener('click', function (event) {
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() {
const listButton = document.querySelector('.list-button');
const tags = document.querySelectorAll('.tag');
const buttonRect = listButton.getBoundingClientRect();
function checkTagVisibility() {
tags.forEach(tag => {
const tagRect = tag.getBoundingClientRect();
if (tagRect.right > buttonRect.left) {
tag.style.visibility = 'hidden';
} else {
tag.style.visibility = 'visible';
}
});
}
document.querySelector('.tags-container').addEventListener('scroll', checkTagVisibility);
checkTagVisibility();
window.addEventListener('DOMContentLoaded', function() {
const listButton = document.querySelector('.list-button');
const tags = document.querySelectorAll('.tag');
const buttonRect = listButton.getBoundingClientRect();
function checkTagVisibility() {
tags.forEach(tag => {
const tagRect = tag.getBoundingClientRect();
if (tagRect.right > buttonRect.left) {
tag.style.visibility = 'hidden';
} else {
tag.style.visibility = 'visible';
}
});
}
document.querySelector('.tags-container').addEventListener('scroll', checkTagVisibility);
checkTagVisibility();
});

View File

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

View File

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

View File

@ -1,14 +1,14 @@
{% extends "content.html" %}
{% include 'subnav.html' %}
{% block title %}🫐videos - artberry🫐{% endblock %}
{% block new_content %}
<div class="container new-content">
<a class="new-content-text">НОВИНКИ (Updated for Videos)</a>
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
</div>
{% endblock %}
{% block content %}
{% include 'tags_list.html' %}
{% extends "content.html" %}
{% include 'subnav.html' %}
{% block title %}🫐videos - artberry🫐{% endblock %}
{% block new_content %}
<div class="container new-content">
<a class="new-content-text">НОВИНКИ (Updated for Videos)</a>
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
</div>
{% endblock %}
{% block content %}
{% include 'tags_list.html' %}
{% 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" %}
{% block title %}🫐login - artberry🫐{% endblock %}
{% set action = 'auth.login' %}
<!DOCTYPE html>
<html lang="ru">
<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 %}
<form method="POST" action="{{ url_for(action) }}">
{{ form.hidden_tag() }}
{% for field, errors in form.errors.items() %}
<ul>
{% for error in errors %}
<li><strong>{{ field.label }}:</strong> {{ error }}</li>
{% endfor %}
</ul>
{% endfor %}
<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>
<label for="{{ form.username.id }}">{{ form.username.label.text }}</label>
{{ form.username(class="input-field", placeholder="Enter username") }}<br>
<label for="{{ form.password.id }}">{{ form.password.label.text }}</label>
{{ form.password(class="input-field", placeholder="Enter password") }}<br>
<div class="recaptcha-container">
{{ form.recaptcha.label }}
{{ form.recaptcha() }}<br>
<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>
{{ form.submit(class="login-button button", value="Login") }}
</form>
<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 %}
</body>
</html>

View File

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

View File

@ -1,13 +1,13 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<div class="pagination-container">
<button class="swipe-button left-swipe-button"><img src="{{ url_for('static', filename='/icons/left-arrow.svg') }}"></button>
<div class="page-buttons-container">
<button class="page-button">1</button>
<button class="page-button">2</button>
<button class="page-button">3</button>
<button class="page-button">4</button>
<button class="page-button">5</button>
<button class="page-button">10</button>
</div>
<button class="swipe-button right-swipe-button"><img src="{{ url_for('static', filename='/icons/right-arrow.svg') }}"></button>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<div class="pagination-container">
<button class="swipe-button left-swipe-button"><img src="{{ url_for('static', filename='/icons/left-arrow.svg') }}"></button>
<div class="page-buttons-container">
<button class="page-button">1</button>
<button class="page-button">2</button>
<button class="page-button">3</button>
<button class="page-button">4</button>
<button class="page-button">5</button>
<button class="page-button">10</button>
</div>
<button class="swipe-button right-swipe-button"><img src="{{ url_for('static', filename='/icons/right-arrow.svg') }}"></button>
</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" %}
{% block title %}🫐register - artberry🫐{% endblock %}
{% set action = 'auth.register' %}
{% block content %}
<form method="POST" action="{{ url_for(action) }}">
{{ form.hidden_tag() }}
<label for="{{ form.username.id }}">{{ form.username.label.text }}</label>
{{ form.username(class="input-field", placeholder="Enter username") }}<br>
<label for="{{ form.password.id }}">{{ form.password.label.text }}</label>
{{ form.password(class="input-field", placeholder="Enter password") }}<br>
<label for="{{ form.confirm_password.id }}">{{ form.confirm_password.label.text }}</label>
{{ form.confirm_password(class="input-field", placeholder="Repeat password") }}<br>
<div class="recaptcha-container">
{{ form.recaptcha.label }}
{{ form.recaptcha() }}<br>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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="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>
{{ form.submit(class="login-button button", value="Register") }}
</form>
<div class="link-container">
<span class="link-text">Already have an account?</span>
<a href="{{ url_for('auth.login') }}" class="button">Login</a>
</div>
{% endblock %}
</body>
<script>
function openLoginModal() {
document.getElementById('registerModal').classList.remove('active');
document.getElementById('loginModal').classList.add('active');
}
</script>
</html>

View File

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

View File

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

View File

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

View File

@ -1,132 +1,132 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="UTF-8">
<title>🫐Content View - Artberry🫐</title>
<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="icon" href="{{ url_for('static', filename='artberry.ico') }}" type="image/x-icon">
</head>
<body>
{% include 'navbar.html' %}
{% if content_type == 'art' %}
<h1>Image</h1>
<div class="details">
<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>Publication Date:</strong> {{ content.publication_date }}</p>
<p><strong>Tags:</strong>
{% 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 %}
{% endfor %}
</p>
</div>
{% elif content_type == 'video' %}
<h1>Video</h1>
<div class="video-details">
<video controls>
<source src="{{ url_for('static', filename='videos/' + content.video_file) }}" type="video/mp4">
</video>
<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>Description: {{ content.description }}</p>
<p><strong>Tags:</strong>
{% 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 %}
{% endfor %}
</p>
</div>
{% elif content_type == 'comic' %}
<h1>{{ content.name }}</h1>
<div class="comic-pages">
{% if comic_pages %}
{% for page in comic_pages %}
<img src="{{ url_for('static', filename=page.file_path.replace('static/', '').replace('\\', '/')) }}" alt="Page {{ page.page_number }}">
{% endfor %}
{% else %}
<p>No pages available for this comic.</p>
{% endif %}
</div>
{% endif %}
{% 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) }}">
<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>
</form>
{% endif %}
{% if content_type != 'comic' %}
<section class="comments">
<h2>Comments</h2>
<div class="comments-list">
{% for comment in comments %}
<div class="comment">
<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')) }}"
alt="Avatar of {{ comment.username }}" class="avatar">
{% 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;">
<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>
</form>
{% endif %}
</a>
<div class="content">
<p>
<a href="{{ url_for('profile', username=comment.username) }}" class="username-link">
<strong>{{ comment.username }}</strong>
</a>
({{ comment.comment_date.strftime('%Y-%m-%d %H:%M') }}):
</p>
<p style="margin-top: 10px;">{{ comment.comment_text }}</p>
</div>
</div>
{% else %}
<p>No comments yet. Be the first to comment!</p>
{% endfor %}
</div>
{% if current_user.is_authenticated %}
<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() }}">
<textarea name="comment" class="input-field" placeholder="Add a comment..." rows="3" maxlength="44" required></textarea>
<button type="submit" class="button">Post Comment</button>
</form>
{% else %}
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to post a comment.</p>
{% endif %}
</section>
{% endif %}
{% if content_type != 'comic' %}
<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=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>
{% if current_user.is_authenticated and content.username == current_user.username %}
{% if content_type == 'art' %}
<a href="{{ url_for('image_edit', id=content.id) }}" class="button">Edit Art</a>
{% elif content_type == 'video' %}
<a href="{{ url_for('video_edit', id=content.id) }}" class="button">Edit Video</a>
{% elif content_type == 'comic' %}
<a href="{{ url_for('comic_edit', id=content.id) }}" class="button">Edit Comic</a>
{% endif %}
{% endif %}
</div>
{% endif %}
<div class="vote-section">
<p>Votes: {{ content.cookie_votes }} 🍪</p>
{% 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">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="button">Vote</button>
</form>
{% else %}
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to vote.</p>
{% endif %}
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="UTF-8">
<title>🫐Content View - Artberry🫐</title>
<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="icon" href="{{ url_for('static', filename='artberry.ico') }}" type="image/x-icon">
</head>
<body>
{% include 'navbar.html' %}
{% if content_type == 'art' %}
<h1>Image</h1>
<div class="details">
<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>Publication Date:</strong> {{ content.publication_date }}</p>
<p><strong>Tags:</strong>
{% 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 %}
{% endfor %}
</p>
</div>
{% elif content_type == 'video' %}
<h1>Video</h1>
<div class="video-details">
<video controls>
<source src="{{ url_for('static', filename='videos/' + content.video_file) }}" type="video/mp4">
</video>
<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>Description: {{ content.description }}</p>
<p><strong>Tags:</strong>
{% 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 %}
{% endfor %}
</p>
</div>
{% elif content_type == 'comic' %}
<h1>{{ content.name }}</h1>
<div class="comic-pages">
{% if comic_pages %}
{% for page in comic_pages %}
<img src="{{ url_for('static', filename=page.file_path.replace('static/', '').replace('\\', '/')) }}" alt="Page {{ page.page_number }}">
{% endfor %}
{% else %}
<p>No pages available for this comic.</p>
{% endif %}
</div>
{% endif %}
{% 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) }}">
<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>
</form>
{% endif %}
{% if content_type != 'comic' %}
<section class="comments">
<h2>Comments</h2>
<div class="comments-list">
{% for comment in comments %}
<div class="comment">
<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')) }}"
alt="Avatar of {{ comment.username }}" class="avatar">
{% 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;">
<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>
</form>
{% endif %}
</a>
<div class="content">
<p>
<a href="{{ url_for('profile', username=comment.username) }}" class="username-link">
<strong>{{ comment.username }}</strong>
</a>
({{ comment.comment_date.strftime('%Y-%m-%d %H:%M') }}):
</p>
<p style="margin-top: 10px;">{{ comment.comment_text }}</p>
</div>
</div>
{% else %}
<p>No comments yet. Be the first to comment!</p>
{% endfor %}
</div>
{% if current_user.is_authenticated %}
<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() }}">
<textarea name="comment" class="input-field" placeholder="Add a comment..." rows="3" maxlength="44" required></textarea>
<button type="submit" class="button">Post Comment</button>
</form>
{% else %}
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to post a comment.</p>
{% endif %}
</section>
{% endif %}
{% if content_type != 'comic' %}
<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=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>
{% if current_user.is_authenticated and content.username == current_user.username %}
{% if content_type == 'art' %}
<a href="{{ url_for('image_edit', id=content.id) }}" class="button">Edit Art</a>
{% elif content_type == 'video' %}
<a href="{{ url_for('video_edit', id=content.id) }}" class="button">Edit Video</a>
{% elif content_type == 'comic' %}
<a href="{{ url_for('comic_edit', id=content.id) }}" class="button">Edit Comic</a>
{% endif %}
{% endif %}
</div>
{% endif %}
<div class="vote-section">
<p>Votes: {{ content.cookie_votes }} 🍪</p>
{% 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">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="button">Vote</button>
</form>
{% else %}
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to vote.</p>
{% endif %}
</div>
</body>
</html>

396
upload.py
View File

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

266
utils.py
View File

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