auth modals
This commit is contained in:
parent
bf5aeb677f
commit
c9e8e5386f
24
.gitignore
vendored
24
.gitignore
vendored
@ -1,13 +1,13 @@
|
|||||||
/instance/
|
/instance/
|
||||||
/static/arts/
|
/static/arts/
|
||||||
/static/comics/
|
/static/comics/
|
||||||
/static/comicthumbs/
|
/static/comicthumbs/
|
||||||
/static/items/
|
/static/items/
|
||||||
/static/posts/
|
/static/posts/
|
||||||
/static/thumbnails/
|
/static/thumbnails/
|
||||||
/static/videos/
|
/static/videos/
|
||||||
/venv/
|
/venv/
|
||||||
/__pycache__/
|
/__pycache__/
|
||||||
static/css/*.css
|
static/css/*.css
|
||||||
static/css/*.css.map
|
static/css/*.css.map
|
||||||
.env
|
.env
|
120
README.md
120
README.md
@ -1,61 +1,61 @@
|
|||||||
[](https://artberry.xyz/static/Logo.svg "test")
|
[](https://artberry.xyz/static/Logo.svg "test")
|
||||||
### Booru сайт нового поколения
|
### Booru сайт нового поколения
|
||||||
|
|
||||||
**Открытый репозиторий веб-приложения [Artberry](https://artberry.xyz/ "Artberry")**
|
**Открытый репозиторий веб-приложения [Artberry](https://artberry.xyz/ "Artberry")**
|
||||||
|
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|
||||||
### Используемые технологии
|
### Используемые технологии
|
||||||
#### Backend:
|
#### Backend:
|
||||||
- ##### ЯП: [Python](http://https://www.python.org/ "Python")
|
- ##### ЯП: [Python](http://https://www.python.org/ "Python")
|
||||||
- ##### ФРЕЙМВОРК: [Flask](https://https://flask.palletsprojects.com/en/stable/ "Flask")
|
- ##### ФРЕЙМВОРК: [Flask](https://https://flask.palletsprojects.com/en/stable/ "Flask")
|
||||||
- ##### ШАБЛОНИЗАЦИЯ: [JINJA](https:/https://jinja.palletsprojects.com/en/stable// "JINJA")
|
- ##### ШАБЛОНИЗАЦИЯ: [JINJA](https:/https://jinja.palletsprojects.com/en/stable// "JINJA")
|
||||||
|
|
||||||
#### Frontend:
|
#### Frontend:
|
||||||
- ##### HTML
|
- ##### HTML
|
||||||
- ##### CSS | [SASS](https://sass-lang.com/ "SASS")
|
- ##### CSS | [SASS](https://sass-lang.com/ "SASS")
|
||||||
- ##### JAVASCRIPT
|
- ##### JAVASCRIPT
|
||||||
|
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|
||||||
#### ЗАПУСК И ТЕСТИРОВАНИЕ
|
#### ЗАПУСК И ТЕСТИРОВАНИЕ
|
||||||
#### Для удобства и скорости разработки выполните следующие шаги:
|
#### Для удобства и скорости разработки выполните следующие шаги:
|
||||||
|
|
||||||
#### Создание виртуального окружения:
|
#### Создание виртуального окружения:
|
||||||
- `python -m venv venv`
|
- `python -m venv venv`
|
||||||
|
|
||||||
#### Запуск виртуального окружения:
|
#### Запуск виртуального окружения:
|
||||||
*WINDOWS*:
|
*WINDOWS*:
|
||||||
- **powershell:** ` .\venv\Scripts\Activate`
|
- **powershell:** ` .\venv\Scripts\Activate`
|
||||||
- **CMD:** `venv\Scripts\activate.bat`
|
- **CMD:** `venv\Scripts\activate.bat`
|
||||||
|
|
||||||
*LINUX* | *MAC*:
|
*LINUX* | *MAC*:
|
||||||
- `source venv/bin/activate`
|
- `source venv/bin/activate`
|
||||||
|
|
||||||
#### Установка зависимостей:
|
#### Установка зависимостей:
|
||||||
- `pip install -r requirements.txt`
|
- `pip install -r requirements.txt`
|
||||||
|
|
||||||
#### Запуск проекта:
|
#### Запуск проекта:
|
||||||
- `python app.py` или `flask run`
|
- `python app.py` или `flask run`
|
||||||
|
|
||||||
#### Для отладки в конце файла `app.py` измените:
|
#### Для отладки в конце файла `app.py` измените:
|
||||||
|
|
||||||
- `app.run(debug=False)` **на:** `app.run(debug=True)`
|
- `app.run(debug=False)` **на:** `app.run(debug=True)`
|
||||||
|
|
||||||
------------
|
------------
|
||||||
|
|
||||||
#### КОМПИЛЯЦИЯ CSS ИЗ SASS
|
#### КОМПИЛЯЦИЯ CSS ИЗ SASS
|
||||||
|
|
||||||
#### Для компиляции в реальном времени:
|
#### Для компиляции в реальном времени:
|
||||||
- `sass --watch static/css/styles.scss:static/css/styles.css`
|
- `sass --watch static/css/styles.scss:static/css/styles.css`
|
||||||
|
|
||||||
*Эта команда будет отслеживать изменения в файле `styles.scss` и автоматически компилировать его в `styles.css`*
|
*Эта команда будет отслеживать изменения в файле `styles.scss` и автоматически компилировать его в `styles.css`*
|
||||||
|
|
||||||
#### Для одноразовой компиляции:
|
#### Для одноразовой компиляции:
|
||||||
- `sass static/css/styles.scss:static/css/styles.css`
|
- `sass static/css/styles.scss:static/css/styles.css`
|
||||||
|
|
||||||
*Эта команда выполнит одноразовую компиляцию из файла `styles.scss` в `styles.css`*
|
*Эта команда выполнит одноразовую компиляцию из файла `styles.scss` в `styles.css`*
|
||||||
|
|
||||||
------------
|
------------
|
412
admin.py
412
admin.py
@ -1,207 +1,207 @@
|
|||||||
from flask import render_template, redirect, url_for, request, abort
|
from flask import render_template, redirect, url_for, request, abort
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from wtforms import StringField, SubmitField
|
from wtforms import StringField, SubmitField
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from models import db, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicVotes, Cookies, UpdateCookiesForm
|
from models import db, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicVotes, Cookies, UpdateCookiesForm
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from utils import update_related_tables
|
from utils import update_related_tables
|
||||||
|
|
||||||
def register_admin_routes(app):
|
def register_admin_routes(app):
|
||||||
@app.route('/admin', methods=['GET', 'POST'])
|
@app.route('/admin', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin():
|
def admin():
|
||||||
if current_user.username != 'naturefie':
|
if current_user.username != 'naturefie':
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
form = UpdateCookiesForm()
|
form = UpdateCookiesForm()
|
||||||
|
|
||||||
user_cookies = {
|
user_cookies = {
|
||||||
user.id: Cookies.query.filter_by(username=user.username).first().cookies if Cookies.query.filter_by(username=user.username).first() else 0
|
user.id: Cookies.query.filter_by(username=user.username).first().cookies if Cookies.query.filter_by(username=user.username).first() else 0
|
||||||
for user in User.query.all()
|
for user in User.query.all()
|
||||||
}
|
}
|
||||||
|
|
||||||
comments = Comments.query.order_by(Comments.comment_date.desc()).all()
|
comments = Comments.query.order_by(Comments.comment_date.desc()).all()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'panel.html',
|
'panel.html',
|
||||||
arts=Image.query.all(),
|
arts=Image.query.all(),
|
||||||
comics=Comic.query.all(),
|
comics=Comic.query.all(),
|
||||||
videos=Video.query.all(),
|
videos=Video.query.all(),
|
||||||
users=User.query.all(),
|
users=User.query.all(),
|
||||||
comments=comments,
|
comments=comments,
|
||||||
form=form,
|
form=form,
|
||||||
user_cookies=user_cookies
|
user_cookies=user_cookies
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route('/admin/delete/<content_type>/<int:content_id>', methods=['POST'])
|
@app.route('/admin/delete/<content_type>/<int:content_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_delete_content(content_type, content_id):
|
def admin_delete_content(content_type, content_id):
|
||||||
models = {
|
models = {
|
||||||
'art': (Image, 'arts', 'image_file', Votes, 'image_id'),
|
'art': (Image, 'arts', 'image_file', Votes, 'image_id'),
|
||||||
'video': (Video, 'videos', 'video_file', VideoVotes, 'video_id'),
|
'video': (Video, 'videos', 'video_file', VideoVotes, 'video_id'),
|
||||||
'comic': (Comic, 'comics', 'comic_folder', ComicVotes, 'comic_id')
|
'comic': (Comic, 'comics', 'comic_folder', ComicVotes, 'comic_id')
|
||||||
}
|
}
|
||||||
|
|
||||||
if content_type not in models:
|
if content_type not in models:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
model, folder, file_field, vote_model, foreign_key = models[content_type]
|
model, folder, file_field, vote_model, foreign_key = models[content_type]
|
||||||
|
|
||||||
content = model.query.get_or_404(content_id)
|
content = model.query.get_or_404(content_id)
|
||||||
|
|
||||||
vote_model.query.filter(getattr(vote_model, foreign_key) == content_id).delete()
|
vote_model.query.filter(getattr(vote_model, foreign_key) == content_id).delete()
|
||||||
|
|
||||||
Comments.query.filter(getattr(Comments, foreign_key) == content_id).delete()
|
Comments.query.filter(getattr(Comments, foreign_key) == content_id).delete()
|
||||||
|
|
||||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'][folder], getattr(content, file_field))
|
file_path = os.path.join(app.config['UPLOAD_FOLDER'][folder], getattr(content, file_field))
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
if os.path.isfile(file_path):
|
if os.path.isfile(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
else:
|
else:
|
||||||
shutil.rmtree(file_path)
|
shutil.rmtree(file_path)
|
||||||
|
|
||||||
db.session.delete(content)
|
db.session.delete(content)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
@app.route('/admin/delete/user/<int:user_id>', methods=['POST'])
|
@app.route('/admin/delete/user/<int:user_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_delete_user(user_id):
|
def admin_delete_user(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if current_user.username != 'naturefie':
|
if current_user.username != 'naturefie':
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
db.session.delete(user)
|
db.session.delete(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
@app.route('/admin/update_comment/<int:comment_id>', methods=['POST'])
|
@app.route('/admin/update_comment/<int:comment_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_update_comment(comment_id):
|
def admin_update_comment(comment_id):
|
||||||
comment = Comments.query.get_or_404(comment_id)
|
comment = Comments.query.get_or_404(comment_id)
|
||||||
if current_user.username != 'naturefie':
|
if current_user.username != 'naturefie':
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
new_text = request.form.get('comment_text', '').strip()
|
new_text = request.form.get('comment_text', '').strip()
|
||||||
if not new_text:
|
if not new_text:
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
comment.comment_text = new_text
|
comment.comment_text = new_text
|
||||||
try:
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print(f"Updated comment ID {comment_id}: {comment.comment_text}")
|
print(f"Updated comment ID {comment_id}: {comment.comment_text}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
print(f"Error updating comment: {e}")
|
print(f"Error updating comment: {e}")
|
||||||
|
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
@app.route('/admin/delete_comment/<int:comment_id>', methods=['POST'])
|
@app.route('/admin/delete_comment/<int:comment_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_delete_comment(comment_id):
|
def admin_delete_comment(comment_id):
|
||||||
comment = Comments.query.get_or_404(comment_id)
|
comment = Comments.query.get_or_404(comment_id)
|
||||||
if current_user.username != 'naturefie':
|
if current_user.username != 'naturefie':
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
db.session.delete(comment)
|
db.session.delete(comment)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
@app.route('/admin/update_cookies/<int:user_id>', methods=['POST'])
|
@app.route('/admin/update_cookies/<int:user_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_update_cookies(user_id):
|
def admin_update_cookies(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
new_cookie_count = request.form.get('cookies', type=int)
|
new_cookie_count = request.form.get('cookies', type=int)
|
||||||
if new_cookie_count is not None and new_cookie_count >= 0:
|
if new_cookie_count is not None and new_cookie_count >= 0:
|
||||||
user_cookies = Cookies.query.filter_by(username=user.username).first()
|
user_cookies = Cookies.query.filter_by(username=user.username).first()
|
||||||
if not user_cookies:
|
if not user_cookies:
|
||||||
user_cookies = Cookies(username=user.username, cookies=new_cookie_count)
|
user_cookies = Cookies(username=user.username, cookies=new_cookie_count)
|
||||||
db.session.add(user_cookies)
|
db.session.add(user_cookies)
|
||||||
else:
|
else:
|
||||||
user_cookies.cookies = new_cookie_count
|
user_cookies.cookies = new_cookie_count
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
@app.route('/admin/update_video/<int:content_id>', methods=['POST'])
|
@app.route('/admin/update_video/<int:content_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_update_video(content_id):
|
def admin_update_video(content_id):
|
||||||
video = Video.query.get_or_404(content_id)
|
video = Video.query.get_or_404(content_id)
|
||||||
if current_user.username != 'naturefie':
|
if current_user.username != 'naturefie':
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
new_video_name = request.form.get('video_name')
|
new_video_name = request.form.get('video_name')
|
||||||
new_description = request.form.get('description')
|
new_description = request.form.get('description')
|
||||||
new_tags = request.form.get('tags')
|
new_tags = request.form.get('tags')
|
||||||
|
|
||||||
if new_video_name and new_video_name != video.video_name:
|
if new_video_name and new_video_name != video.video_name:
|
||||||
if len(new_video_name) < 3 or len(new_video_name) > 100:
|
if len(new_video_name) < 3 or len(new_video_name) > 100:
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
video.video_name = new_video_name
|
video.video_name = new_video_name
|
||||||
|
|
||||||
if new_description:
|
if new_description:
|
||||||
video.description = new_description
|
video.description = new_description
|
||||||
|
|
||||||
if new_tags:
|
if new_tags:
|
||||||
video.tags = new_tags
|
video.tags = new_tags
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
@app.route('/admin/update_user/<int:user_id>', methods=['POST'])
|
@app.route('/admin/update_user/<int:user_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_update_user(user_id):
|
def admin_update_user(user_id):
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if current_user.username != 'naturefie':
|
if current_user.username != 'naturefie':
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
new_username = request.form.get('username')
|
new_username = request.form.get('username')
|
||||||
new_password = request.form.get('password')
|
new_password = request.form.get('password')
|
||||||
|
|
||||||
if new_username and new_username != user.username:
|
if new_username and new_username != user.username:
|
||||||
if len(new_username) < 3 or len(new_username) > 20:
|
if len(new_username) < 3 or len(new_username) > 20:
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
if User.query.filter_by(username=new_username).first():
|
if User.query.filter_by(username=new_username).first():
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
old_username = user.username
|
old_username = user.username
|
||||||
user.username = new_username
|
user.username = new_username
|
||||||
update_related_tables(old_username, new_username)
|
update_related_tables(old_username, new_username)
|
||||||
|
|
||||||
if new_password:
|
if new_password:
|
||||||
if len(new_password) < 6:
|
if len(new_password) < 6:
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8')
|
hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8')
|
||||||
user.encrypted_password = hashed_password
|
user.encrypted_password = hashed_password
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
||||||
|
|
||||||
@app.route('/admin/update_tags/<content_type>/<int:content_id>', methods=['POST'])
|
@app.route('/admin/update_tags/<content_type>/<int:content_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_update_tags(content_type, content_id):
|
def admin_update_tags(content_type, content_id):
|
||||||
models = {
|
models = {
|
||||||
'art': Image,
|
'art': Image,
|
||||||
'video': Video,
|
'video': Video,
|
||||||
'comic': Comic
|
'comic': Comic
|
||||||
}
|
}
|
||||||
|
|
||||||
if content_type not in models:
|
if content_type not in models:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
model = models[content_type]
|
model = models[content_type]
|
||||||
content = model.query.get_or_404(content_id)
|
content = model.query.get_or_404(content_id)
|
||||||
|
|
||||||
new_tags = request.form.get('tags', '').strip()
|
new_tags = request.form.get('tags', '').strip()
|
||||||
|
|
||||||
content.tags = new_tags
|
content.tags = new_tags
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(url_for('admin'))
|
return redirect(url_for('admin'))
|
142
auth.py
142
auth.py
@ -1,66 +1,78 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, request
|
from flask import Blueprint, render_template, redirect, url_for, request
|
||||||
from flask_login import login_user, logout_user, login_required, current_user
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from models import db, User
|
from models import db, User
|
||||||
from utils import get_client_ip
|
from utils import get_client_ip
|
||||||
from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField
|
from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField
|
||||||
from flask_bcrypt import Bcrypt
|
from flask_bcrypt import Bcrypt
|
||||||
from wtforms.validators import DataRequired, Length, EqualTo
|
from wtforms.validators import DataRequired, Length, EqualTo
|
||||||
|
from config import Config
|
||||||
auth_bp = Blueprint('auth', __name__)
|
auth_bp = Blueprint('auth', __name__)
|
||||||
bcrypt = Bcrypt()
|
bcrypt = Bcrypt()
|
||||||
|
|
||||||
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
|
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
|
||||||
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
|
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
|
||||||
recaptcha = RecaptchaField()
|
recaptcha = RecaptchaField()
|
||||||
submit = SubmitField('Register')
|
submit = SubmitField('Register')
|
||||||
|
|
||||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||||
def register():
|
def register():
|
||||||
form = RegistrationForm()
|
form = RegistrationForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
|
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
|
||||||
|
ip_address = get_client_ip()
|
||||||
ip_address = get_client_ip()
|
username = form.username.data.lower()
|
||||||
|
|
||||||
existing_user = User.query.filter_by(ip_address=ip_address).first()
|
existing_user = User.query.filter_by(ip_address=ip_address).first()
|
||||||
if existing_user:
|
user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address)
|
||||||
return render_template('register.html', form=form)
|
|
||||||
|
try:
|
||||||
username = form.username.data.lower()
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address)
|
return redirect(url_for('auth.login'))
|
||||||
|
except IntegrityError:
|
||||||
try:
|
db.session.rollback()
|
||||||
db.session.add(user)
|
|
||||||
db.session.commit()
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return redirect(url_for('auth.login'))
|
return render_template('register-modal.html', form=form)
|
||||||
except IntegrityError:
|
|
||||||
db.session.rollback()
|
return render_template('register.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
|
||||||
|
|
||||||
return render_template('register.html', form=form)
|
|
||||||
|
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
def login():
|
||||||
def login():
|
form = LoginForm()
|
||||||
form = LoginForm()
|
|
||||||
|
if form.validate_on_submit():
|
||||||
if form.validate_on_submit():
|
user = User.query.filter_by(username=form.username.data).first()
|
||||||
user = User.query.filter_by(username=form.username.data).first()
|
if user and user.check_password(form.password.data):
|
||||||
|
login_user(user)
|
||||||
if user and user.check_password(form.password.data):
|
if user.ip_address is None:
|
||||||
login_user(user)
|
ip_address = get_client_ip()
|
||||||
|
user.ip_address = ip_address
|
||||||
if user.ip_address is None:
|
db.session.commit()
|
||||||
ip_address = get_client_ip()
|
return redirect(url_for('profile', username=user.username))
|
||||||
user.ip_address = ip_address
|
|
||||||
db.session.commit()
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
|
return render_template('login-modal.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
|
||||||
return redirect(url_for('profile', username=user.username))
|
|
||||||
|
return render_template('login.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
|
||||||
return render_template('login.html', form=form)
|
|
||||||
|
|
||||||
@auth_bp.route('/logout')
|
|
||||||
def logout():
|
@auth_bp.route('/register-modal')
|
||||||
logout_user()
|
def register_modal():
|
||||||
|
form = RegistrationForm()
|
||||||
|
return render_template('register-modal.html', form=form)
|
||||||
|
|
||||||
|
@auth_bp.route('/login-modal')
|
||||||
|
def login_modal():
|
||||||
|
form = LoginForm()
|
||||||
|
return render_template('login-modal.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/logout')
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
52
config.py
52
config.py
@ -1,27 +1,27 @@
|
|||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv, find_dotenv
|
from dotenv import load_dotenv, find_dotenv
|
||||||
|
|
||||||
dotenv_path = find_dotenv()
|
dotenv_path = find_dotenv()
|
||||||
load_dotenv(dotenv_path, override=True)
|
load_dotenv(dotenv_path, override=True)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||||
WTF_CSRF_ENABLED = True
|
WTF_CSRF_ENABLED = True
|
||||||
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
|
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
|
||||||
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
|
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
|
||||||
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
|
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
|
||||||
UPLOAD_FOLDER = {
|
UPLOAD_FOLDER = {
|
||||||
'images': 'static/arts/',
|
'images': 'static/arts/',
|
||||||
'arts': 'static/arts/',
|
'arts': 'static/arts/',
|
||||||
'videos': 'static/videos/',
|
'videos': 'static/videos/',
|
||||||
'thumbnails': 'static/thumbnails/',
|
'thumbnails': 'static/thumbnails/',
|
||||||
'avatars': 'static/avatars/',
|
'avatars': 'static/avatars/',
|
||||||
'banners': 'static/banners/',
|
'banners': 'static/banners/',
|
||||||
'comics': 'static/comics',
|
'comics': 'static/comics',
|
||||||
'comicthumbs': 'static/comicthumbs/',
|
'comicthumbs': 'static/comicthumbs/',
|
||||||
'posts': 'static/posts/'
|
'posts': 'static/posts/'
|
||||||
}
|
}
|
||||||
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||||
ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'avi', 'mov'}
|
ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'avi', 'mov'}
|
||||||
MAX_IMAGE_SIZE = 15 * 1024 * 1024
|
MAX_IMAGE_SIZE = 15 * 1024 * 1024
|
||||||
MAX_VIDEO_SIZE = 10 * 1024 * 1024 * 1024
|
MAX_VIDEO_SIZE = 10 * 1024 * 1024 * 1024
|
16
license
16
license
@ -1,9 +1,9 @@
|
|||||||
Copyright (c) 2025 artberry.xyz
|
Copyright (c) 2025 artberry.xyz
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
The "artberry.xyz" brand, logo, and trademarks are protected and may not be used without explicit permission from the copyright holder. This license does not grant any rights to use the brand or trademarks associated with "artberry.xyz".
|
The "artberry.xyz" brand, logo, and trademarks are protected and may not be used without explicit permission from the copyright holder. This license does not grant any rights to use the brand or trademarks associated with "artberry.xyz".
|
528
models.py
528
models.py
@ -1,266 +1,264 @@
|
|||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
import shutil
|
import shutil
|
||||||
import random
|
import random
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify
|
from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_bcrypt import Bcrypt
|
from flask_bcrypt import Bcrypt
|
||||||
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
||||||
from flask_wtf import FlaskForm, RecaptchaField, CSRFProtect
|
from flask_wtf import FlaskForm, RecaptchaField, CSRFProtect
|
||||||
from flask_wtf.file import FileAllowed
|
from flask_wtf.file import FileAllowed
|
||||||
from flask_wtf.csrf import validate_csrf
|
from flask_wtf.csrf import validate_csrf
|
||||||
from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField, RadioField, SelectField, TextAreaField
|
from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField, RadioField, SelectField, TextAreaField
|
||||||
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Regexp
|
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Regexp
|
||||||
from dotenv import load_dotenv, find_dotenv
|
from dotenv import load_dotenv, find_dotenv
|
||||||
import aiofiles.os
|
import aiofiles.os
|
||||||
from sqlalchemy import func, or_
|
from sqlalchemy import func, or_
|
||||||
import magic
|
import magic
|
||||||
from config import Config
|
from config import Config
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField
|
from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
from flask_wtf.file import FileAllowed
|
from flask_wtf.file import FileAllowed
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
bcrypt = Bcrypt()
|
bcrypt = Bcrypt()
|
||||||
|
|
||||||
class Comments(db.Model):
|
class Comments(db.Model):
|
||||||
__tablename__ = 'comments'
|
__tablename__ = 'comments'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String, nullable=False)
|
username = db.Column(db.String, nullable=False)
|
||||||
image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True)
|
image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True)
|
||||||
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True)
|
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True)
|
||||||
comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=True)
|
comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=True)
|
||||||
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=True)
|
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=True)
|
||||||
comment_text = db.Column(db.Text, nullable=False)
|
comment_text = db.Column(db.Text, nullable=False)
|
||||||
comment_date = db.Column(db.DateTime, default=datetime.utcnow)
|
comment_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
image = db.relationship('Image', back_populates='comments')
|
image = db.relationship('Image', back_populates='comments')
|
||||||
video = db.relationship('Video', back_populates='comments')
|
video = db.relationship('Video', back_populates='comments')
|
||||||
comic = db.relationship('Comic', back_populates='comments', overlaps="comic_link")
|
comic = db.relationship('Comic', back_populates='comments', overlaps="comic_link")
|
||||||
post = db.relationship('Post', backref='comments')
|
post = db.relationship('Post', backref='comments')
|
||||||
|
|
||||||
class User(db.Model, UserMixin):
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(20), unique=True, nullable=False)
|
username = db.Column(db.String(20), unique=True, nullable=False)
|
||||||
encrypted_password = db.Column(db.String(60), nullable=False)
|
encrypted_password = db.Column(db.String(60), nullable=False)
|
||||||
ip_address = db.Column(db.String(15), nullable=True)
|
ip_address = db.Column(db.String(15), nullable=True)
|
||||||
avatar_file = db.Column(db.String(50), nullable=True)
|
avatar_file = db.Column(db.String(50), nullable=True)
|
||||||
banner_file = db.Column(db.String(50), nullable=True)
|
banner_file = db.Column(db.String(50), nullable=True)
|
||||||
bio = db.Column(db.Text, nullable=True)
|
bio = db.Column(db.Text, nullable=True)
|
||||||
current_item = db.Column(db.String(30), nullable=True)
|
current_item = db.Column(db.String(30), nullable=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<User {self.username}>'
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return bcrypt.check_password_hash(self.encrypted_password, password)
|
return bcrypt.check_password_hash(self.encrypted_password, password)
|
||||||
|
|
||||||
class Image(db.Model):
|
class Image(db.Model):
|
||||||
__tablename__ = 'image'
|
__tablename__ = 'image'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
image_file = db.Column(db.String(40), nullable=False)
|
image_file = db.Column(db.String(40), nullable=False)
|
||||||
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
||||||
publication_date = db.Column(db.DateTime, default=datetime.utcnow)
|
publication_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
tags = db.Column(db.String(100), nullable=True)
|
tags = db.Column(db.String(100), nullable=True)
|
||||||
cookie_votes = db.Column(db.Integer, default=0)
|
cookie_votes = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
comments = db.relationship('Comments', back_populates='image', cascade='all, delete-orphan')
|
comments = db.relationship('Comments', back_populates='image', cascade='all, delete-orphan')
|
||||||
|
|
||||||
class Votes(db.Model):
|
class Votes(db.Model):
|
||||||
__tablename__ = 'votes'
|
__tablename__ = 'votes'
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(100), nullable=False)
|
username = db.Column(db.String(100), nullable=False)
|
||||||
image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=False)
|
image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=False)
|
||||||
|
|
||||||
image = db.relationship('Image', backref=db.backref('votes', lazy=True))
|
image = db.relationship('Image', backref=db.backref('votes', lazy=True))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Vote {self.username} for image {self.image_id}>'
|
return f'<Vote {self.username} for image {self.image_id}>'
|
||||||
|
|
||||||
class VideoVotes(db.Model):
|
class VideoVotes(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(120), nullable=False)
|
username = db.Column(db.String(120), nullable=False)
|
||||||
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False)
|
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False)
|
||||||
|
|
||||||
class Video(db.Model):
|
class Video(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
video_file = db.Column(db.String(100), nullable=False)
|
video_file = db.Column(db.String(100), nullable=False)
|
||||||
video_name = db.Column(db.String(100), nullable=False)
|
video_name = db.Column(db.String(100), nullable=False)
|
||||||
video_thumbnail_file = db.Column(db.String(100), nullable=False)
|
video_thumbnail_file = db.Column(db.String(100), nullable=False)
|
||||||
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
||||||
publication_date = db.Column(db.DateTime, default=datetime.utcnow)
|
publication_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
tags = db.Column(db.String(100), nullable=True)
|
tags = db.Column(db.String(100), nullable=True)
|
||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
cookie_votes = db.Column(db.Integer, default=0)
|
cookie_votes = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
comments = db.relationship('Comments', back_populates='video')
|
comments = db.relationship('Comments', back_populates='video')
|
||||||
|
|
||||||
class Comic(db.Model):
|
class Comic(db.Model):
|
||||||
__tablename__ = 'comics'
|
__tablename__ = 'comics'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
comic_folder = db.Column(db.String(100), nullable=False)
|
comic_folder = db.Column(db.String(100), nullable=False)
|
||||||
comic_thumbnail_file = db.Column(db.String(100), nullable=False)
|
comic_thumbnail_file = db.Column(db.String(100), nullable=False)
|
||||||
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
||||||
name = db.Column(db.String(100), nullable=False, unique=True)
|
name = db.Column(db.String(100), nullable=False, unique=True)
|
||||||
publication_date = db.Column(db.DateTime, default=datetime.utcnow)
|
publication_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
tags = db.Column(db.String(100), nullable=True)
|
tags = db.Column(db.String(100), nullable=True)
|
||||||
cookie_votes = db.Column(db.Integer, default=0)
|
cookie_votes = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
comments = db.relationship('Comments', back_populates='comic', overlaps="comic_link")
|
comments = db.relationship('Comments', back_populates='comic', overlaps="comic_link")
|
||||||
pages = db.relationship('ComicPage', back_populates='comic', cascade="all, delete-orphan")
|
pages = db.relationship('ComicPage', back_populates='comic', cascade="all, delete-orphan")
|
||||||
|
|
||||||
class ComicPage(db.Model):
|
class ComicPage(db.Model):
|
||||||
__tablename__ = 'comic_pages'
|
__tablename__ = 'comic_pages'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
comic_id = db.Column(db.Integer, db.ForeignKey('comics.id', ondelete='CASCADE'), nullable=False)
|
comic_id = db.Column(db.Integer, db.ForeignKey('comics.id', ondelete='CASCADE'), nullable=False)
|
||||||
page_number = db.Column(db.Integer, nullable=False)
|
page_number = db.Column(db.Integer, nullable=False)
|
||||||
file_path = db.Column(db.String(200), nullable=False)
|
file_path = db.Column(db.String(200), nullable=False)
|
||||||
|
|
||||||
comic = db.relationship('Comic', back_populates='pages')
|
comic = db.relationship('Comic', back_populates='pages')
|
||||||
|
|
||||||
class ComicVotes(db.Model):
|
class ComicVotes(db.Model):
|
||||||
__tablename__ = 'comic_votes'
|
__tablename__ = 'comic_votes'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(100), nullable=False)
|
username = db.Column(db.String(100), nullable=False)
|
||||||
comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=False)
|
comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=False)
|
||||||
vote = db.Column(db.Integer)
|
vote = db.Column(db.Integer)
|
||||||
|
|
||||||
comic = db.relationship('Comic', backref='votes', lazy=True)
|
comic = db.relationship('Comic', backref='votes', lazy=True)
|
||||||
|
|
||||||
class Cookies(db.Model):
|
class Cookies(db.Model):
|
||||||
username = db.Column(db.String(20), primary_key=True, nullable=False)
|
username = db.Column(db.String(20), primary_key=True, nullable=False)
|
||||||
cookies = db.Column(db.Integer, default=0)
|
cookies = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
class Views(db.Model):
|
class Views(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True)
|
image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True)
|
||||||
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True)
|
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True)
|
||||||
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
||||||
view_date = db.Column(db.DateTime, default=datetime.utcnow)
|
view_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.UniqueConstraint('image_id', 'username', name='unique_image_view'),
|
db.UniqueConstraint('image_id', 'username', name='unique_image_view'),
|
||||||
db.UniqueConstraint('video_id', 'username', name='unique_video_view')
|
db.UniqueConstraint('video_id', 'username', name='unique_video_view')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Item(db.Model):
|
class Item(db.Model):
|
||||||
__tablename__ = 'items'
|
__tablename__ = 'items'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
item_path = db.Column(db.String, nullable=False)
|
item_path = db.Column(db.String, nullable=False)
|
||||||
price = db.Column(db.Integer, nullable=False, default=0)
|
price = db.Column(db.Integer, nullable=False, default=0)
|
||||||
visible = db.Column(db.Boolean, default=True)
|
visible = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Item {self.id}: {self.item_path}, Price: {self.price}, Visible: {self.visible}>'
|
return f'<Item {self.id}: {self.item_path}, Price: {self.price}, Visible: {self.visible}>'
|
||||||
|
|
||||||
class UserItem(db.Model):
|
class UserItem(db.Model):
|
||||||
__tablename__ = 'user_items'
|
__tablename__ = 'user_items'
|
||||||
|
|
||||||
username = db.Column(db.String, db.ForeignKey('user.username'), primary_key=True)
|
username = db.Column(db.String, db.ForeignKey('user.username'), primary_key=True)
|
||||||
item_id = db.Column(db.Integer, db.ForeignKey('items.id'), primary_key=True)
|
item_id = db.Column(db.Integer, db.ForeignKey('items.id'), primary_key=True)
|
||||||
|
|
||||||
item = db.relationship('Item', backref=db.backref('user_items', lazy=True))
|
item = db.relationship('Item', backref=db.backref('user_items', lazy=True))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<UserItem {self.username}, Item {self.item_id}>'
|
return f'<UserItem {self.username}, Item {self.item_id}>'
|
||||||
|
|
||||||
class Post(db.Model):
|
class Post(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
|
||||||
post_date = db.Column(db.DateTime, default=datetime.utcnow)
|
post_date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
text = db.Column(db.Text, nullable=False)
|
text = db.Column(db.Text, nullable=False)
|
||||||
media_file = db.Column(db.String(100), nullable=True)
|
media_file = db.Column(db.String(100), nullable=True)
|
||||||
|
|
||||||
user = db.relationship('User', backref=db.backref('posts', lazy=True))
|
user = db.relationship('User', backref=db.backref('posts', lazy=True))
|
||||||
|
|
||||||
class Subscription(db.Model):
|
class Subscription(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('subscriptions', lazy=True))
|
user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('subscriptions', lazy=True))
|
||||||
author = db.relationship('User', foreign_keys=[author_id], backref=db.backref('followers', lazy=True))
|
author = db.relationship('User', foreign_keys=[author_id], backref=db.backref('followers', lazy=True))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Subscription user_id={self.user_id} author_id={self.author_id}>'
|
return f'<Subscription user_id={self.user_id} author_id={self.author_id}>'
|
||||||
|
|
||||||
class UploadForm(FlaskForm):
|
class UploadForm(FlaskForm):
|
||||||
image_file = FileField('Choose File', validators=[DataRequired()])
|
image_file = FileField('Choose File', validators=[DataRequired()])
|
||||||
tags = StringField('Tags (comma-separated)', validators=[DataRequired()])
|
tags = StringField('Tags (comma-separated)', validators=[DataRequired()])
|
||||||
recaptcha = RecaptchaField()
|
recaptcha = RecaptchaField()
|
||||||
agree_with_rules = BooleanField('I agree with the publication rules',
|
agree_with_rules = BooleanField('I agree with the publication rules',
|
||||||
validators=[DataRequired(message="You must agree with the publication rules.")])
|
validators=[DataRequired(message="You must agree with the publication rules.")])
|
||||||
submit = SubmitField('Upload')
|
submit = SubmitField('Upload')
|
||||||
|
|
||||||
class UploadVideoForm(FlaskForm):
|
class UploadVideoForm(FlaskForm):
|
||||||
video_file = FileField('Video File', validators=[DataRequired()])
|
video_file = FileField('Video File', validators=[DataRequired()])
|
||||||
thumbnail = FileField('Thumbnail', validators=[DataRequired(), FileAllowed(['jpg', 'png', 'jpeg'])])
|
thumbnail = FileField('Thumbnail', validators=[DataRequired(), FileAllowed(['jpg', 'png', 'jpeg'])])
|
||||||
name = StringField('Video Name', validators=[DataRequired()])
|
name = StringField('Video Name', validators=[DataRequired()])
|
||||||
tags = StringField('Tags', validators=[DataRequired()])
|
tags = StringField('Tags', validators=[DataRequired()])
|
||||||
description = StringField('Description', validators=[DataRequired()])
|
description = StringField('Description', validators=[DataRequired()])
|
||||||
recaptcha = RecaptchaField()
|
recaptcha = RecaptchaField()
|
||||||
submit = SubmitField('Upload')
|
submit = SubmitField('Upload')
|
||||||
agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired()])
|
agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired()])
|
||||||
|
|
||||||
class UploadComicForm(FlaskForm):
|
class UploadComicForm(FlaskForm):
|
||||||
title = StringField('Comic Name', validators=[DataRequired()])
|
title = StringField('Comic Name', validators=[DataRequired()])
|
||||||
thumbnail = FileField('Thumbnail', validators=[DataRequired()])
|
thumbnail = FileField('Thumbnail', validators=[DataRequired()])
|
||||||
tags = StringField('Tags (comma-separated)')
|
tags = StringField('Tags (comma-separated)')
|
||||||
recaptcha = RecaptchaField()
|
recaptcha = RecaptchaField()
|
||||||
agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired(message="You must agree with the publication rules.")])
|
agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired(message="You must agree with the publication rules.")])
|
||||||
submit = SubmitField('Upload')
|
submit = SubmitField('Upload')
|
||||||
|
|
||||||
class EditTagsForm(FlaskForm):
|
class EditTagsForm(FlaskForm):
|
||||||
tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter tags"})
|
tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter tags"})
|
||||||
submit = SubmitField('Save')
|
submit = SubmitField('Save')
|
||||||
|
|
||||||
class EditVideoForm(FlaskForm):
|
class EditVideoForm(FlaskForm):
|
||||||
video_name = StringField('Title', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter video title"})
|
video_name = StringField('Title', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter video title"})
|
||||||
video_thumbnail = FileField('Thumbnail', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Only images!')])
|
video_thumbnail = FileField('Thumbnail', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Only images!')])
|
||||||
description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)], render_kw={"placeholder": "Enter video description"})
|
description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)], render_kw={"placeholder": "Enter video description"})
|
||||||
tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Tags"})
|
tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Tags"})
|
||||||
submit = SubmitField('Save')
|
submit = SubmitField('Save')
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
class LoginForm(FlaskForm):
|
||||||
username = StringField('Username', validators=[DataRequired()])
|
username = StringField('Username', validators=[DataRequired()])
|
||||||
password = PasswordField('Password', validators=[DataRequired()])
|
password = PasswordField('Password', validators=[DataRequired()])
|
||||||
recaptcha = RecaptchaField()
|
recaptcha = RecaptchaField()
|
||||||
submit = SubmitField('Login')
|
submit = SubmitField('Login')
|
||||||
|
|
||||||
class RegistrationForm(FlaskForm):
|
|
||||||
username = StringField(
|
class RegistrationForm(FlaskForm):
|
||||||
'Username',
|
username = StringField('Username',
|
||||||
validators=[
|
validators=[DataRequired(), Length(3,20),
|
||||||
DataRequired(),
|
Regexp('^[a-zA-Z0-9_]+$')])
|
||||||
Length(min=3, max=20),
|
password = PasswordField('Password',
|
||||||
Regexp(r'^[a-zA-Z0-9_]+$', message="Username can contain only letters, numbers, and underscores.")
|
validators=[DataRequired(), Length(min=6)])
|
||||||
]
|
confirm_password = PasswordField('Confirm Password',
|
||||||
)
|
validators=[DataRequired(), EqualTo('password')])
|
||||||
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
|
recaptcha = RecaptchaField()
|
||||||
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
|
submit = SubmitField('Register')
|
||||||
recaptcha = RecaptchaField()
|
|
||||||
submit = SubmitField('Register')
|
class EmptyForm(FlaskForm):
|
||||||
|
pass
|
||||||
class EmptyForm(FlaskForm):
|
|
||||||
pass
|
class UpdateCookiesForm(FlaskForm):
|
||||||
|
cookies = StringField('Количество печенек', validators=[DataRequired()])
|
||||||
class UpdateCookiesForm(FlaskForm):
|
|
||||||
cookies = StringField('Количество печенек', validators=[DataRequired()])
|
|
||||||
submit = SubmitField('Применить')
|
submit = SubmitField('Применить')
|
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
File diff suppressed because it is too large
Load Diff
23
static/favicon.svg
Normal file
23
static/favicon.svg
Normal 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 |
@ -1,3 +1,3 @@
|
|||||||
<svg width="15" height="25" viewBox="0 0 15 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="15" height="25" viewBox="0 0 15 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M14.0607 13.5607C14.6464 12.9749 14.6464 12.0251 14.0607 11.4393L4.51472 1.8934C3.92893 1.30761 2.97919 1.30761 2.3934 1.8934C1.80761 2.47919 1.80761 3.42893 2.3934 4.01472L10.8787 12.5L2.3934 20.9853C1.80761 21.5711 1.80761 22.5208 2.3934 23.1066C2.97919 23.6924 3.92893 23.6924 4.51472 23.1066L14.0607 13.5607ZM12 14H13V11H12V14Z" fill="#8784C9"/>
|
<path d="M14.0607 13.5607C14.6464 12.9749 14.6464 12.0251 14.0607 11.4393L4.51472 1.8934C3.92893 1.30761 2.97919 1.30761 2.3934 1.8934C1.80761 2.47919 1.80761 3.42893 2.3934 4.01472L10.8787 12.5L2.3934 20.9853C1.80761 21.5711 1.80761 22.5208 2.3934 23.1066C2.97919 23.6924 3.92893 23.6924 4.51472 23.1066L14.0607 13.5607ZM12 14H13V11H12V14Z" fill="#8784C9"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 465 B |
@ -1,15 +1,15 @@
|
|||||||
let pageCount = 1;
|
let pageCount = 1;
|
||||||
const maxPages = 64;
|
const maxPages = 64;
|
||||||
|
|
||||||
function addPage() {
|
function addPage() {
|
||||||
if (pageCount >= maxPages) {
|
if (pageCount >= maxPages) {
|
||||||
alert(`You can't add more than ${maxPages} pages!`);
|
alert(`You can't add more than ${maxPages} pages!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pageCount++;
|
pageCount++;
|
||||||
const newPage = document.createElement("div");
|
const newPage = document.createElement("div");
|
||||||
newPage.classList.add("form-group");
|
newPage.classList.add("form-group");
|
||||||
newPage.innerHTML = `<label>Page ${pageCount}:</label><input type="file" name="pages[]" class="file-input" accept="image/*" required>`;
|
newPage.innerHTML = `<label>Page ${pageCount}:</label><input type="file" name="pages[]" class="file-input" accept="image/*" required>`;
|
||||||
document.getElementById("pages").appendChild(newPage);
|
document.getElementById("pages").appendChild(newPage);
|
||||||
}
|
}
|
@ -1,64 +1,63 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const cardCovers = document.querySelectorAll('.small-card-cover, .card-cover');
|
const cardCovers = document.querySelectorAll('.small-card-cover, .card-cover');
|
||||||
|
|
||||||
cardCovers.forEach(cardCover => {
|
cardCovers.forEach(cardCover => {
|
||||||
const thumbnail = cardCover.querySelector('.thumbnail');
|
const thumbnail = cardCover.querySelector('.thumbnail');
|
||||||
const video = cardCover.querySelector('.preview-video');
|
const video = cardCover.querySelector('.preview-video');
|
||||||
const videoSource = video.querySelector('source');
|
const videoSource = video.querySelector('source');
|
||||||
let hoverTimeout;
|
|
||||||
|
const showPreview = () => {
|
||||||
cardCover.addEventListener('mouseenter', () => {
|
thumbnail.style.display = 'none';
|
||||||
hoverTimeout = setTimeout(() => {
|
video.style.display = 'block';
|
||||||
|
|
||||||
if (!videoSource.src) {
|
if (video.duration > 15) {
|
||||||
videoSource.src = video.dataset.src;
|
video.currentTime = 10;
|
||||||
video.load();
|
const loopSegment = () => {
|
||||||
}
|
if (video.currentTime >= 15) {
|
||||||
|
video.currentTime = 10;
|
||||||
thumbnail.style.display = 'none';
|
video.play();
|
||||||
video.style.display = 'block';
|
}
|
||||||
|
};
|
||||||
video.addEventListener('loadedmetadata', () => {
|
video.addEventListener('timeupdate', loopSegment);
|
||||||
|
cardCover.loopSegment = loopSegment;
|
||||||
if (video.duration > 15) {
|
} else {
|
||||||
|
video.currentTime = 0;
|
||||||
video.currentTime = 10;
|
const loopEntireVideo = () => {
|
||||||
const loopSegment = () => {
|
if (video.currentTime >= video.duration) {
|
||||||
if (video.currentTime >= 15) {
|
video.currentTime = 0;
|
||||||
video.currentTime = 10;
|
video.play();
|
||||||
video.play();
|
}
|
||||||
}
|
};
|
||||||
};
|
video.addEventListener('timeupdate', loopEntireVideo);
|
||||||
video.addEventListener('timeupdate', loopSegment);
|
cardCover.loopSegment = loopEntireVideo;
|
||||||
cardCover.loopSegment = loopSegment;
|
}
|
||||||
} else {
|
|
||||||
|
video.play();
|
||||||
video.currentTime = 0;
|
};
|
||||||
const loopEntireVideo = () => {
|
|
||||||
if (video.currentTime >= video.duration) {
|
cardCover.addEventListener('mouseenter', () => {
|
||||||
video.currentTime = 0;
|
video.pause();
|
||||||
video.play();
|
videoSource.src = video.dataset.src;
|
||||||
}
|
video.load();
|
||||||
};
|
video.addEventListener('canplay', showPreview, { once: true });
|
||||||
video.addEventListener('timeupdate', loopEntireVideo);
|
});
|
||||||
cardCover.loopSegment = loopEntireVideo;
|
|
||||||
}
|
cardCover.addEventListener('mouseleave', (e) => {
|
||||||
|
const toElement = e.relatedTarget;
|
||||||
video.play();
|
|
||||||
});
|
if (!cardCover.contains(toElement)) {
|
||||||
}, 2000);
|
video.pause();
|
||||||
});
|
video.currentTime = 0;
|
||||||
|
video.style.display = 'none';
|
||||||
cardCover.addEventListener('mouseleave', () => {
|
thumbnail.style.display = 'block';
|
||||||
clearTimeout(hoverTimeout);
|
|
||||||
video.pause();
|
if (cardCover.loopSegment) {
|
||||||
video.style.display = 'none';
|
video.removeEventListener('timeupdate', cardCover.loopSegment);
|
||||||
thumbnail.style.display = 'block';
|
delete cardCover.loopSegment;
|
||||||
|
}
|
||||||
if (cardCover.loopSegment) {
|
|
||||||
video.removeEventListener('timeupdate', cardCover.loopSegment);
|
videoSource.src = '';
|
||||||
delete cardCover.loopSegment;
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
@ -1,17 +1,92 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const iconContainer = document.querySelector('.icon-container');
|
const iconContainer = document.querySelector('.icon-container');
|
||||||
const dropdownMenu = document.querySelector('.dropdown-menu');
|
const dropdownMenu = document.querySelector('.dropdown-menu');
|
||||||
|
|
||||||
iconContainer.addEventListener('click', function (event) {
|
iconContainer.addEventListener('click', function (event) {
|
||||||
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
|
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', function () {
|
document.addEventListener('click', function () {
|
||||||
dropdownMenu.style.display = 'none';
|
dropdownMenu.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
dropdownMenu.addEventListener('click', function (event) {
|
dropdownMenu.addEventListener('click', function (event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
// Делегируем клики для открытия логина/регистрации внутри любых модалок
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
const target = e.target.closest('[data-action]');
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const action = target.dataset.action;
|
||||||
|
if (action === 'open-register') {
|
||||||
|
openRegisterModal();
|
||||||
|
} else if (action === 'open-login') {
|
||||||
|
openLoginModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function closeAllModals() {
|
||||||
|
const modalContainer = document.getElementById('modal-container');
|
||||||
|
if (modalContainer) modalContainer.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRegisterModal() {
|
||||||
|
closeAllModals();
|
||||||
|
|
||||||
|
const modalContainer = document.createElement('div');
|
||||||
|
modalContainer.id = 'modal-container';
|
||||||
|
document.body.appendChild(modalContainer);
|
||||||
|
|
||||||
|
fetch('/register-modal')
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
modalContainer.innerHTML = html;
|
||||||
|
reloadRecaptcha(modalContainer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLoginModal() {
|
||||||
|
closeAllModals();
|
||||||
|
|
||||||
|
const modalContainer = document.createElement('div');
|
||||||
|
modalContainer.id = 'modal-container';
|
||||||
|
document.body.appendChild(modalContainer);
|
||||||
|
|
||||||
|
fetch('/login-modal')
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
modalContainer.innerHTML = html;
|
||||||
|
reloadRecaptcha(modalContainer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadRecaptcha(modalContainer) {
|
||||||
|
const recaptchaScript = modalContainer.querySelector('script[src*="recaptcha"]');
|
||||||
|
if (recaptchaScript) recaptchaScript.remove();
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://www.google.com/recaptcha/api.js?render=explicit&onload=recaptchaCallback';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.recaptchaCallback = function () {
|
||||||
|
const recaptchaElement = document.querySelector('.g-recaptcha');
|
||||||
|
if (recaptchaElement && typeof grecaptcha !== 'undefined') {
|
||||||
|
grecaptcha.render(recaptchaElement, {
|
||||||
|
theme: "dark",
|
||||||
|
size: "default"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
const modal = document.querySelector('#modal-container .modal');
|
||||||
|
if (modal && !modal.contains(e.target)) {
|
||||||
|
closeAllModals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
window.addEventListener('DOMContentLoaded', function() {
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
const listButton = document.querySelector('.list-button');
|
const listButton = document.querySelector('.list-button');
|
||||||
const tags = document.querySelectorAll('.tag');
|
const tags = document.querySelectorAll('.tag');
|
||||||
|
|
||||||
const buttonRect = listButton.getBoundingClientRect();
|
const buttonRect = listButton.getBoundingClientRect();
|
||||||
|
|
||||||
function checkTagVisibility() {
|
function checkTagVisibility() {
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
const tagRect = tag.getBoundingClientRect();
|
const tagRect = tag.getBoundingClientRect();
|
||||||
|
|
||||||
if (tagRect.right > buttonRect.left) {
|
if (tagRect.right > buttonRect.left) {
|
||||||
tag.style.visibility = 'hidden';
|
tag.style.visibility = 'hidden';
|
||||||
} else {
|
} else {
|
||||||
tag.style.visibility = 'visible';
|
tag.style.visibility = 'visible';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector('.tags-container').addEventListener('scroll', checkTagVisibility);
|
document.querySelector('.tags-container').addEventListener('scroll', checkTagVisibility);
|
||||||
|
|
||||||
checkTagVisibility();
|
checkTagVisibility();
|
||||||
});
|
});
|
@ -1,40 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% include 'navbar.html' %}
|
{% include 'navbar.html' %}
|
||||||
{% include 'subnav.html' %}
|
{% include 'subnav.html' %}
|
||||||
<div class="container img-new-content">
|
<div class="container img-new-content">
|
||||||
<a class="new-content-text">ПОПУЛЯРНОЕ</a>
|
<a class="new-content-text">ПОПУЛЯРНОЕ</a>
|
||||||
<div class="ac-img-small-cards-grid">
|
<div class="ac-img-small-cards-grid">
|
||||||
{% for i in range(36) %}
|
{% for i in range(36) %}
|
||||||
<div class="ct-img-card">
|
<div class="ct-img-card">
|
||||||
<div class="ct-img-card-cover"></div>
|
<div class="ct-img-card-cover"></div>
|
||||||
<div class="ct-small-card-info">
|
<div class="ct-small-card-info">
|
||||||
<div class="ct-small-card-header">
|
<div class="ct-small-card-header">
|
||||||
<span class="ct-small-card-text">Totoka</span>
|
<span class="ct-small-card-text">Totoka</span>
|
||||||
<div class="ct-small-card-stats">
|
<div class="ct-small-card-stats">
|
||||||
<div class="ct-small-stat">
|
<div class="ct-small-stat">
|
||||||
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
||||||
<span class="ct-small-card-text">134</span>
|
<span class="ct-small-card-text">134</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ct-small-stat">
|
<div class="ct-small-stat">
|
||||||
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
||||||
<span class="ct-small-card-text">32113</span>
|
<span class="ct-small-card-text">32113</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="ct-small-card-text">Big Brother Keep Hugging Me</p>
|
<p class="ct-small-card-text">Big Brother Keep Hugging Me</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<button class="most-new-button">
|
<button class="most-new-button">
|
||||||
<span class="most-new-button-text">Смотреть Больше</span>
|
<span class="most-new-button-text">Смотреть Больше</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% include 'pagination.html' %}
|
{% include 'pagination.html' %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,113 +1,113 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% include 'navbar.html' %}
|
{% include 'navbar.html' %}
|
||||||
{% include 'subnav.html' %}
|
{% include 'subnav.html' %}
|
||||||
{% include 'tags_list.html' %}
|
{% include 'tags_list.html' %}
|
||||||
<div class="container img-new-content">
|
<div class="container img-new-content">
|
||||||
<a class="new-content-text">НОВИНКИ</a>
|
<a class="new-content-text">НОВИНКИ</a>
|
||||||
<div class="img-small-cards-grid">
|
<div class="img-small-cards-grid">
|
||||||
{% for i in range(7) %}
|
{% for i in range(7) %}
|
||||||
<div class="img-small-card">
|
<div class="img-small-card">
|
||||||
<div class="img-small-card-cover"></div>
|
<div class="img-small-card-cover"></div>
|
||||||
<div class="small-card-info">
|
<div class="small-card-info">
|
||||||
<div class="small-card-header">
|
<div class="small-card-header">
|
||||||
<span class="small-card-text">Totoka</span>
|
<span class="small-card-text">Totoka</span>
|
||||||
<div class="small-card-stats">
|
<div class="small-card-stats">
|
||||||
<div class="small-stat">
|
<div class="small-stat">
|
||||||
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
||||||
<span class="small-card-text">134</span>
|
<span class="small-card-text">134</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small-stat">
|
<div class="small-stat">
|
||||||
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
||||||
<span class="small-card-text">32113</span>
|
<span class="small-card-text">32113</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="small-card-text">Big Brother Keep Hugging Me</p>
|
<p class="small-card-text">Big Brother Keep Hugging Me</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<button class="view-more-button" style="width: 1503px; height: 40px; gap: 10px; padding: 10px 86px;">
|
<button class="view-more-button" style="width: 1503px; height: 40px; gap: 10px; padding: 10px 86px;">
|
||||||
<span class="new-context-button-text">Смотреть Больше</span>
|
<span class="new-context-button-text">Смотреть Больше</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="container img-popular-content">
|
<div class="container img-popular-content">
|
||||||
<a class="popular-content-text">ПОПУЛЯРНОЕ</a>
|
<a class="popular-content-text">ПОПУЛЯРНОЕ</a>
|
||||||
<div class="img-cards-grid">
|
<div class="img-cards-grid">
|
||||||
{% for i in range(12) %}
|
{% for i in range(12) %}
|
||||||
<div class="img-card">
|
<div class="img-card">
|
||||||
<div class="img-card-cover"></div>
|
<div class="img-card-cover"></div>
|
||||||
<div class="card-info">
|
<div class="card-info">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title" style="color: #3C3882;">Totoka</span>
|
<span class="card-title" style="color: #3C3882;">Totoka</span>
|
||||||
<div class="card-stats">
|
<div class="card-stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
||||||
<span style="color: #8784C9;">134</span>
|
<span style="color: #8784C9;">134</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
||||||
<span style="color: #8784C9;">32113</span>
|
<span style="color: #8784C9;">32113</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text" style="font-family: Nunito, sans-serif;">Big Brother Keep Hugging Me </p>
|
<p class="card-text" style="font-family: Nunito, sans-serif;">Big Brother Keep Hugging Me </p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<button class="view-more-button" style="width: 1500px; height: 40px; padding: 10px 86px;">
|
<button class="view-more-button" style="width: 1500px; height: 40px; padding: 10px 86px;">
|
||||||
<span class="new-context-button-text">Смотреть Больше</span>
|
<span class="new-context-button-text">Смотреть Больше</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="container img-viewed-content">
|
<div class="container img-viewed-content">
|
||||||
<a class="viewed-content-text">ПРОСМАТРИВАЕМОЕ</a>
|
<a class="viewed-content-text">ПРОСМАТРИВАЕМОЕ</a>
|
||||||
<div class="img-cards-grid">
|
<div class="img-cards-grid">
|
||||||
{% for i in range(12) %}
|
{% for i in range(12) %}
|
||||||
<div class="img-card">
|
<div class="img-card">
|
||||||
<div class="img-card-cover"></div>
|
<div class="img-card-cover"></div>
|
||||||
<div class="card-info">
|
<div class="card-info">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title" style="color: #3C3882;">Totoka</span>
|
<span class="card-title" style="color: #3C3882;">Totoka</span>
|
||||||
<div class="card-stats">
|
<div class="card-stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
||||||
<span style="color: #8784C9;">134</span>
|
<span style="color: #8784C9;">134</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
||||||
<span style="color: #8784C9;">32113</span>
|
<span style="color: #8784C9;">32113</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text" style="font-family: Nunito, sans-serif;">Big Brother Keep Hugging Me </p>
|
<p class="card-text" style="font-family: Nunito, sans-serif;">Big Brother Keep Hugging Me </p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<button class="view-more-button" style="width: 1500px; height: 40px; padding: 10px 86px;margin-bottom: -170px;">
|
<button class="view-more-button" style="width: 1500px; height: 40px; padding: 10px 86px;margin-bottom: -170px;">
|
||||||
<span class="new-context-button-text">Смотреть Больше</span>
|
<span class="new-context-button-text">Смотреть Больше</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="container img-popular-categories">
|
<div class="container img-popular-categories">
|
||||||
<a class="popular-categories-text">ПОПУЛЯРНЫЕ КАТЕГОРИИ</a>
|
<a class="popular-categories-text">ПОПУЛЯРНЫЕ КАТЕГОРИИ</a>
|
||||||
{% for i in range(6) %}
|
{% for i in range(6) %}
|
||||||
<div class="pc-card">
|
<div class="pc-card">
|
||||||
<div class="pc-card-cover"></div>
|
<div class="pc-card-cover"></div>
|
||||||
<div class="pc-card-info">
|
<div class="pc-card-info">
|
||||||
<div class="pc-card-stats">
|
<div class="pc-card-stats">
|
||||||
<span class="category-name-text">Category</span>
|
<span class="category-name-text">Category</span>
|
||||||
<span class="categories-number" style="--length: 4;">14655</span>
|
<span class="categories-number" style="--length: 4;">14655</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,14 +1,14 @@
|
|||||||
{% extends "content.html" %}
|
{% extends "content.html" %}
|
||||||
{% include 'subnav.html' %}
|
{% include 'subnav.html' %}
|
||||||
{% block title %}🫐videos - artberry🫐{% endblock %}
|
{% block title %}🫐videos - artberry🫐{% endblock %}
|
||||||
|
|
||||||
{% block new_content %}
|
{% block new_content %}
|
||||||
<div class="container new-content">
|
<div class="container new-content">
|
||||||
<a class="new-content-text">НОВИНКИ (Updated for Videos)</a>
|
<a class="new-content-text">НОВИНКИ (Updated for Videos)</a>
|
||||||
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'tags_list.html' %}
|
{% include 'tags_list.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
47
templates/login-modal.html
Normal file
47
templates/login-modal.html
Normal 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>
|
@ -1,35 +1,60 @@
|
|||||||
{% extends "auth.html" %}
|
<!DOCTYPE html>
|
||||||
{% block title %}🫐login - artberry🫐{% endblock %}
|
<html lang="ru">
|
||||||
{% set action = 'auth.login' %}
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@500&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loginModal" class="modal active">
|
||||||
|
<form method="POST" action="{{ url_for('auth.register') }}">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="form-inner-container">
|
||||||
|
<p class="login-form-title">ВХОД</p>
|
||||||
|
|
||||||
|
<div class="modal-login-input-container">
|
||||||
|
{{ form.username(class_="modal-login-text-input", placeholder="Имя пользователя") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-login-input-container">
|
||||||
|
{{ form.password(class_="modal-login-text-input password-input", placeholder="Пароль") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% block content %}
|
<div class="login-link-container">
|
||||||
<form method="POST" action="{{ url_for(action) }}">
|
<span class="login-prompt">
|
||||||
{{ form.hidden_tag() }}
|
<a href="#" class="login-link" data-action="forgot-password">Забыли пароль?</a>
|
||||||
|
</span>
|
||||||
{% for field, errors in form.errors.items() %}
|
</div>
|
||||||
<ul>
|
|
||||||
{% for error in errors %}
|
<div class="login-link-container">
|
||||||
<li><strong>{{ field.label }}:</strong> {{ error }}</li>
|
<span class="login-prompt">
|
||||||
{% endfor %}
|
Нет аккаунта?
|
||||||
</ul>
|
<a href="#" class="login-link" data-action="open-register">Зарегистрируйся тут!</a>
|
||||||
{% endfor %}
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<label for="{{ form.username.id }}">{{ form.username.label.text }}</label>
|
<div class="recaptcha-wrapper">
|
||||||
{{ form.username(class="input-field", placeholder="Enter username") }}<br>
|
{{ form.recaptcha() }}
|
||||||
|
</div>
|
||||||
<label for="{{ form.password.id }}">{{ form.password.label.text }}</label>
|
|
||||||
{{ form.password(class="input-field", placeholder="Enter password") }}<br>
|
<button type="submit" class="modal-login-button">
|
||||||
|
<span class="modal-login-button-text">Войти</span>
|
||||||
<div class="recaptcha-container">
|
</button>
|
||||||
{{ form.recaptcha.label }}
|
</div>
|
||||||
{{ form.recaptcha() }}<br>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.matches('[data-action="open-register"]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
closeAllModals();
|
||||||
|
openRegisterModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{{ form.submit(class="login-button button", value="Login") }}
|
</body>
|
||||||
</form>
|
</html>
|
||||||
|
|
||||||
<div class="link-container">
|
|
||||||
<span class="link-text">Don't have an account?</span>
|
|
||||||
<a href="{{ url_for('auth.register') }}" class="button">Register</a>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,55 +1,59 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="navbar-wrapper">
|
<div class="navbar-wrapper">
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<img src="{{ url_for('static', filename='navbar/logo.svg') }}" alt="Логотип" class="logo">
|
<img src="{{ url_for('static', filename='navbar/logo.svg') }}" alt="Логотип" class="logo">
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<div class="search-icon-container">
|
<div class="search-icon-container">
|
||||||
<img src="{{ url_for('static', filename='navbar/search-icon.svg') }}" alt="Поиск" class="search-icon">
|
<img src="{{ url_for('static', filename='navbar/search-icon.svg') }}" alt="Поиск" class="search-icon">
|
||||||
<img src="{{ url_for('static', filename='navbar/search-hover.svg') }}" alt="Поиск (hover)"
|
<img src="{{ url_for('static', filename='navbar/search-hover.svg') }}" alt="Поиск (hover)"
|
||||||
class="search-hover-icon">
|
class="search-hover-icon">
|
||||||
</div>
|
</div>
|
||||||
<input type="text" placeholder="Поиск" class="search-input" onfocus="this.placeholder=''" onblur="this.placeholder='Поиск'">
|
<input type="text" placeholder="Поиск" class="search-input" onfocus="this.placeholder=''" onblur="this.placeholder='Поиск'">
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
<img src="{{ url_for('static', filename='navbar/video-icon.svg') }}" alt="Видео" class="video-icon">
|
<img src="{{ url_for('static', filename='navbar/video-icon.svg') }}" alt="Видео" class="video-icon">
|
||||||
<img src="{{ url_for('static', filename='navbar/tray-icon.svg') }}" alt="Поднос" class="tray-icon">
|
<img src="{{ url_for('static', filename='navbar/tray-icon.svg') }}" alt="Поднос" class="tray-icon">
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<div class="dropdown-item">Пункт 1</div>
|
<div class="dropdown-item">Пункт 1</div>
|
||||||
<div class="dropdown-item">Пункт 2</div>
|
<div class="dropdown-item">Пункт 2</div>
|
||||||
<div class="dropdown-item">Пункт 3</div>
|
<div class="dropdown-item">Пункт 3</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="translate-btn">
|
<div class="translate-btn">
|
||||||
<img src="{{ url_for('static', filename='navbar/translate-icon.svg') }}" alt="Перевод"
|
<img src="{{ url_for('static', filename='navbar/translate-icon.svg') }}" alt="Перевод"
|
||||||
class="translate-icon">
|
class="translate-icon">
|
||||||
<img src="{{ url_for('static', filename='navbar/translate-hover.svg') }}" alt="Перевод (hover)"
|
<img src="{{ url_for('static', filename='navbar/translate-hover.svg') }}" alt="Перевод (hover)"
|
||||||
class="translate-hover-icon">
|
class="translate-hover-icon">
|
||||||
</div>
|
</div>
|
||||||
<nav class="menu">
|
<nav class="menu">
|
||||||
<a href="/videos">ВИДЕО</a>
|
<a href="/videos">ВИДЕО</a>
|
||||||
<a href="/">АРТЫ</a>
|
<a href="/">АРТЫ</a>
|
||||||
<a href="/comics">МАНГА</a>
|
<a href="/comics">МАНГА</a>
|
||||||
<a href="/gifs">ГИФКИ</a>
|
<a href="/gifs">ГИФКИ</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
<div class="discord-icon-container">
|
<div class="discord-icon-container">
|
||||||
<img src="{{ url_for('static', filename='navbar/discord-icon.svg') }}" alt="Discord"
|
<img src="{{ url_for('static', filename='navbar/discord-icon.svg') }}" alt="Discord"
|
||||||
class="discord-icon">
|
class="discord-icon">
|
||||||
<img src="{{ url_for('static', filename='navbar/discord-hover.svg') }}" alt="Discord Hover"
|
<img src="{{ url_for('static', filename='navbar/discord-hover.svg') }}" alt="Discord Hover"
|
||||||
class="discord-hover-icon">
|
class="discord-hover-icon">
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url_for('auth.login') }}"><button class="login-btn">ВОЙТИ</button></a>
|
<button class="login-btn" onclick="openRegisterModal()">ВОЙТИ</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/navbar.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/navbar.js') }}"></script>
|
||||||
</body>
|
<script>
|
||||||
|
window.recaptchaSiteKey = "{{ recaptcha_key }}";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,13 +1,13 @@
|
|||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
<button class="swipe-button left-swipe-button"><img src="{{ url_for('static', filename='/icons/left-arrow.svg') }}"></button>
|
<button class="swipe-button left-swipe-button"><img src="{{ url_for('static', filename='/icons/left-arrow.svg') }}"></button>
|
||||||
<div class="page-buttons-container">
|
<div class="page-buttons-container">
|
||||||
<button class="page-button">1</button>
|
<button class="page-button">1</button>
|
||||||
<button class="page-button">2</button>
|
<button class="page-button">2</button>
|
||||||
<button class="page-button">3</button>
|
<button class="page-button">3</button>
|
||||||
<button class="page-button">4</button>
|
<button class="page-button">4</button>
|
||||||
<button class="page-button">5</button>
|
<button class="page-button">5</button>
|
||||||
<button class="page-button">10</button>
|
<button class="page-button">10</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="swipe-button right-swipe-button"><img src="{{ url_for('static', filename='/icons/right-arrow.svg') }}"></button>
|
<button class="swipe-button right-swipe-button"><img src="{{ url_for('static', filename='/icons/right-arrow.svg') }}"></button>
|
||||||
</div>
|
</div>
|
35
templates/register-modal.html
Normal file
35
templates/register-modal.html
Normal 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>
|
@ -1,28 +1,54 @@
|
|||||||
{% extends "auth.html" %}
|
<!DOCTYPE html>
|
||||||
{% block title %}🫐register - artberry🫐{% endblock %}
|
<html lang="ru">
|
||||||
{% set action = 'auth.register' %}
|
<head>
|
||||||
{% block content %}
|
<meta charset="UTF-8">
|
||||||
<form method="POST" action="{{ url_for(action) }}">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{{ form.hidden_tag() }}
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@500&display=swap" rel="stylesheet">
|
||||||
<label for="{{ form.username.id }}">{{ form.username.label.text }}</label>
|
</head>
|
||||||
{{ form.username(class="input-field", placeholder="Enter username") }}<br>
|
<body>
|
||||||
|
<div id="registerModal" class="modal active">
|
||||||
<label for="{{ form.password.id }}">{{ form.password.label.text }}</label>
|
<form method="POST" action="{{ url_for('auth.register') }}">
|
||||||
{{ form.password(class="input-field", placeholder="Enter password") }}<br>
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="form-inner-container">
|
||||||
<label for="{{ form.confirm_password.id }}">{{ form.confirm_password.label.text }}</label>
|
<p class="reg-form-title">РЕГИСТРАЦИЯ</p>
|
||||||
{{ form.confirm_password(class="input-field", placeholder="Repeat password") }}<br>
|
|
||||||
|
<div class="modal-register-input-container">
|
||||||
<div class="recaptcha-container">
|
{{ form.username(class_="modal-register-text-input", placeholder="Имя пользователя") }}
|
||||||
{{ form.recaptcha.label }}
|
</div>
|
||||||
{{ form.recaptcha() }}<br>
|
|
||||||
|
<div class="modal-register-input-container">
|
||||||
|
{{ form.password(class_="modal-register-text-input password-input", placeholder="Пароль") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-register-input-container">
|
||||||
|
{{ form.confirm_password(class_="modal-register-text-input password-input", placeholder="Повтори пароль") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-link-container">
|
||||||
|
<span class="login-prompt">
|
||||||
|
Уже есть аккаунт?
|
||||||
|
<a href="#" class="login-link" data-action="open-login">Войди тут!</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recaptcha-wrapper">
|
||||||
|
{{ form.recaptcha() }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="modal-register-button">
|
||||||
|
<span class="modal-register-button-text">Зарегестрироваться</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</body>
|
||||||
{{ form.submit(class="login-button button", value="Register") }}
|
<script>
|
||||||
</form>
|
function openLoginModal() {
|
||||||
<div class="link-container">
|
document.getElementById('registerModal').classList.remove('active');
|
||||||
<span class="link-text">Already have an account?</span>
|
document.getElementById('loginModal').classList.add('active');
|
||||||
<a href="{{ url_for('auth.login') }}" class="button">Login</a>
|
}
|
||||||
</div>
|
|
||||||
{% endblock %}
|
</script>
|
||||||
|
|
||||||
|
</html>
|
@ -1,51 +1,51 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subnav-container {
|
.subnav-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #3C3882;
|
background-color: #3C3882;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 112px;
|
top: 112px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background-color: #3C3882;
|
background-color: #3C3882;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #8784C9;
|
color: #8784C9;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:not(:last-child) {
|
.button:not(:last-child) {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="subnav-container">
|
<div class="subnav-container">
|
||||||
<button class="button">ТЕГИ</button>
|
<button class="button">ТЕГИ</button>
|
||||||
<button class="button">КАТЕГОРИИ</button>
|
<button class="button">КАТЕГОРИИ</button>
|
||||||
<button class="button">ПЕРСОНАЖИ</button>
|
<button class="button">ПЕРСОНАЖИ</button>
|
||||||
<button class="button">КОЛЛЕКЦИИ</button>
|
<button class="button">КОЛЛЕКЦИИ</button>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,21 +1,21 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<body>
|
<body>
|
||||||
<div class="tags-container">
|
<div class="tags-container">
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
<button class="tag">{{ tag }}</button>
|
<button class="tag">{{ tag }}</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button class="list-button">
|
<button class="list-button">
|
||||||
<img src="{{ url_for('static', filename='icons/list-icon.svg') }}" alt="List Icon">
|
<img src="{{ url_for('static', filename='icons/list-icon.svg') }}" alt="List Icon">
|
||||||
</button>
|
</button>
|
||||||
<div class="taglist-shadow"></div>
|
<div class="taglist-shadow"></div>
|
||||||
</div>
|
</div>
|
||||||
<script src="{{ url_for('static', filename='js/taglist.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/taglist.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</head>
|
</head>
|
||||||
</html>
|
</html>
|
@ -1,129 +1,129 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
</head>
|
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}" type="image/svg+xml">
|
||||||
<body>
|
</head>
|
||||||
{% include 'navbar.html' %}
|
<body>
|
||||||
{% include 'subnav.html' %}
|
{% include 'navbar.html' %}
|
||||||
{% include 'tags_list.html' %}
|
{% include 'subnav.html' %}
|
||||||
<div class="container new-content">
|
{% include 'tags_list.html' %}
|
||||||
<a class="new-content-text">НОВИНКИ</a>
|
<div class="container new-content">
|
||||||
<div class="small-cards-grid">
|
<a class="new-content-text">НОВИНКИ</a>
|
||||||
{% for video_data in videos[:5] %}
|
<div class="small-cards-grid">
|
||||||
<div class="small-card">
|
{% for video_data in videos[:5] %}
|
||||||
<div class="small-card-cover">
|
<div class="small-card">
|
||||||
<img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail">
|
<div class="small-card-cover">
|
||||||
<video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}">
|
<img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail">
|
||||||
<source type="video/mp4">
|
<video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}">
|
||||||
Your browser does not support the video tag.
|
<source type="video/mp4">
|
||||||
</video>
|
Your browser does not support the video tag.
|
||||||
</div>
|
</video>
|
||||||
<div class="small-card-info">
|
</div>
|
||||||
<div class="small-card-header">
|
<div class="small-card-info">
|
||||||
<span class="small-card-text">{{ video_data.video.username }}</span>
|
<div class="small-card-header">
|
||||||
<div class="small-card-stats">
|
<span class="small-card-text">{{ video_data.video.username }}</span>
|
||||||
<div class="small-stat">
|
<div class="small-card-stats">
|
||||||
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
<div class="small-stat">
|
||||||
<span class="small-card-text">{{ video_data.video.cookie_votes }}</span>
|
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
||||||
</div>
|
<span class="small-card-text">{{ video_data.video.cookie_votes }}</span>
|
||||||
<div class="small-stat">
|
</div>
|
||||||
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
<div class="small-stat">
|
||||||
<span class="small-card-text">{{ video_data.views_count }}</span>
|
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
||||||
</div>
|
<span class="small-card-text">{{ video_data.views_count }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="small-card-text">{{ video_data.video.video_name }}</p>
|
</div>
|
||||||
</div>
|
<p class="small-card-text">{{ video_data.video.video_name }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
</div>
|
||||||
</div>
|
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
||||||
<div class="container popular-content">
|
</div>
|
||||||
<a class="popular-content-text">ПОПУЛЯРНОЕ</a>
|
<div class="container popular-content">
|
||||||
<div class="cards-grid">
|
<a class="popular-content-text">ПОПУЛЯРНОЕ</a>
|
||||||
{% for video_data in popular_videos[:8] %}
|
<div class="cards-grid">
|
||||||
<div class="card">
|
{% for video_data in popular_videos[:8] %}
|
||||||
<div class="card-cover">
|
<div class="card">
|
||||||
<img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail">
|
<div class="card-cover">
|
||||||
<video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}">
|
<img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail">
|
||||||
<source type="video/mp4">
|
<video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}">
|
||||||
Your browser does not support the video tag.
|
<source type="video/mp4">
|
||||||
</video>
|
Your browser does not support the video tag.
|
||||||
</div>
|
</video>
|
||||||
<div class="card-info">
|
</div>
|
||||||
<div class="card-header">
|
<div class="card-info">
|
||||||
<span class="card-title" style="color: #3C3882;">{{ video_data.video.username }}</span>
|
<div class="card-header">
|
||||||
<div class="card-stats">
|
<span class="card-title" style="color: #3C3882;">{{ video_data.video.username }}</span>
|
||||||
<div class="stat">
|
<div class="card-stats">
|
||||||
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
<div class="stat">
|
||||||
<span style="color: #8784C9;">{{ video_data.video.cookie_votes }}</span>
|
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
||||||
</div>
|
<span style="color: #8784C9;">{{ video_data.video.cookie_votes }}</span>
|
||||||
<div class="stat">
|
</div>
|
||||||
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
<div class="stat">
|
||||||
<span style="color: #8784C9;">{{ video_data.views_count }}</span>
|
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
||||||
</div>
|
<span style="color: #8784C9;">{{ video_data.views_count }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text" style="font-family: Nunito, sans-serif;">{{ video_data.video.video_name }}</p>
|
</div>
|
||||||
</div>
|
<p class="card-text" style="font-family: Nunito, sans-serif;">{{ video_data.video.video_name }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
</div>
|
||||||
</div>
|
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
||||||
|
</div>
|
||||||
<div class="container viewed-content">
|
|
||||||
<a class="viewed-content-text">ПРОСМАТРИВАЕМОЕ</a>
|
<div class="container viewed-content">
|
||||||
<div class="cards-grid">
|
<a class="viewed-content-text">ПРОСМАТРИВАЕМОЕ</a>
|
||||||
{% for video_data in most_viewed_videos[:8] %}
|
<div class="cards-grid">
|
||||||
<div class="card">
|
{% for video_data in most_viewed_videos[:8] %}
|
||||||
<div class="card-cover">
|
<div class="card">
|
||||||
<img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail">
|
<div class="card-cover">
|
||||||
<video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}">
|
<img src="{{ url_for('static', filename='thumbnails/' + video_data.video.video_thumbnail_file) }}" alt="Thumbnail" class="thumbnail">
|
||||||
<source type="video/mp4">
|
<video class="preview-video" muted preload="none" data-src="{{ url_for('static', filename='videos/' + video_data.video.video_file) }}">
|
||||||
Your browser does not support the video tag.
|
<source type="video/mp4">
|
||||||
</video>
|
Your browser does not support the video tag.
|
||||||
</div>
|
</video>
|
||||||
<div class="card-info">
|
</div>
|
||||||
<div class="card-header">
|
<div class="card-info">
|
||||||
<span class="card-title" style="color: #3C3882;">{{ video_data.video.username }}</span>
|
<div class="card-header">
|
||||||
<div class="card-stats">
|
<span class="card-title" style="color: #3C3882;">{{ video_data.video.username }}</span>
|
||||||
<div class="stat">
|
<div class="card-stats">
|
||||||
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
<div class="stat">
|
||||||
<span style="color: #8784C9;">{{ video_data.video.cookie_votes }}</span>
|
<img src="{{ url_for('static', filename='card/like-icon.svg') }}" alt="Лайк" width="20" height="20">
|
||||||
</div>
|
<span style="color: #8784C9;">{{ video_data.video.cookie_votes }}</span>
|
||||||
<div class="stat">
|
</div>
|
||||||
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
<div class="stat">
|
||||||
<span style="color: #8784C9;">{{ video_data.views_count }}</span>
|
<img src="{{ url_for('static', filename='card/views-icon.svg') }}" alt="Просмотры" width="20" height="20">
|
||||||
</div>
|
<span style="color: #8784C9;">{{ video_data.views_count }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text" style="font-family: Nunito, sans-serif;">{{ video_data.video.video_name }}</p>
|
</div>
|
||||||
</div>
|
<p class="card-text" style="font-family: Nunito, sans-serif;">{{ video_data.video.video_name }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
</div>
|
||||||
</div>
|
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
||||||
|
</div>
|
||||||
<div class="container popular-categories">
|
|
||||||
<a class="popular-categories-text">ПОПУЛЯРНЫЕ КАТЕГОРИИ</a>
|
<div class="container popular-categories">
|
||||||
{% for i in range(6) %}
|
<a class="popular-categories-text">ПОПУЛЯРНЫЕ КАТЕГОРИИ</a>
|
||||||
<div class="pc-card">
|
{% for i in range(6) %}
|
||||||
<div class="pc-card-cover"></div>
|
<div class="pc-card">
|
||||||
<div class="pc-card-info">
|
<div class="pc-card-cover"></div>
|
||||||
<div class="pc-card-stats">
|
<div class="pc-card-info">
|
||||||
<span class="category-name-text">Category</span>
|
<div class="pc-card-stats">
|
||||||
<span class="categories-number" style="--length: 4;">14655</span>
|
<span class="category-name-text">Category</span>
|
||||||
</div>
|
<span class="categories-number" style="--length: 4;">14655</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
{% endfor %}
|
||||||
</div>
|
<button class="view-more-button"><span class="new-context-button-text">Смотреть Больше</span></button>
|
||||||
<script src="{{ url_for('static', filename='js/hoverPreview.js') }}"></script>
|
</div>
|
||||||
<script src="{{ url_for('static', filename='js/adjustScrollbar.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/hoverPreview.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,132 +1,132 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>🫐Content View - Artberry🫐</title>
|
<title>🫐Content View - Artberry🫐</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
<link rel="icon" href="{{ url_for('static', filename='artberry.ico') }}" type="image/x-icon">
|
<link rel="icon" href="{{ url_for('static', filename='artberry.ico') }}" type="image/x-icon">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% include 'navbar.html' %}
|
{% include 'navbar.html' %}
|
||||||
|
|
||||||
{% if content_type == 'art' %}
|
{% if content_type == 'art' %}
|
||||||
<h1>Image</h1>
|
<h1>Image</h1>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<img src="{{ url_for('static', filename='arts/' + content.image_file) }}" alt="Art Image">
|
<img src="{{ url_for('static', filename='arts/' + content.image_file) }}" alt="Art Image">
|
||||||
<p><strong>Author:</strong> <a href="{{ url_for('profile', username=content.username) }}">{{ content.username }}</a></p>
|
<p><strong>Author:</strong> <a href="{{ url_for('profile', username=content.username) }}">{{ content.username }}</a></p>
|
||||||
<p><strong>Publication Date:</strong> {{ content.publication_date }}</p>
|
<p><strong>Publication Date:</strong> {{ content.publication_date }}</p>
|
||||||
<p><strong>Tags:</strong>
|
<p><strong>Tags:</strong>
|
||||||
{% for tag in content.tags.split(',') %}
|
{% for tag in content.tags.split(',') %}
|
||||||
<a href="{{ url_for('index', search=tag.strip()) }}" class="tag-link">{{ tag.strip() }}</a>{% if not loop.last %}, {% endif %}
|
<a href="{{ url_for('index', search=tag.strip()) }}" class="tag-link">{{ tag.strip() }}</a>{% if not loop.last %}, {% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% elif content_type == 'video' %}
|
{% elif content_type == 'video' %}
|
||||||
<h1>Video</h1>
|
<h1>Video</h1>
|
||||||
<div class="video-details">
|
<div class="video-details">
|
||||||
<video controls>
|
<video controls>
|
||||||
<source src="{{ url_for('static', filename='videos/' + content.video_file) }}" type="video/mp4">
|
<source src="{{ url_for('static', filename='videos/' + content.video_file) }}" type="video/mp4">
|
||||||
</video>
|
</video>
|
||||||
<p><strong>Author:</strong> <a href="{{ url_for('profile', username=content.username) }}">{{ content.username }}</a></p>
|
<p><strong>Author:</strong> <a href="{{ url_for('profile', username=content.username) }}">{{ content.username }}</a></p>
|
||||||
<p><strong>Publication Date:</strong> {{ content.publication_date }}</p>
|
<p><strong>Publication Date:</strong> {{ content.publication_date }}</p>
|
||||||
<p>Description: {{ content.description }}</p>
|
<p>Description: {{ content.description }}</p>
|
||||||
<p><strong>Tags:</strong>
|
<p><strong>Tags:</strong>
|
||||||
{% for tag in content.tags.split(',') %}
|
{% for tag in content.tags.split(',') %}
|
||||||
<a href="{{ url_for('videos', search=tag.strip()) }}" class="tag-link">{{ tag.strip() }}</a>{% if not loop.last %}, {% endif %}
|
<a href="{{ url_for('videos', search=tag.strip()) }}" class="tag-link">{{ tag.strip() }}</a>{% if not loop.last %}, {% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% elif content_type == 'comic' %}
|
{% elif content_type == 'comic' %}
|
||||||
<h1>{{ content.name }}</h1>
|
<h1>{{ content.name }}</h1>
|
||||||
<div class="comic-pages">
|
<div class="comic-pages">
|
||||||
{% if comic_pages %}
|
{% if comic_pages %}
|
||||||
{% for page in comic_pages %}
|
{% for page in comic_pages %}
|
||||||
<img src="{{ url_for('static', filename=page.file_path.replace('static/', '').replace('\\', '/')) }}" alt="Page {{ page.page_number }}">
|
<img src="{{ url_for('static', filename=page.file_path.replace('static/', '').replace('\\', '/')) }}" alt="Page {{ page.page_number }}">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No pages available for this comic.</p>
|
<p>No pages available for this comic.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if current_user.is_authenticated and current_user.username == content.username %}
|
{% if current_user.is_authenticated and current_user.username == content.username %}
|
||||||
<form method="POST" action="{{ url_for('delete', content_type=content_type, content_id=content.id) }}">
|
<form method="POST" action="{{ url_for('delete', content_type=content_type, content_id=content.id) }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="button" onclick="return confirm('Are you sure you want to delete this content?');">Delete</button>
|
<button type="submit" class="button" onclick="return confirm('Are you sure you want to delete this content?');">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if content_type != 'comic' %}
|
{% if content_type != 'comic' %}
|
||||||
<section class="comments">
|
<section class="comments">
|
||||||
<h2>Comments</h2>
|
<h2>Comments</h2>
|
||||||
<div class="comments-list">
|
<div class="comments-list">
|
||||||
{% for comment in comments %}
|
{% for comment in comments %}
|
||||||
<div class="comment">
|
<div class="comment">
|
||||||
<a href="{{ url_for('profile', username=comment.username) }}">
|
<a href="{{ url_for('profile', username=comment.username) }}">
|
||||||
<img src="{{ url_for('static', filename='avatars/' + (avatars[comment.username] if avatars.get(comment.username) else 'default_avatar.png')) }}"
|
<img src="{{ url_for('static', filename='avatars/' + (avatars[comment.username] if avatars.get(comment.username) else 'default_avatar.png')) }}"
|
||||||
alt="Avatar of {{ comment.username }}" class="avatar">
|
alt="Avatar of {{ comment.username }}" class="avatar">
|
||||||
{% if current_user.is_authenticated and comment.username == current_user.username %}
|
{% if current_user.is_authenticated and comment.username == current_user.username %}
|
||||||
<form class="button" action="{{ url_for('delete_comment', comment_id=comment.id) }}" method="POST" style="display:inline;">
|
<form class="button" action="{{ url_for('delete_comment', comment_id=comment.id) }}" method="POST" style="display:inline;">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="delete-button" onclick="return confirm('Are you sure you want to delete this comment?')">Delete</button>
|
<button type="submit" class="delete-button" onclick="return confirm('Are you sure you want to delete this comment?')">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('profile', username=comment.username) }}" class="username-link">
|
<a href="{{ url_for('profile', username=comment.username) }}" class="username-link">
|
||||||
<strong>{{ comment.username }}</strong>
|
<strong>{{ comment.username }}</strong>
|
||||||
</a>
|
</a>
|
||||||
({{ comment.comment_date.strftime('%Y-%m-%d %H:%M') }}):
|
({{ comment.comment_date.strftime('%Y-%m-%d %H:%M') }}):
|
||||||
</p>
|
</p>
|
||||||
<p style="margin-top: 10px;">{{ comment.comment_text }}</p>
|
<p style="margin-top: 10px;">{{ comment.comment_text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No comments yet. Be the first to comment!</p>
|
<p>No comments yet. Be the first to comment!</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<form method="POST" action="{{ url_for('view', content_type=content_type, id=content.id) }}" class="comment-form">
|
<form method="POST" action="{{ url_for('view', content_type=content_type, id=content.id) }}" class="comment-form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<textarea name="comment" class="input-field" placeholder="Add a comment..." rows="3" maxlength="44" required></textarea>
|
<textarea name="comment" class="input-field" placeholder="Add a comment..." rows="3" maxlength="44" required></textarea>
|
||||||
<button type="submit" class="button">Post Comment</button>
|
<button type="submit" class="button">Post Comment</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to post a comment.</p>
|
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to post a comment.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if content_type != 'comic' %}
|
{% if content_type != 'comic' %}
|
||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
<a href="{{ url_for('view', content_type=content_type, id=prev_content.id, page=request.args.get('page', 1)) }}" class="button">← Prev</a>
|
<a href="{{ url_for('view', content_type=content_type, id=prev_content.id, page=request.args.get('page', 1)) }}" class="button">← Prev</a>
|
||||||
<a href="{{ url_for('view', content_type=content_type, id=random_content.id, page=request.args.get('page', 1)) }}" class="button">Random</a>
|
<a href="{{ url_for('view', content_type=content_type, id=random_content.id, page=request.args.get('page', 1)) }}" class="button">Random</a>
|
||||||
<a href="{{ url_for('view', content_type=content_type, id=next_content.id, page=request.args.get('page', 1)) }}" class="button">Next →</a>
|
<a href="{{ url_for('view', content_type=content_type, id=next_content.id, page=request.args.get('page', 1)) }}" class="button">Next →</a>
|
||||||
|
|
||||||
{% if current_user.is_authenticated and content.username == current_user.username %}
|
{% if current_user.is_authenticated and content.username == current_user.username %}
|
||||||
{% if content_type == 'art' %}
|
{% if content_type == 'art' %}
|
||||||
<a href="{{ url_for('image_edit', id=content.id) }}" class="button">Edit Art</a>
|
<a href="{{ url_for('image_edit', id=content.id) }}" class="button">Edit Art</a>
|
||||||
{% elif content_type == 'video' %}
|
{% elif content_type == 'video' %}
|
||||||
<a href="{{ url_for('video_edit', id=content.id) }}" class="button">Edit Video</a>
|
<a href="{{ url_for('video_edit', id=content.id) }}" class="button">Edit Video</a>
|
||||||
{% elif content_type == 'comic' %}
|
{% elif content_type == 'comic' %}
|
||||||
<a href="{{ url_for('comic_edit', id=content.id) }}" class="button">Edit Comic</a>
|
<a href="{{ url_for('comic_edit', id=content.id) }}" class="button">Edit Comic</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="vote-section">
|
<div class="vote-section">
|
||||||
<p>Votes: {{ content.cookie_votes }} 🍪</p>
|
<p>Votes: {{ content.cookie_votes }} 🍪</p>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<form action="{{ url_for('vote_' + content_type, **({'image_id': content.id} if content_type == 'art' else {'video_id': content.id} if content_type == 'video' else {'comic_id': content.id})) }}" method="POST">
|
<form action="{{ url_for('vote_' + content_type, **({'image_id': content.id} if content_type == 'art' else {'video_id': content.id} if content_type == 'video' else {'comic_id': content.id})) }}" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="button">Vote</button>
|
<button type="submit" class="button">Vote</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to vote.</p>
|
<p>You need to <a href="{{ url_for('auth.login') }}">log in</a> to vote.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
396
upload.py
396
upload.py
@ -1,199 +1,199 @@
|
|||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import aiofiles
|
import aiofiles
|
||||||
from flask import Blueprint, render_template, redirect, url_for, request, current_app
|
from flask import Blueprint, render_template, redirect, url_for, request, current_app
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from models import db, Image, Video, Comic, ComicPage, Post, Cookies, UploadForm, UploadVideoForm, UploadComicForm
|
from models import db, Image, Video, Comic, ComicPage, Post, Cookies, UploadForm, UploadVideoForm, UploadComicForm
|
||||||
from utils import allowed_file, check_file_content, check_file_size, convert_to_webp, generate_unique_filename
|
from utils import allowed_file, check_file_content, check_file_size, convert_to_webp, generate_unique_filename
|
||||||
|
|
||||||
upload_bp = Blueprint('upload', __name__)
|
upload_bp = Blueprint('upload', __name__)
|
||||||
|
|
||||||
@upload_bp.route('/upload', methods=['GET', 'POST'])
|
@upload_bp.route('/upload', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
async def upload():
|
async def upload():
|
||||||
form = UploadForm()
|
form = UploadForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
image_file = form.image_file.data
|
image_file = form.image_file.data
|
||||||
tags = form.tags.data
|
tags = form.tags.data
|
||||||
allowed_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
|
allowed_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
|
||||||
|
|
||||||
if not (allowed_file(image_file.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
|
if not (allowed_file(image_file.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
|
||||||
check_file_content(image_file, allowed_mime_types) and
|
check_file_content(image_file, allowed_mime_types) and
|
||||||
await check_file_size(image_file, current_app.config['MAX_IMAGE_SIZE'])):
|
await check_file_size(image_file, current_app.config['MAX_IMAGE_SIZE'])):
|
||||||
return redirect(url_for('upload.upload'))
|
return redirect(url_for('upload.upload'))
|
||||||
|
|
||||||
unique_filename = f"{uuid.uuid4().hex}.webp"
|
unique_filename = f"{uuid.uuid4().hex}.webp"
|
||||||
filepath = os.path.join(current_app.config['UPLOAD_FOLDER']['images'], unique_filename)
|
filepath = os.path.join(current_app.config['UPLOAD_FOLDER']['images'], unique_filename)
|
||||||
if os.path.exists(filepath):
|
if os.path.exists(filepath):
|
||||||
return redirect(url_for('upload.upload'))
|
return redirect(url_for('upload.upload'))
|
||||||
|
|
||||||
webp_image = await convert_to_webp(image_file)
|
webp_image = await convert_to_webp(image_file)
|
||||||
async with aiofiles.open(filepath, 'wb') as f:
|
async with aiofiles.open(filepath, 'wb') as f:
|
||||||
await f.write(webp_image.read())
|
await f.write(webp_image.read())
|
||||||
|
|
||||||
img = Image(image_file=unique_filename, username=current_user.username, tags=tags, cookie_votes=0)
|
img = Image(image_file=unique_filename, username=current_user.username, tags=tags, cookie_votes=0)
|
||||||
db.session.add(img)
|
db.session.add(img)
|
||||||
|
|
||||||
user_cookie = Cookies.query.filter_by(username=current_user.username).first()
|
user_cookie = Cookies.query.filter_by(username=current_user.username).first()
|
||||||
if user_cookie:
|
if user_cookie:
|
||||||
user_cookie.cookies += 1
|
user_cookie.cookies += 1
|
||||||
else:
|
else:
|
||||||
user_cookie = Cookies(username=current_user.username, cookies=1)
|
user_cookie = Cookies(username=current_user.username, cookies=1)
|
||||||
db.session.add(user_cookie)
|
db.session.add(user_cookie)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
return render_template('upload.html', form=form)
|
return render_template('upload.html', form=form)
|
||||||
|
|
||||||
@upload_bp.route('/upload_video', methods=['GET', 'POST'])
|
@upload_bp.route('/upload_video', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
async def upload_video():
|
async def upload_video():
|
||||||
form = UploadVideoForm()
|
form = UploadVideoForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
video_file = form.video_file.data
|
video_file = form.video_file.data
|
||||||
video_thumbnail = form.thumbnail.data
|
video_thumbnail = form.thumbnail.data
|
||||||
video_name = form.name.data
|
video_name = form.name.data
|
||||||
tags = form.tags.data
|
tags = form.tags.data
|
||||||
description = form.description.data
|
description = form.description.data
|
||||||
|
|
||||||
allowed_video_mime_types = {'video/mp4', 'video/x-msvideo', 'video/quicktime'}
|
allowed_video_mime_types = {'video/mp4', 'video/x-msvideo', 'video/quicktime'}
|
||||||
allowed_image_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
|
allowed_image_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
|
||||||
|
|
||||||
if video_file and video_thumbnail:
|
if video_file and video_thumbnail:
|
||||||
if not (allowed_file(video_file.filename, current_app.config['ALLOWED_VIDEO_EXTENSIONS']) and
|
if not (allowed_file(video_file.filename, current_app.config['ALLOWED_VIDEO_EXTENSIONS']) and
|
||||||
check_file_content(video_file, allowed_video_mime_types)):
|
check_file_content(video_file, allowed_video_mime_types)):
|
||||||
return redirect(url_for('upload.upload_video'))
|
return redirect(url_for('upload.upload_video'))
|
||||||
if not await check_file_size(video_file, current_app.config['MAX_VIDEO_SIZE']):
|
if not await check_file_size(video_file, current_app.config['MAX_VIDEO_SIZE']):
|
||||||
return redirect(url_for('upload.upload_video'))
|
return redirect(url_for('upload.upload_video'))
|
||||||
if not (allowed_file(video_thumbnail.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
|
if not (allowed_file(video_thumbnail.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
|
||||||
check_file_content(video_thumbnail, allowed_image_mime_types)):
|
check_file_content(video_thumbnail, allowed_image_mime_types)):
|
||||||
return redirect(url_for('upload.upload_video'))
|
return redirect(url_for('upload.upload_video'))
|
||||||
|
|
||||||
video_filename = await generate_unique_filename(current_app.config['UPLOAD_FOLDER']['videos'], 'mp4')
|
video_filename = await generate_unique_filename(current_app.config['UPLOAD_FOLDER']['videos'], 'mp4')
|
||||||
thumbnail_filename = f"{uuid.uuid4().hex}.webp"
|
thumbnail_filename = f"{uuid.uuid4().hex}.webp"
|
||||||
|
|
||||||
video_path = os.path.join(current_app.config['UPLOAD_FOLDER']['videos'], video_filename)
|
video_path = os.path.join(current_app.config['UPLOAD_FOLDER']['videos'], video_filename)
|
||||||
thumbnail_path = os.path.join(current_app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename)
|
thumbnail_path = os.path.join(current_app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename)
|
||||||
|
|
||||||
async with aiofiles.open(video_path, 'wb') as f:
|
async with aiofiles.open(video_path, 'wb') as f:
|
||||||
await f.write(video_file.read())
|
await f.write(video_file.read())
|
||||||
|
|
||||||
webp_thumbnail = await convert_to_webp(video_thumbnail)
|
webp_thumbnail = await convert_to_webp(video_thumbnail)
|
||||||
async with aiofiles.open(thumbnail_path, 'wb') as f:
|
async with aiofiles.open(thumbnail_path, 'wb') as f:
|
||||||
await f.write(webp_thumbnail.read())
|
await f.write(webp_thumbnail.read())
|
||||||
|
|
||||||
video = Video(
|
video = Video(
|
||||||
video_file=video_filename,
|
video_file=video_filename,
|
||||||
video_name=video_name,
|
video_name=video_name,
|
||||||
video_thumbnail_file=thumbnail_filename,
|
video_thumbnail_file=thumbnail_filename,
|
||||||
username=current_user.username,
|
username=current_user.username,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
description=description
|
description=description
|
||||||
)
|
)
|
||||||
db.session.add(video)
|
db.session.add(video)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(url_for('videos'))
|
return redirect(url_for('videos'))
|
||||||
|
|
||||||
return render_template('upload_video.html', form=form)
|
return render_template('upload_video.html', form=form)
|
||||||
|
|
||||||
@upload_bp.route('/comic_upload', methods=['GET', 'POST'])
|
@upload_bp.route('/comic_upload', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
async def comic_upload():
|
async def comic_upload():
|
||||||
form = UploadComicForm()
|
form = UploadComicForm()
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
ct = form.thumbnail.data
|
ct = form.thumbnail.data
|
||||||
n = form.title.data
|
n = form.title.data
|
||||||
tags = form.tags.data
|
tags = form.tags.data
|
||||||
|
|
||||||
allowed_image_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
|
allowed_image_mime_types = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
|
||||||
|
|
||||||
if db.session.execute(db.select(Comic).filter_by(name=n)).scalar():
|
if db.session.execute(db.select(Comic).filter_by(name=n)).scalar():
|
||||||
return render_template('comic_upload.html', form=form)
|
return render_template('comic_upload.html', form=form)
|
||||||
|
|
||||||
if ct:
|
if ct:
|
||||||
if not (allowed_file(ct.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
|
if not (allowed_file(ct.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
|
||||||
check_file_content(ct, allowed_image_mime_types)):
|
check_file_content(ct, allowed_image_mime_types)):
|
||||||
return redirect(url_for('upload.comic_upload'))
|
return redirect(url_for('upload.comic_upload'))
|
||||||
|
|
||||||
tf = f"{uuid.uuid4().hex}.webp"
|
tf = f"{uuid.uuid4().hex}.webp"
|
||||||
tp = os.path.join(current_app.config['UPLOAD_FOLDER']['comicthumbs'], tf)
|
tp = os.path.join(current_app.config['UPLOAD_FOLDER']['comicthumbs'], tf)
|
||||||
webp_thumbnail = await convert_to_webp(ct)
|
webp_thumbnail = await convert_to_webp(ct)
|
||||||
async with aiofiles.open(tp, 'wb') as f:
|
async with aiofiles.open(tp, 'wb') as f:
|
||||||
await f.write(webp_thumbnail.read())
|
await f.write(webp_thumbnail.read())
|
||||||
|
|
||||||
cf = os.path.join(current_app.config['UPLOAD_FOLDER']['comics'], n)
|
cf = os.path.join(current_app.config['UPLOAD_FOLDER']['comics'], n)
|
||||||
os.makedirs(cf, exist_ok=True)
|
os.makedirs(cf, exist_ok=True)
|
||||||
|
|
||||||
new_comic = Comic(
|
new_comic = Comic(
|
||||||
comic_folder=n,
|
comic_folder=n,
|
||||||
comic_thumbnail_file=tf,
|
comic_thumbnail_file=tf,
|
||||||
username=current_user.username,
|
username=current_user.username,
|
||||||
name=n,
|
name=n,
|
||||||
tags=tags
|
tags=tags
|
||||||
)
|
)
|
||||||
db.session.add(new_comic)
|
db.session.add(new_comic)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
async def save_pages():
|
async def save_pages():
|
||||||
pages = request.files.getlist('pages[]')
|
pages = request.files.getlist('pages[]')
|
||||||
for i, p in enumerate(sorted(pages, key=lambda x: x.filename), start=1):
|
for i, p in enumerate(sorted(pages, key=lambda x: x.filename), start=1):
|
||||||
if p:
|
if p:
|
||||||
if not (allowed_file(p.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
|
if not (allowed_file(p.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']) and
|
||||||
check_file_content(p, allowed_image_mime_types)):
|
check_file_content(p, allowed_image_mime_types)):
|
||||||
return redirect(url_for('upload.comic_upload'))
|
return redirect(url_for('upload.comic_upload'))
|
||||||
|
|
||||||
filename = f"{uuid.uuid4().hex}.webp"
|
filename = f"{uuid.uuid4().hex}.webp"
|
||||||
file_path = os.path.join(cf, filename)
|
file_path = os.path.join(cf, filename)
|
||||||
webp_page = await convert_to_webp(p)
|
webp_page = await convert_to_webp(p)
|
||||||
async with aiofiles.open(file_path, 'wb') as f:
|
async with aiofiles.open(file_path, 'wb') as f:
|
||||||
await f.write(webp_page.read())
|
await f.write(webp_page.read())
|
||||||
|
|
||||||
new_page = ComicPage(comic_id=new_comic.id, page_number=i, file_path=file_path)
|
new_page = ComicPage(comic_id=new_comic.id, page_number=i, file_path=file_path)
|
||||||
db.session.add(new_page)
|
db.session.add(new_page)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
await save_pages()
|
await save_pages()
|
||||||
return redirect(url_for('comics'))
|
return redirect(url_for('comics'))
|
||||||
else:
|
else:
|
||||||
for field, errors in form.errors.items():
|
for field, errors in form.errors.items():
|
||||||
for error in errors:
|
for error in errors:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return render_template('comic_upload.html', form=form)
|
return render_template('comic_upload.html', form=form)
|
||||||
|
|
||||||
@upload_bp.route('/upload_post', methods=['GET', 'POST'])
|
@upload_bp.route('/upload_post', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
async def upload_post():
|
async def upload_post():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
post_text = request.form.get('post_text')
|
post_text = request.form.get('post_text')
|
||||||
post_media = request.files.get('post_media')
|
post_media = request.files.get('post_media')
|
||||||
|
|
||||||
if post_text:
|
if post_text:
|
||||||
new_post = Post(
|
new_post = Post(
|
||||||
username=current_user.username,
|
username=current_user.username,
|
||||||
text=post_text
|
text=post_text
|
||||||
)
|
)
|
||||||
db.session.add(new_post)
|
db.session.add(new_post)
|
||||||
|
|
||||||
if post_media and allowed_file(post_media.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']):
|
if post_media and allowed_file(post_media.filename, current_app.config['ALLOWED_IMAGE_EXTENSIONS']):
|
||||||
if await check_file_size(post_media, current_app.config['MAX_IMAGE_SIZE']):
|
if await check_file_size(post_media, current_app.config['MAX_IMAGE_SIZE']):
|
||||||
unique_filename = f"{uuid.uuid4().hex}.webp"
|
unique_filename = f"{uuid.uuid4().hex}.webp"
|
||||||
media_path = os.path.join(current_app.config['UPLOAD_FOLDER']['posts'], unique_filename)
|
media_path = os.path.join(current_app.config['UPLOAD_FOLDER']['posts'], unique_filename)
|
||||||
webp_image = await convert_to_webp(post_media)
|
webp_image = await convert_to_webp(post_media)
|
||||||
async with aiofiles.open(media_path, 'wb') as f:
|
async with aiofiles.open(media_path, 'wb') as f:
|
||||||
await f.write(webp_image.read())
|
await f.write(webp_image.read())
|
||||||
new_post.media_file = unique_filename
|
new_post.media_file = unique_filename
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('upload.upload_post'))
|
return redirect(url_for('upload.upload_post'))
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('user_posts', username=current_user.username))
|
return redirect(url_for('user_posts', username=current_user.username))
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('upload.upload_post'))
|
return redirect(url_for('upload.upload_post'))
|
||||||
|
|
||||||
return render_template('upload_post.html')
|
return render_template('upload_post.html')
|
266
utils.py
266
utils.py
@ -1,134 +1,134 @@
|
|||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
import uuid
|
import uuid
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import asyncio
|
import asyncio
|
||||||
import magic
|
import magic
|
||||||
import re
|
import re
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from sqlalchemy import func, or_
|
from sqlalchemy import func, or_
|
||||||
from flask import request
|
from flask import request
|
||||||
from models import db, Comments, Image, Video, Comic, Post, User
|
from models import db, Comments, Image, Video, Comic, Post, User
|
||||||
|
|
||||||
def allowed_file(filename, allowed_extensions):
|
def allowed_file(filename, allowed_extensions):
|
||||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
|
||||||
|
|
||||||
async def check_file_size(file, max_size):
|
async def check_file_size(file, max_size):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
return await loop.run_in_executor(None, _sync_check_file_size, file, max_size)
|
return await loop.run_in_executor(None, _sync_check_file_size, file, max_size)
|
||||||
|
|
||||||
def _sync_check_file_size(file, max_size):
|
def _sync_check_file_size(file, max_size):
|
||||||
file.seek(0, os.SEEK_END)
|
file.seek(0, os.SEEK_END)
|
||||||
file_size = file.tell()
|
file_size = file.tell()
|
||||||
file.seek(0)
|
file.seek(0)
|
||||||
return file_size <= max_size
|
return file_size <= max_size
|
||||||
|
|
||||||
def check_file_content(file, allowed_mime_types):
|
def check_file_content(file, allowed_mime_types):
|
||||||
mime = magic.Magic(mime=True)
|
mime = magic.Magic(mime=True)
|
||||||
file_mime_type = mime.from_buffer(file.read(1024))
|
file_mime_type = mime.from_buffer(file.read(1024))
|
||||||
file.seek(0)
|
file.seek(0)
|
||||||
return file_mime_type in allowed_mime_types
|
return file_mime_type in allowed_mime_types
|
||||||
|
|
||||||
async def convert_to_webp(image_file):
|
async def convert_to_webp(image_file):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
return await loop.run_in_executor(None, _sync_convert_to_webp, image_file)
|
return await loop.run_in_executor(None, _sync_convert_to_webp, image_file)
|
||||||
|
|
||||||
def _sync_convert_to_webp(image_file):
|
def _sync_convert_to_webp(image_file):
|
||||||
with PILImage.open(image_file) as img:
|
with PILImage.open(image_file) as img:
|
||||||
output = io.BytesIO()
|
output = io.BytesIO()
|
||||||
img.convert("RGB").save(output, format="WEBP", quality=90, optimize=True)
|
img.convert("RGB").save(output, format="WEBP", quality=90, optimize=True)
|
||||||
output.seek(0)
|
output.seek(0)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
async def generate_unique_filename(upload_folder, extension):
|
async def generate_unique_filename(upload_folder, extension):
|
||||||
while True:
|
while True:
|
||||||
unique_filename = f"{uuid.uuid4().hex}.{extension}"
|
unique_filename = f"{uuid.uuid4().hex}.{extension}"
|
||||||
file_path = os.path.join(upload_folder, unique_filename)
|
file_path = os.path.join(upload_folder, unique_filename)
|
||||||
if not await aiofiles.os.path.exists(file_path):
|
if not await aiofiles.os.path.exists(file_path):
|
||||||
return unique_filename
|
return unique_filename
|
||||||
|
|
||||||
def update_related_tables(old_username, new_username):
|
def update_related_tables(old_username, new_username):
|
||||||
models_to_update = [Comments, Image, Video, Comic, Post]
|
models_to_update = [Comments, Image, Video, Comic, Post]
|
||||||
for model in models_to_update:
|
for model in models_to_update:
|
||||||
for record in model.query.filter_by(username=old_username).all():
|
for record in model.query.filter_by(username=old_username).all():
|
||||||
record.username = new_username
|
record.username = new_username
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def get_content_query(model, subscriptions, search_query):
|
def get_content_query(model, subscriptions, search_query):
|
||||||
query = model.query
|
query = model.query
|
||||||
|
|
||||||
if search_query:
|
if search_query:
|
||||||
|
|
||||||
tags = [tag.strip().lower() for tag in search_query.replace(',', ' ').split()]
|
tags = [tag.strip().lower() for tag in search_query.replace(',', ' ').split()]
|
||||||
|
|
||||||
filter_condition = [
|
filter_condition = [
|
||||||
model.tags.like(f'%{tag}%') for tag in tags
|
model.tags.like(f'%{tag}%') for tag in tags
|
||||||
]
|
]
|
||||||
|
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(*filter_condition)
|
or_(*filter_condition)
|
||||||
)
|
)
|
||||||
|
|
||||||
if subscriptions:
|
if subscriptions:
|
||||||
query = query.filter(or_(
|
query = query.filter(or_(
|
||||||
model.username.in_(subscriptions),
|
model.username.in_(subscriptions),
|
||||||
model.username.notin_(subscriptions)
|
model.username.notin_(subscriptions)
|
||||||
))
|
))
|
||||||
|
|
||||||
query = query.order_by(
|
query = query.order_by(
|
||||||
func.coalesce(model.cookie_votes, 0).desc(),
|
func.coalesce(model.cookie_votes, 0).desc(),
|
||||||
model.publication_date.desc()
|
model.publication_date.desc()
|
||||||
)
|
)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def _sync_check_file_size(file, max_size):
|
def _sync_check_file_size(file, max_size):
|
||||||
file.seek(0, os.SEEK_END)
|
file.seek(0, os.SEEK_END)
|
||||||
file_size = file.tell()
|
file_size = file.tell()
|
||||||
file.seek(0)
|
file.seek(0)
|
||||||
return file_size <= max_size
|
return file_size <= max_size
|
||||||
|
|
||||||
async def generate_unique_filename(filename, upload_folder):
|
async def generate_unique_filename(filename, upload_folder):
|
||||||
base, ext = os.path.splitext(secure_filename(filename))
|
base, ext = os.path.splitext(secure_filename(filename))
|
||||||
while True:
|
while True:
|
||||||
unique_filename = f"{base}_{uuid.uuid4().hex}{ext}"
|
unique_filename = f"{base}_{uuid.uuid4().hex}{ext}"
|
||||||
file_path = os.path.join(upload_folder, unique_filename)
|
file_path = os.path.join(upload_folder, unique_filename)
|
||||||
if not await aiofiles.os.path.exists(file_path):
|
if not await aiofiles.os.path.exists(file_path):
|
||||||
return unique_filename
|
return unique_filename
|
||||||
|
|
||||||
def get_client_ip():
|
def get_client_ip():
|
||||||
if 'X-Forwarded-For' in request.headers:
|
if 'X-Forwarded-For' in request.headers:
|
||||||
forwarded_for = request.headers['X-Forwarded-For']
|
forwarded_for = request.headers['X-Forwarded-For']
|
||||||
ip_address = forwarded_for.split(',')[0]
|
ip_address = forwarded_for.split(',')[0]
|
||||||
else:
|
else:
|
||||||
ip_address = request.remote_addr
|
ip_address = request.remote_addr
|
||||||
|
|
||||||
return ip_address
|
return ip_address
|
||||||
|
|
||||||
def validate_username(self, username):
|
def validate_username(self, username):
|
||||||
username.data = username.data.lower()
|
username.data = username.data.lower()
|
||||||
user = User.query.filter_by(username=username.data).first()
|
user = User.query.filter_by(username=username.data).first()
|
||||||
if user:
|
if user:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not re.match(r'^[a-z0-9]+$', username.data):
|
if not re.match(r'^[a-z0-9]+$', username.data):
|
||||||
return
|
return
|
||||||
|
|
||||||
def validate_ip(self):
|
def validate_ip(self):
|
||||||
ip_address = get_client_ip()
|
ip_address = get_client_ip()
|
||||||
user_with_ip = User.query.filter_by(ip_address=ip_address).first()
|
user_with_ip = User.query.filter_by(ip_address=ip_address).first()
|
||||||
if user_with_ip:
|
if user_with_ip:
|
||||||
return
|
return
|
||||||
|
|
||||||
def get_autocomplete_suggestions(query):
|
def get_autocomplete_suggestions(query):
|
||||||
|
|
||||||
last_tag = query.split(',')[-1].strip()
|
last_tag = query.split(',')[-1].strip()
|
||||||
|
|
||||||
all_tags = Image.query.with_entities(Image.tags).all()
|
all_tags = Image.query.with_entities(Image.tags).all()
|
||||||
|
|
||||||
unique_tags = set(tag.strip() for tags in all_tags if tags.tags for tag in tags.tags.split(','))
|
unique_tags = set(tag.strip() for tags in all_tags if tags.tags for tag in tags.tags.split(','))
|
||||||
|
|
||||||
filtered_tags = [tag for tag in unique_tags if last_tag.lower() in tag.lower()]
|
filtered_tags = [tag for tag in unique_tags if last_tag.lower() in tag.lower()]
|
||||||
|
|
||||||
return filtered_tags[:5]
|
return filtered_tags[:5]
|
Loading…
x
Reference in New Issue
Block a user