diff --git a/.gitignore b/.gitignore index 28e0928..f5a7b4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,13 @@ -/instance/ -/static/arts/ -/static/comics/ -/static/comicthumbs/ -/static/items/ -/static/posts/ -/static/thumbnails/ -/static/videos/ -/venv/ -/__pycache__/ -static/css/*.css -static/css/*.css.map +/instance/ +/static/arts/ +/static/comics/ +/static/comicthumbs/ +/static/items/ +/static/posts/ +/static/thumbnails/ +/static/videos/ +/venv/ +/__pycache__/ +static/css/*.css +static/css/*.css.map .env \ No newline at end of file diff --git a/README.md b/README.md index c846eb7..970533c 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,61 @@ -[![](https://artberry.xyz/static/Logo.svg "test")](https://artberry.xyz/static/Logo.svg "test") -### Booru сайт нового поколения - -**Открытый репозиторий веб-приложения [Artberry](https://artberry.xyz/ "Artberry")** - ------------- - - -### Используемые технологии -#### Backend: -- ##### ЯП: [Python](http://https://www.python.org/ "Python") -- ##### ФРЕЙМВОРК: [Flask](https://https://flask.palletsprojects.com/en/stable/ "Flask") -- ##### ШАБЛОНИЗАЦИЯ: [JINJA](https:/https://jinja.palletsprojects.com/en/stable// "JINJA") - -#### Frontend: -- ##### HTML -- ##### CSS | [SASS](https://sass-lang.com/ "SASS") -- ##### JAVASCRIPT - ------------- - - -#### ЗАПУСК И ТЕСТИРОВАНИЕ -#### Для удобства и скорости разработки выполните следующие шаги: - -#### Создание виртуального окружения: -- `python -m venv venv` - -#### Запуск виртуального окружения: -*WINDOWS*: -- **powershell:** ` .\venv\Scripts\Activate` -- **CMD:** `venv\Scripts\activate.bat` - -*LINUX* | *MAC*: -- `source venv/bin/activate` - -#### Установка зависимостей: -- `pip install -r requirements.txt` - -#### Запуск проекта: -- `python app.py` или `flask run` - -#### Для отладки в конце файла `app.py` измените: - -- `app.run(debug=False)` **на:** `app.run(debug=True)` - ------------- - -#### КОМПИЛЯЦИЯ CSS ИЗ SASS - -#### Для компиляции в реальном времени: -- `sass --watch static/css/styles.scss:static/css/styles.css` - -*Эта команда будет отслеживать изменения в файле `styles.scss` и автоматически компилировать его в `styles.css`* - -#### Для одноразовой компиляции: -- `sass static/css/styles.scss:static/css/styles.css` - -*Эта команда выполнит одноразовую компиляцию из файла `styles.scss` в `styles.css`* - +[![](https://artberry.xyz/static/Logo.svg "test")](https://artberry.xyz/static/Logo.svg "test") +### Booru сайт нового поколения + +**Открытый репозиторий веб-приложения [Artberry](https://artberry.xyz/ "Artberry")** + +------------ + + +### Используемые технологии +#### Backend: +- ##### ЯП: [Python](http://https://www.python.org/ "Python") +- ##### ФРЕЙМВОРК: [Flask](https://https://flask.palletsprojects.com/en/stable/ "Flask") +- ##### ШАБЛОНИЗАЦИЯ: [JINJA](https:/https://jinja.palletsprojects.com/en/stable// "JINJA") + +#### Frontend: +- ##### HTML +- ##### CSS | [SASS](https://sass-lang.com/ "SASS") +- ##### JAVASCRIPT + +------------ + + +#### ЗАПУСК И ТЕСТИРОВАНИЕ +#### Для удобства и скорости разработки выполните следующие шаги: + +#### Создание виртуального окружения: +- `python -m venv venv` + +#### Запуск виртуального окружения: +*WINDOWS*: +- **powershell:** ` .\venv\Scripts\Activate` +- **CMD:** `venv\Scripts\activate.bat` + +*LINUX* | *MAC*: +- `source venv/bin/activate` + +#### Установка зависимостей: +- `pip install -r requirements.txt` + +#### Запуск проекта: +- `python app.py` или `flask run` + +#### Для отладки в конце файла `app.py` измените: + +- `app.run(debug=False)` **на:** `app.run(debug=True)` + +------------ + +#### КОМПИЛЯЦИЯ CSS ИЗ SASS + +#### Для компиляции в реальном времени: +- `sass --watch static/css/styles.scss:static/css/styles.css` + +*Эта команда будет отслеживать изменения в файле `styles.scss` и автоматически компилировать его в `styles.css`* + +#### Для одноразовой компиляции: +- `sass static/css/styles.scss:static/css/styles.css` + +*Эта команда выполнит одноразовую компиляцию из файла `styles.scss` в `styles.css`* + ------------ \ No newline at end of file diff --git a/admin.py b/admin.py index 6d6b9c4..0d3c447 100644 --- a/admin.py +++ b/admin.py @@ -1,207 +1,207 @@ -from flask import render_template, redirect, url_for, request, abort -from flask_login import login_required, current_user -from wtforms import StringField, SubmitField -from wtforms.validators import DataRequired -from flask_wtf import FlaskForm -from models import db, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicVotes, Cookies, UpdateCookiesForm -import os -import shutil -import uuid -import aiofiles -import bcrypt -from utils import update_related_tables - -def register_admin_routes(app): - @app.route('/admin', methods=['GET', 'POST']) - @login_required - def admin(): - if current_user.username != 'naturefie': - return redirect(url_for('index')) - - form = UpdateCookiesForm() - - user_cookies = { - user.id: Cookies.query.filter_by(username=user.username).first().cookies if Cookies.query.filter_by(username=user.username).first() else 0 - for user in User.query.all() - } - - comments = Comments.query.order_by(Comments.comment_date.desc()).all() - - return render_template( - 'panel.html', - arts=Image.query.all(), - comics=Comic.query.all(), - videos=Video.query.all(), - users=User.query.all(), - comments=comments, - form=form, - user_cookies=user_cookies - ) - - @app.route('/admin/delete//', methods=['POST']) - @login_required - def admin_delete_content(content_type, content_id): - models = { - 'art': (Image, 'arts', 'image_file', Votes, 'image_id'), - 'video': (Video, 'videos', 'video_file', VideoVotes, 'video_id'), - 'comic': (Comic, 'comics', 'comic_folder', ComicVotes, 'comic_id') - } - - if content_type not in models: - abort(404) - - model, folder, file_field, vote_model, foreign_key = models[content_type] - - content = model.query.get_or_404(content_id) - - vote_model.query.filter(getattr(vote_model, foreign_key) == content_id).delete() - - Comments.query.filter(getattr(Comments, foreign_key) == content_id).delete() - - file_path = os.path.join(app.config['UPLOAD_FOLDER'][folder], getattr(content, file_field)) - if os.path.exists(file_path): - if os.path.isfile(file_path): - os.remove(file_path) - else: - shutil.rmtree(file_path) - - db.session.delete(content) - db.session.commit() - return redirect(url_for('admin')) - - @app.route('/admin/delete/user/', methods=['POST']) - @login_required - def admin_delete_user(user_id): - user = User.query.get_or_404(user_id) - if current_user.username != 'naturefie': - return redirect(url_for('admin')) - db.session.delete(user) - db.session.commit() - return redirect(url_for('admin')) - - @app.route('/admin/update_comment/', methods=['POST']) - @login_required - def admin_update_comment(comment_id): - comment = Comments.query.get_or_404(comment_id) - if current_user.username != 'naturefie': - abort(403) - - new_text = request.form.get('comment_text', '').strip() - if not new_text: - return redirect(url_for('admin')) - - comment.comment_text = new_text - try: - db.session.commit() - print(f"Updated comment ID {comment_id}: {comment.comment_text}") - except Exception as e: - db.session.rollback() - print(f"Error updating comment: {e}") - - return redirect(url_for('admin')) - - @app.route('/admin/delete_comment/', methods=['POST']) - @login_required - def admin_delete_comment(comment_id): - comment = Comments.query.get_or_404(comment_id) - if current_user.username != 'naturefie': - abort(403) - - db.session.delete(comment) - db.session.commit() - return redirect(url_for('admin')) - - @app.route('/admin/update_cookies/', methods=['POST']) - @login_required - def admin_update_cookies(user_id): - user = User.query.get_or_404(user_id) - if request.method == 'POST': - new_cookie_count = request.form.get('cookies', type=int) - if new_cookie_count is not None and new_cookie_count >= 0: - user_cookies = Cookies.query.filter_by(username=user.username).first() - if not user_cookies: - user_cookies = Cookies(username=user.username, cookies=new_cookie_count) - db.session.add(user_cookies) - else: - user_cookies.cookies = new_cookie_count - db.session.commit() - - return redirect(url_for('admin')) - - @app.route('/admin/update_video/', methods=['POST']) - @login_required - def admin_update_video(content_id): - video = Video.query.get_or_404(content_id) - if current_user.username != 'naturefie': - return redirect(url_for('admin')) - - new_video_name = request.form.get('video_name') - new_description = request.form.get('description') - new_tags = request.form.get('tags') - - if new_video_name and new_video_name != video.video_name: - if len(new_video_name) < 3 or len(new_video_name) > 100: - return redirect(url_for('admin')) - - video.video_name = new_video_name - - if new_description: - video.description = new_description - - if new_tags: - video.tags = new_tags - - db.session.commit() - return redirect(url_for('admin')) - - @app.route('/admin/update_user/', methods=['POST']) - @login_required - def admin_update_user(user_id): - user = User.query.get_or_404(user_id) - if current_user.username != 'naturefie': - return redirect(url_for('admin')) - - new_username = request.form.get('username') - new_password = request.form.get('password') - - if new_username and new_username != user.username: - if len(new_username) < 3 or len(new_username) > 20: - return redirect(url_for('admin')) - if User.query.filter_by(username=new_username).first(): - return redirect(url_for('admin')) - - old_username = user.username - user.username = new_username - update_related_tables(old_username, new_username) - - if new_password: - if len(new_password) < 6: - return redirect(url_for('admin')) - - hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8') - user.encrypted_password = hashed_password - - db.session.commit() - return redirect(url_for('admin')) - - @app.route('/admin/update_tags//', methods=['POST']) - @login_required - def admin_update_tags(content_type, content_id): - models = { - 'art': Image, - 'video': Video, - 'comic': Comic - } - - if content_type not in models: - abort(404) - - model = models[content_type] - content = model.query.get_or_404(content_id) - - new_tags = request.form.get('tags', '').strip() - - content.tags = new_tags - db.session.commit() - +from flask import render_template, redirect, url_for, request, abort +from flask_login import login_required, current_user +from wtforms import StringField, SubmitField +from wtforms.validators import DataRequired +from flask_wtf import FlaskForm +from models import db, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicVotes, Cookies, UpdateCookiesForm +import os +import shutil +import uuid +import aiofiles +import bcrypt +from utils import update_related_tables + +def register_admin_routes(app): + @app.route('/admin', methods=['GET', 'POST']) + @login_required + def admin(): + if current_user.username != 'naturefie': + return redirect(url_for('index')) + + form = UpdateCookiesForm() + + user_cookies = { + user.id: Cookies.query.filter_by(username=user.username).first().cookies if Cookies.query.filter_by(username=user.username).first() else 0 + for user in User.query.all() + } + + comments = Comments.query.order_by(Comments.comment_date.desc()).all() + + return render_template( + 'panel.html', + arts=Image.query.all(), + comics=Comic.query.all(), + videos=Video.query.all(), + users=User.query.all(), + comments=comments, + form=form, + user_cookies=user_cookies + ) + + @app.route('/admin/delete//', methods=['POST']) + @login_required + def admin_delete_content(content_type, content_id): + models = { + 'art': (Image, 'arts', 'image_file', Votes, 'image_id'), + 'video': (Video, 'videos', 'video_file', VideoVotes, 'video_id'), + 'comic': (Comic, 'comics', 'comic_folder', ComicVotes, 'comic_id') + } + + if content_type not in models: + abort(404) + + model, folder, file_field, vote_model, foreign_key = models[content_type] + + content = model.query.get_or_404(content_id) + + vote_model.query.filter(getattr(vote_model, foreign_key) == content_id).delete() + + Comments.query.filter(getattr(Comments, foreign_key) == content_id).delete() + + file_path = os.path.join(app.config['UPLOAD_FOLDER'][folder], getattr(content, file_field)) + if os.path.exists(file_path): + if os.path.isfile(file_path): + os.remove(file_path) + else: + shutil.rmtree(file_path) + + db.session.delete(content) + db.session.commit() + return redirect(url_for('admin')) + + @app.route('/admin/delete/user/', methods=['POST']) + @login_required + def admin_delete_user(user_id): + user = User.query.get_or_404(user_id) + if current_user.username != 'naturefie': + return redirect(url_for('admin')) + db.session.delete(user) + db.session.commit() + return redirect(url_for('admin')) + + @app.route('/admin/update_comment/', methods=['POST']) + @login_required + def admin_update_comment(comment_id): + comment = Comments.query.get_or_404(comment_id) + if current_user.username != 'naturefie': + abort(403) + + new_text = request.form.get('comment_text', '').strip() + if not new_text: + return redirect(url_for('admin')) + + comment.comment_text = new_text + try: + db.session.commit() + print(f"Updated comment ID {comment_id}: {comment.comment_text}") + except Exception as e: + db.session.rollback() + print(f"Error updating comment: {e}") + + return redirect(url_for('admin')) + + @app.route('/admin/delete_comment/', methods=['POST']) + @login_required + def admin_delete_comment(comment_id): + comment = Comments.query.get_or_404(comment_id) + if current_user.username != 'naturefie': + abort(403) + + db.session.delete(comment) + db.session.commit() + return redirect(url_for('admin')) + + @app.route('/admin/update_cookies/', methods=['POST']) + @login_required + def admin_update_cookies(user_id): + user = User.query.get_or_404(user_id) + if request.method == 'POST': + new_cookie_count = request.form.get('cookies', type=int) + if new_cookie_count is not None and new_cookie_count >= 0: + user_cookies = Cookies.query.filter_by(username=user.username).first() + if not user_cookies: + user_cookies = Cookies(username=user.username, cookies=new_cookie_count) + db.session.add(user_cookies) + else: + user_cookies.cookies = new_cookie_count + db.session.commit() + + return redirect(url_for('admin')) + + @app.route('/admin/update_video/', methods=['POST']) + @login_required + def admin_update_video(content_id): + video = Video.query.get_or_404(content_id) + if current_user.username != 'naturefie': + return redirect(url_for('admin')) + + new_video_name = request.form.get('video_name') + new_description = request.form.get('description') + new_tags = request.form.get('tags') + + if new_video_name and new_video_name != video.video_name: + if len(new_video_name) < 3 or len(new_video_name) > 100: + return redirect(url_for('admin')) + + video.video_name = new_video_name + + if new_description: + video.description = new_description + + if new_tags: + video.tags = new_tags + + db.session.commit() + return redirect(url_for('admin')) + + @app.route('/admin/update_user/', methods=['POST']) + @login_required + def admin_update_user(user_id): + user = User.query.get_or_404(user_id) + if current_user.username != 'naturefie': + return redirect(url_for('admin')) + + new_username = request.form.get('username') + new_password = request.form.get('password') + + if new_username and new_username != user.username: + if len(new_username) < 3 or len(new_username) > 20: + return redirect(url_for('admin')) + if User.query.filter_by(username=new_username).first(): + return redirect(url_for('admin')) + + old_username = user.username + user.username = new_username + update_related_tables(old_username, new_username) + + if new_password: + if len(new_password) < 6: + return redirect(url_for('admin')) + + hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8') + user.encrypted_password = hashed_password + + db.session.commit() + return redirect(url_for('admin')) + + @app.route('/admin/update_tags//', methods=['POST']) + @login_required + def admin_update_tags(content_type, content_id): + models = { + 'art': Image, + 'video': Video, + 'comic': Comic + } + + if content_type not in models: + abort(404) + + model = models[content_type] + content = model.query.get_or_404(content_id) + + new_tags = request.form.get('tags', '').strip() + + content.tags = new_tags + db.session.commit() + return redirect(url_for('admin')) \ No newline at end of file diff --git a/app.py b/app.py index 57fe59c..2cd3d54 100644 --- a/app.py +++ b/app.py @@ -1,953 +1,957 @@ -import os -import io -import re -import uuid -import shutil -import random -import asyncio -from datetime import datetime -import requests -from PIL import Image as PILImage -from sqlalchemy.exc import IntegrityError -from werkzeug.exceptions import BadRequest -from werkzeug.security import generate_password_hash, check_password_hash -from werkzeug.utils import secure_filename -from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify -from flask_sqlalchemy import SQLAlchemy -from flask_bcrypt import Bcrypt -from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user -from flask_wtf import FlaskForm, RecaptchaField, CSRFProtect -from flask_wtf.file import FileAllowed -from flask_wtf.csrf import validate_csrf -from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField, RadioField, SelectField, TextAreaField -from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Regexp -from dotenv import load_dotenv, find_dotenv -import aiofiles.os -from sqlalchemy import func, or_ -import magic -from config import Config -from models import db, bcrypt, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicPage, ComicVotes, Cookies, Views, Item, UserItem, Post, Subscription, EditTagsForm, EditVideoForm, RegistrationForm, LoginForm, EmptyForm -from admin import register_admin_routes -from upload import upload_bp -from utils import allowed_file, check_file_content, check_file_size, convert_to_webp, generate_unique_filename, get_content_query, get_client_ip, get_autocomplete_suggestions -from auth import auth_bp - -app = Flask(__name__) -csrf = CSRFProtect(app) - -app.config.from_object(Config) - -db.init_app(app) -bcrypt.init_app(app) -login_manager = LoginManager(app) -login_manager.login_view = 'auth.login' - -@login_manager.user_loader -def load_user(user_id): - return User.query.get(int(user_id)) - -register_admin_routes(app) -app.register_blueprint(upload_bp) -app.register_blueprint(auth_bp) - -@app.route('/') -def index(): - page = request.args.get('page', 1, type=int) - search_query = request.args.get('search') - - subscriptions = [] - if current_user.is_authenticated: - - subscriptions = [ - sub.author.username - for sub in Subscription.query.filter_by(user_id=current_user.id).all() - ] - - query = get_content_query(Image, subscriptions, search_query) - pagination = query.paginate(page=page, per_page=25, error_out=False) - - user_cookies = 0 - if current_user.is_authenticated: - user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() - user_cookies = user_cookies_record.cookies if user_cookies_record else 0 - - return render_template( - 'index.html', - images=pagination.items, - pagination=pagination, - user_cookies=user_cookies, - search_query=search_query, - content_type='art' - ) - -@app.route('/arts') -def arts(): - return render_template( - 'arts.html' - ) - -@app.route('/catalogue/arts') -def arts_catalogue(): - return render_template( - 'arts-catalogue.html' - ) - -@app.route('/vote_art/', methods=['POST']) -@login_required -def vote_art(image_id): - image = Image.query.get_or_404(image_id) - user_cookies = Cookies.query.filter_by(username=current_user.username).first() - - if image.username == current_user.username: - return redirect(url_for('view', content_type='art', id=image_id)) - - if user_cookies and user_cookies.cookies > 0: - existing_vote = Votes.query.filter_by(username=current_user.username, image_id=image_id).first() - if not existing_vote: - user_cookies.cookies -= 1 - image.cookie_votes += 1 - - new_vote = Votes(username=current_user.username, image_id=image.id) - db.session.add(new_vote) - - db.session.commit() - - return redirect(url_for('view', content_type='art', id=image_id)) - -@app.route('/view//', methods=['GET', 'POST']) -def view(content_type, id): - comments = [] - avatars = {user.username: user.avatar_file for user in User.query.all()} - cu = current_user.username if current_user.is_authenticated else None - - user_cookies = 0 - if current_user.is_authenticated: - user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() - user_cookies = user_cookies_record.cookies if user_cookies_record else 0 - - if content_type == 'art': - content = Image.query.get_or_404(id) - comments = Comments.query.filter_by(image_id=id).order_by(Comments.comment_date.desc()).all() - - if current_user.is_authenticated: - existing_view = Views.query.filter_by(image_id=id, username=current_user.username).first() - if not existing_view: - new_view = Views(image_id=id, username=current_user.username) - db.session.add(new_view) - db.session.commit() - - search_query = request.args.get('search') - page = request.args.get('page', 1, type=int) - subscriptions = [] - - if current_user.is_authenticated: - subscriptions = [ - sub.author.username - for sub in Subscription.query.filter_by(user_id=current_user.id).all() - ] - - query = get_content_query(Image, subscriptions, search_query) - all_images = query.all() - image_ids = [image.id for image in all_images] - - current_index = image_ids.index(id) - prev_index = (current_index - 1) % len(image_ids) - next_index = (current_index + 1) % len(image_ids) - - prev_content = all_images[prev_index] - next_content = all_images[next_index] - random_content = random.choice(all_images) - - elif content_type == 'video': - content = Video.query.get_or_404(id) - comments = Comments.query.filter_by(video_id=id).order_by(Comments.comment_date.desc()).all() - - if current_user.is_authenticated: - existing_view = Views.query.filter_by(video_id=id, username=current_user.username).first() - if not existing_view: - new_view = Views(video_id=id, username=current_user.username) - db.session.add(new_view) - db.session.commit() - - all_videos = Video.query.order_by(Video.id).all() - video_ids = [video.id for video in all_videos] - current_index = video_ids.index(id) - prev_index = (current_index - 1) % len(all_videos) - next_index = (current_index + 1) % len(all_videos) - - prev_content = all_videos[prev_index] - next_content = all_videos[next_index] - - random_content = random.choice(all_videos) - - elif content_type == 'comic': - content = Comic.query.get_or_404(id) - comments = Comments.query.filter_by(comic_id=id).order_by(Comments.comment_date.desc()).all() - - if current_user.is_authenticated: - existing_view = Views.query.filter_by(image_id=id, username=current_user.username).first() - if not existing_view: - new_view = Views(image_id=id, username=current_user.username) - db.session.add(new_view) - db.session.commit() - - comic_pages = ComicPage.query.filter_by(comic_id=id).order_by(ComicPage.page_number).all() - - if not comic_pages: - return render_template('error.html', message="Comic pages not found") - - all_comics = Comic.query.order_by(Comic.id).all() - comic_ids = [comic.id for comic in all_comics] - current_index = comic_ids.index(id) - prev_index = (current_index - 1) % len(all_comics) - next_index = (current_index + 1) % len(all_comics) - - prev_content = all_comics[prev_index] - next_content = all_comics[next_index] - - random_content = random.choice(all_comics) - - else: - abort(404) - - if request.method == 'POST' and current_user.is_authenticated: - comment_text = request.form.get('comment') - max_comment_length = 44 - if comment_text and len(comment_text) > max_comment_length: - return redirect(url_for('view', content_type=content_type, id=id)) - - if comment_text: - new_comment = None - if content_type == 'art': - new_comment = Comments(username=cu, image_id=id, comment_text=comment_text) - elif content_type == 'video': - new_comment = Comments(username=cu, video_id=id, comment_text=comment_text) - elif content_type == 'comic': - new_comment = Comments(username=cu, comic_id=id, comment_text=comment_text) - - if new_comment: - db.session.add(new_comment) - db.session.commit() - return redirect(url_for('view', content_type=content_type, id=id)) - - return render_template( - 'view.html', - content=content, - content_type=content_type, - comments=comments, - avatars=avatars, - prev_content=prev_content, - next_content=next_content, - random_content=random_content, - user_cookies=user_cookies, - comic_pages=comic_pages if content_type == 'comic' else None - ) - -@app.route('/images') -def drawings(): - return render_template( - 'images.html' - ) - -@app.route('/tags_list/') -def tags_list(page_type): - comics_tags = [comic.tags for comic in Comic.query.all() if comic.tags] - images_tags = [image.tags for image in Image.query.all() if image.tags] - videos_tags = [video.tags for video in Video.query.all() if video.tags] - - if page_type == 'video': - all_tags = [tag.strip() for tags in videos_tags for tag in tags.split(',')] - elif page_type == 'comic': - all_tags = [tag.strip() for tags in comics_tags for tag in tags.split(',')] - elif page_type == 'image': - all_tags = [tag.strip() for tags in images_tags for tag in tags.split(',')] - else: - all_tags = set(tag.strip() for tags in (comics_tags + images_tags + videos_tags) for tag in tags.split(',')) - - sorted_tags = sorted(set(all_tags)) - - return render_template('tags_list.html', tags=sorted_tags, page_type=page_type) - -@app.route('/subnav') -def subnav(): - return render_template( - 'subnav.html' - ) - -@app.route('/image_edit/', methods=['GET', 'POST']) -@login_required -def image_edit(id): - - image = Image.query.get_or_404(id) - - if image.username != current_user.username: - return redirect(url_for('index')) - - form = EditTagsForm() - - if form.validate_on_submit(): - - image.tags = form.tags.data - db.session.commit() - return redirect(url_for('view', content_type='art', id=id)) - - if request.method == 'GET': - form.tags.data = image.tags - - image_preview_url = url_for('static', filename=f'arts/{image.image_file}') - - return render_template( - 'image_edit.html', - form=form, - image=image, - image_preview_url=image_preview_url - ) - -@app.route('/video_edit/', methods=['GET', 'POST']) -@login_required -async def video_edit(id): - video = Video.query.get_or_404(id) - - if video.username != current_user.username: - return redirect(url_for('index')) - - form = EditVideoForm() - - if request.method == 'GET': - form.video_name.data = video.video_name - form.description.data = video.description - form.tags.data = video.tags - - if form.validate_on_submit(): - video.video_name = form.video_name.data - video.description = form.description.data - video.tags = form.tags.data - - if form.video_thumbnail.data: - thumbnail_file = form.video_thumbnail.data - if allowed_file(thumbnail_file.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): - - thumbnail_filename = await generate_unique_filename( - app.config['UPLOAD_FOLDER']['thumbnails'], 'webp' - ) - thumbnail_path = os.path.join(app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename) - - webp_thumbnail = await convert_to_webp(thumbnail_file) - async with aiofiles.open(thumbnail_path, 'wb') as f: - await f.write(webp_thumbnail.read()) - - video.video_thumbnail_file = thumbnail_filename - - db.session.commit() - return redirect(url_for('view', content_type='video', id=id)) - - video_preview_url = url_for('static', filename=f'videos/{video.video_thumbnail_file}') - - return render_template( - 'video_edit.html', - form=form, - video=video, - video_preview_url=video_preview_url - ) - -@app.route('/navbar') -def navbar(): - return render_template( - 'navbar.html' -) - -@app.route('/card') -def card(): - return render_template( - 'card.html' - ) - -@app.route('/gifs') -def gifs(): - return render_template( - 'gifs.html' - ) - -@app.route('/autocomplete') -def autocomplete(): - search_query = request.args.get('search', '', type=str) - - if search_query: - - suggestions = get_autocomplete_suggestions(search_query) - else: - suggestions = [] - - return jsonify(suggestions) - -@app.route('/user_pubs//') -def user_pubs(pub_type, username): - p = request.args.get('page', 1, type=int) - search_query = request.args.get('search', '') - - if pub_type == 'arts': - query = Image.query.filter_by(username=username) - if search_query: - - query = query.filter(or_( - Image.image_file.ilike(f'%{search_query}%'), - Image.tags.ilike(f'%{search_query}%') - )) - items = query.order_by(Image.publication_date.desc()).paginate(page=p, per_page=25, error_out=False) - - elif pub_type == 'videos': - query = Video.query.filter_by(username=username) - if search_query: - - query = query.filter(Video.video_name.ilike(f'%{search_query}%')) - items = query.order_by(Video.publication_date.desc()).paginate(page=p, per_page=25, error_out=False) - - elif pub_type == 'comics': - query = Comic.query.filter_by(username=username) - if search_query: - - query = query.filter(Comic.name.ilike(f'%{search_query}%')) - items = query.order_by(Comic.publication_date.desc()).paginate(page=p, per_page=25, error_out=False) - - else: - abort(404) - - user_cookies = Cookies.query.filter_by(username=username).first() - cookies_count = user_cookies.cookies if user_cookies else 0 - - return render_template( - 'user_pubs.html', - items=items.items, - pagination=items, - pub_type=pub_type, - username=username, - cookies_count=cookies_count, - search_query=search_query - ) - -@app.route('/delete//', methods=['POST']) -@login_required -def delete(content_type, content_id): - if content_type == 'art': - content = Image.query.get_or_404(content_id) - file_path = os.path.join(app.config['UPLOAD_FOLDER']['arts'], content.image_file) - elif content_type == 'video': - content = Video.query.get_or_404(content_id) - file_path = os.path.join(app.config['UPLOAD_FOLDER']['videos'], content.video_file) - elif content_type == 'comic': - content = Comic.query.get_or_404(content_id) - file_path = os.path.join(app.config['UPLOAD_FOLDER']['comics'], content.comic_folder) - else: - abort(404, "Invalid content type") - - if content.username != current_user.username: - abort(403) - - if os.path.exists(file_path): - if os.path.isdir(file_path): - shutil.rmtree(file_path) - else: - os.remove(file_path) - - db.session.delete(content) - db.session.commit() - return redirect(url_for('profile', username=current_user.username)) - - -@app.route('/delete_comment/', methods=['POST']) -@login_required -def delete_comment(comment_id): - comment = db.session.get(Comments, comment_id) or abort(404) - - if comment.image_id: - content_type = 'art' - content_id = comment.image_id - elif comment.video_id: - content_type = 'video' - content_id = comment.video_id - else: - content_type = 'comic' - content_id = comment.comic_id - - if comment.username == current_user.username: - try: - if hasattr(comment, 'post_id') and comment.post_id: - post_id = comment.post_id - db.session.delete(comment) - db.session.commit() - return redirect(url_for('user_posts', username=current_user.username)) - - db.session.delete(comment) - db.session.commit() - return redirect(url_for('view', content_type=content_type, id=content_id)) - except IntegrityError: - db.session.rollback() - return redirect(url_for('view', content_type=content_type, id=content_id)) - - return redirect(request.referrer or url_for('index')) - -@app.route('/videos') -def videos(): - page = request.args.get('page', 1, type=int) - search_query = request.args.get('search') - - subscriptions = [] - if current_user.is_authenticated: - subscriptions = [sub.author_id for sub in Subscription.query.filter_by(user_id=current_user.id).all()] - - query = get_content_query(Video, subscriptions, search_query) - pagination = query.paginate(page=page, per_page=10, error_out=False) - - videos_with_views = [] - for video in pagination.items: - views_count = db.session.query(func.count(Views.id)).filter(Views.video_id == video.id).scalar() - videos_with_views.append({ - 'video': video, - 'views_count': views_count - }) - - popular_videos = [ - { - 'video': video[0], - 'views_count': video[1] - } - for video in db.session.query(Video, func.count(Views.id).label('views_count')) - .outerjoin(Views, Views.video_id == Video.id) - .group_by(Video.id) - .order_by(func.count(Views.id).desc(), Video.cookie_votes.desc()) - .limit(8) - .all() - ] - - most_viewed_videos = [ - { - 'video': video[0], - 'views_count': video[1] - } - for video in db.session.query(Video, func.count(Views.id).label('views_count')) - .outerjoin(Views, Views.video_id == Video.id) - .group_by(Video.id) - .order_by(func.count(Views.id).desc()) - .limit(8) - .all() - ] - - videos_tags = [video.tags for video in Video.query.all() if video.tags] - all_tags = [tag.strip() for tags in videos_tags for tag in tags.split(',')] - sorted_tags = sorted(set(all_tags)) - - user_cookies = 0 - if current_user.is_authenticated: - user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() - user_cookies = user_cookies_record.cookies if user_cookies_record else 0 - - return render_template( - 'videos.html', - videos=videos_with_views, - pagination=pagination, - user_cookies=user_cookies, - search_query=search_query, - content_type='video', - tags=sorted_tags, - popular_videos=popular_videos, - most_viewed_videos=most_viewed_videos - ) - - -@app.route('/vote_video/', methods=['POST']) -@login_required -def vote_video(video_id): - video = Video.query.get_or_404(video_id) - user_cookies = Cookies.query.filter_by(username=current_user.username).first() - - if video.username == current_user.username: - return redirect(url_for('view', content_type='video', id=video_id)) - - if user_cookies and user_cookies.cookies > 0: - existing_vote = VideoVotes.query.filter_by(username=current_user.username, video_id=video_id).first() - if not existing_vote: - user_cookies.cookies -= 1 - video.cookie_votes += 1 - new_vote = VideoVotes(username=current_user.username, video_id=video.id) - db.session.add(new_vote) - db.session.commit() - - return redirect(url_for('view', content_type='video', id=video_id)) - -@app.route('/comics') -def comics(): - page = request.args.get('page', 1, type=int) - search_query = request.args.get('search') - - subscriptions = [] - if current_user.is_authenticated: - subscriptions = [sub.author_id for sub in Subscription.query.filter_by(user_id=current_user.id).all()] - - query = get_content_query(Comic, subscriptions, search_query) - pagination = query.paginate(page=page, per_page=10, error_out=False) - - user_cookies = 0 - if current_user.is_authenticated: - user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() - user_cookies = user_cookies_record.cookies if user_cookies_record else 0 - - return render_template( - 'comics.html', - comics=pagination.items, - pagination=pagination, - user_cookies=user_cookies, - search_query=search_query, - content_type='comic' - ) - -@app.route('/vote_comic/', methods=['POST']) -@login_required -def vote_comic(comic_id): - comic = Comic.query.get_or_404(comic_id) - user_cookies = Cookies.query.filter_by(username=current_user.username).first() - - if comic.username == current_user.username: - return redirect(url_for('view', content_type='comic', id=comic_id)) - - if user_cookies and user_cookies.cookies > 0: - existing_vote = ComicVotes.query.filter_by(username=current_user.username, comic_id=comic_id).first() - if not existing_vote: - user_cookies.cookies -= 1 - comic.cookie_votes += 1 - new_vote = ComicVotes(username=current_user.username, comic_id=comic.id) - db.session.add(new_vote) - db.session.commit() - - return redirect(url_for('view', content_type='comic', id=comic_id)) - -@app.route('/comic_edit/', methods=['GET', 'POST']) -@login_required -async def comic_edit(comic_id): - comic = Comic.query.get_or_404(comic_id) - - if comic.username != current_user.username: - return redirect(url_for('index')) - - cfp = os.path.join(app.config['UPLOAD_FOLDER']['comics'], comic.name) - if not os.path.exists(cfp): - os.makedirs(cfp) - - form = EmptyForm() - - if request.method == 'POST' and form.validate_on_submit(): - action = request.form.get('action') - - if action == 'delete' and (page_id := request.form.get('page')): - page = ComicPage.query.get(page_id) - if page: - os.remove(page.file_path.replace('\\', '/')) - db.session.delete(page) - db.session.commit() - pages = ComicPage.query.filter_by(comic_id=comic.id).order_by(ComicPage.page_number).all() - for i, page in enumerate(pages, start=1): - page.page_number = i - db.session.commit() - - elif action == 'update' and (page_id := request.form.get('page')) and 'new_page' in request.files: - new_page = request.files['new_page'] - filename = f"{uuid.uuid4().hex}.webp" - file_path = os.path.join(cfp, filename).replace('\\', '/') - webp_image = await convert_to_webp(new_page) - async with aiofiles.open(file_path, 'wb') as f: - await f.write(webp_image.read()) - - page = ComicPage.query.get(page_id) - if page: - os.remove(page.file_path.replace('\\', '/')) - page.file_path = file_path - db.session.commit() - - elif action == 'add' and 'new_page' in request.files: - new_page = request.files['new_page'] - filename = f"{uuid.uuid4().hex}.webp" - file_path = os.path.join(cfp, filename).replace('\\', '/') - webp_image = await convert_to_webp(new_page) - async with aiofiles.open(file_path, 'wb') as f: - await f.write(webp_image.read()) - - page_number = (db.session.query(db.func.max(ComicPage.page_number)).filter_by(comic_id=comic.id).scalar() or 0) + 1 - db.session.add(ComicPage(comic_id=comic.id, page_number=page_number, file_path=file_path)) - db.session.commit() - - return redirect(url_for('comic_edit', comic_id=comic.id)) - - comic_pages = ComicPage.query.filter_by(comic_id=comic.id).order_by(ComicPage.page_number).all() - return render_template('comic_edit.html', comic=comic, comic_pages=comic_pages, form=form) - -def update_related_tables(old_username, new_username): - - Comments.query.filter_by(username=old_username).update({"username": new_username}) - - Image.query.filter_by(username=old_username).update({"username": new_username}) - - Video.query.filter_by(username=old_username).update({"username": new_username}) - - Comic.query.filter_by(username=old_username).update({"username": new_username}) - - Cookies.query.filter_by(username=old_username).update({"username": new_username}) - - Post.query.filter_by(username=old_username).update({"username": new_username}) - - Subscription.query.filter_by(user_id=old_username).update({"user_id": new_username}) - Subscription.query.filter_by(author_id=old_username).update({"author_id": new_username}) - - UserItem.query.filter_by(username=old_username).update({"username": new_username}) - - db.session.commit() - -@app.route('/profile_edit', methods=['GET', 'POST']) -@login_required -def profile_edit(): - if request.method == 'POST': - section = request.form.get('section') - - if section == 'avatar_banner': - - avatar = request.files.get('avatar') - banner = request.files.get('banner') - - if avatar and allowed_file(avatar.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): - if check_file_size(avatar, app.config['MAX_IMAGE_SIZE']): - avatar_filename = secure_filename(avatar.filename) - avatar_path = os.path.join(app.config['UPLOAD_FOLDER']['avatars'], avatar_filename) - avatar.save(avatar_path) - current_user.avatar_file = avatar_filename - else: - return redirect(url_for('profile_edit')) - - if banner and allowed_file(banner.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): - if check_file_size(banner, app.config['MAX_IMAGE_SIZE']): - banner_filename = secure_filename(banner.filename) - banner_path = os.path.join(app.config['UPLOAD_FOLDER']['banners'], banner_filename) - banner.save(banner_path) - current_user.banner_file = banner_filename - else: - return redirect(url_for('profile_edit')) - - db.session.commit() - - return redirect(url_for('profile', username=current_user.username)) - - elif section == 'username': - - new_username = request.form.get('new_username') - if new_username and new_username != current_user.username: - if len(new_username) < 3 or len(new_username) > 20: - pass - elif User.query.filter_by(username=new_username).first(): - pass - else: - old_username = current_user.username - current_user.username = new_username - update_related_tables(old_username, new_username) - return redirect(url_for('profile', username=new_username)) - - elif section == 'bio': - - bio = request.form.get('bio') - current_user.bio = bio - db.session.commit() - - elif section == 'password': - - current_password = request.form.get('current_password') - new_password = request.form.get('new_password') - confirm_password = request.form.get('confirm_new_password') - - if not current_user.check_password(current_password): - pass - elif new_password != confirm_password: - pass - elif len(new_password) < 6: - pass - else: - current_user.encrypted_password = bcrypt.generate_password_hash(new_password).decode('utf-8') - db.session.commit() - return redirect(url_for('profile_edit')) - - elif section == 'create_post': - - post_text = request.form.get('post_text') - post_media = request.files.get('post_media') - - if post_text: - new_post = Post( - username=current_user.username, - text=post_text - ) - db.session.add(new_post) - - if post_media and allowed_file(post_media.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): - if check_file_size(post_media, app.config['MAX_IMAGE_SIZE']): - media_filename = secure_filename(post_media.filename) - media_path = os.path.join(app.config['UPLOAD_FOLDER']['posts'], media_filename) - post_media.save(media_path) - new_post.media_file = media_filename - db.session.commit() - pass - else: - return redirect(url_for('profile_edit')) - - db.session.commit() - return redirect(url_for('user_posts', username=current_user.username)) - - elif section == 'decoration': - selected_item_id = request.form.get('selected_item') - if selected_item_id: - - selected_item = UserItem.query.filter_by(username=current_user.username, item_id=selected_item_id).first() - - if selected_item: - - current_user.current_item = selected_item.item.id - db.session.commit() - pass - else: - pass - return redirect(url_for('profile', username=current_user.username)) - - db.session.commit() - return redirect(url_for('profile_edit')) - - return render_template( - 'profile_edit.html', - user=current_user, - user_items=UserItem.query.filter_by(username=current_user.username).all() - ) - -@app.route('/profile/') -def profile(username): - user = User.query.filter_by(username=username).first_or_404() - - if not user.avatar_file: - user.avatar_file = 'default-avatar.png' - - if not user.banner_file: - user.banner_file = 'default-banner.png' - - total_arts = Image.query.filter_by(username=username).count() - total_videos = Video.query.filter_by(username=username).count() - total_comics = Comic.query.filter_by(username=username).count() - - arts = Image.query.filter_by(username=username).order_by(Image.cookie_votes.desc(), Image.publication_date.desc()).limit(3).all() - videos = Video.query.filter_by(username=username).order_by(Video.cookie_votes.desc(), Video.publication_date.desc()).limit(3).all() - comics = Comic.query.filter_by(username=username).order_by(Comic.cookie_votes.desc(), Comic.publication_date.desc()).limit(3).all() - - subscribed = ( - current_user.is_authenticated and - Subscription.query.filter_by(user_id=current_user.id, author_id=user.id).first() is not None - ) - - current_item = Item.query.get(user.current_item) if user.current_item else None - - is_current_user = current_user.is_authenticated and current_user.username == user.username - - subscriptions_count = Subscription.query.filter_by(author_id=user.id).count() - - return render_template( - 'profile.html', - user=user, - arts=arts, - videos=videos, - comics=comics, - total_arts=total_arts, - total_videos=total_videos, - total_comics=total_comics, - current_item=current_item, - is_current_user=is_current_user, - subscribed=subscribed, - subscriptions_count=subscriptions_count - ) - -@app.route('/posts/') -def user_posts(username): - user = User.query.filter_by(username=username).first_or_404() - posts = Post.query.filter_by(username=username).order_by(Post.post_date.desc()).all() - avatars = {user.username: user.avatar_file for user in User.query.all()} - return render_template('post.html', username=username, posts=posts, avatars=avatars) - -@app.route('/posts//add_comment/', methods=['POST']) -@login_required -def add_comment(username, post_id): - comment_text = request.form.get('comment_text') - if not comment_text: - return redirect(url_for('user_posts', username=username)) - - comment = Comments( - username=current_user.username, - post_id=post_id, - comment_text=comment_text, - comment_date=datetime.utcnow() - ) - db.session.add(comment) - db.session.commit() - return redirect(url_for('user_posts', username=username)) - -@app.route('/subscribe/', methods=['POST']) -@login_required -def subscribe(author_id): - if author_id == current_user.id: - return redirect(url_for('profile', username=current_user.username)) - - existing_subscription = Subscription.query.filter_by(user_id=current_user.id, author_id=author_id).first() - if existing_subscription: - return redirect(url_for('profile', username=current_user.username)) - - new_subscription = Subscription(user_id=current_user.id, author_id=author_id) - db.session.add(new_subscription) - db.session.commit() - - return redirect(url_for('profile', username=current_user.username)) - -@app.route('/unsubscribe/', methods=['POST']) -@login_required -def unsubscribe(author_id): - - subscription = Subscription.query.filter_by( - user_id=current_user.id, author_id=author_id).first() - if subscription: - db.session.delete(subscription) - db.session.commit() - return redirect(url_for('profile', username=User.query.get(author_id).username)) - -@app.route('/delete_post/', methods=['POST']) -@login_required -def delete_post(post_id): - try: - validate_csrf(request.form.get('csrf_token')) - except BadRequest: - return redirect(url_for('user_posts', username=current_user.username)) - - post = Post.query.get_or_404(post_id) - if post.username != current_user.username: - abort(403) - - if post.media_file: - media_path = os.path.join(app.config['UPLOAD_FOLDER']['posts'], post.media_file) - if os.path.exists(media_path): - os.remove(media_path) - db.session.delete(post) - db.session.commit() - return redirect(url_for('user_posts', username=current_user.username)) - -@app.route('/privacy_policy') -def privacy_policy(): - return render_template('privacy_policy.html') - -@app.route('/terms_of_use') -def terms_of_use(): - return render_template('terms_of_use.html') - -@app.route('/publication_rules') -def publication_rules(): - return render_template('publication_rules.html') - -if __name__ == '__main__': - with app.app_context(): - db.create_all() - app.run(debug=False) \ No newline at end of file +import os +import io +import re +import uuid +import shutil +import random +import asyncio +from datetime import datetime +import requests +from PIL import Image as PILImage +from sqlalchemy.exc import IntegrityError +from werkzeug.exceptions import BadRequest +from werkzeug.security import generate_password_hash, check_password_hash +from werkzeug.utils import secure_filename +from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify +from flask_sqlalchemy import SQLAlchemy +from flask_bcrypt import Bcrypt +from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user +from flask_wtf import FlaskForm, RecaptchaField, CSRFProtect +from flask_wtf.file import FileAllowed +from flask_wtf.csrf import validate_csrf +from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField, RadioField, SelectField, TextAreaField +from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Regexp +from dotenv import load_dotenv, find_dotenv +import aiofiles.os +from sqlalchemy import func, or_ +import magic +from config import Config +from models import db, bcrypt, User, Comments, Image, Votes, VideoVotes, Video, Comic, ComicPage, ComicVotes, Cookies, Views, Item, UserItem, Post, Subscription, EditTagsForm, EditVideoForm, RegistrationForm, LoginForm, EmptyForm +from admin import register_admin_routes +from upload import upload_bp +from utils import allowed_file, check_file_content, check_file_size, convert_to_webp, generate_unique_filename, get_content_query, get_client_ip, get_autocomplete_suggestions +from auth import auth_bp + +app = Flask(__name__) +csrf = CSRFProtect(app) + +app.config.from_object(Config) + +db.init_app(app) +bcrypt.init_app(app) +login_manager = LoginManager(app) +login_manager.login_view = 'auth.login' + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + +register_admin_routes(app) +app.register_blueprint(upload_bp) +app.register_blueprint(auth_bp) + +@app.route('/') +def index(): + page = request.args.get('page', 1, type=int) + search_query = request.args.get('search') + + subscriptions = [] + if current_user.is_authenticated: + + subscriptions = [ + sub.author.username + for sub in Subscription.query.filter_by(user_id=current_user.id).all() + ] + + query = get_content_query(Image, subscriptions, search_query) + pagination = query.paginate(page=page, per_page=25, error_out=False) + + user_cookies = 0 + if current_user.is_authenticated: + user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() + user_cookies = user_cookies_record.cookies if user_cookies_record else 0 + + return render_template( + 'index.html', + images=pagination.items, + pagination=pagination, + user_cookies=user_cookies, + search_query=search_query, + content_type='art' + ) + +@app.route('/arts') +def arts(): + return render_template( + 'arts.html' + ) + +@app.route('/catalogue/arts') +def arts_catalogue(): + return render_template( + 'arts-catalogue.html' + ) + +@app.route('/vote_art/', methods=['POST']) +@login_required +def vote_art(image_id): + image = Image.query.get_or_404(image_id) + user_cookies = Cookies.query.filter_by(username=current_user.username).first() + + if image.username == current_user.username: + return redirect(url_for('view', content_type='art', id=image_id)) + + if user_cookies and user_cookies.cookies > 0: + existing_vote = Votes.query.filter_by(username=current_user.username, image_id=image_id).first() + if not existing_vote: + user_cookies.cookies -= 1 + image.cookie_votes += 1 + + new_vote = Votes(username=current_user.username, image_id=image.id) + db.session.add(new_vote) + + db.session.commit() + + return redirect(url_for('view', content_type='art', id=image_id)) + +@app.route('/view//', methods=['GET', 'POST']) +def view(content_type, id): + comments = [] + avatars = {user.username: user.avatar_file for user in User.query.all()} + cu = current_user.username if current_user.is_authenticated else None + + user_cookies = 0 + if current_user.is_authenticated: + user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() + user_cookies = user_cookies_record.cookies if user_cookies_record else 0 + + if content_type == 'art': + content = Image.query.get_or_404(id) + comments = Comments.query.filter_by(image_id=id).order_by(Comments.comment_date.desc()).all() + + if current_user.is_authenticated: + existing_view = Views.query.filter_by(image_id=id, username=current_user.username).first() + if not existing_view: + new_view = Views(image_id=id, username=current_user.username) + db.session.add(new_view) + db.session.commit() + + search_query = request.args.get('search') + page = request.args.get('page', 1, type=int) + subscriptions = [] + + if current_user.is_authenticated: + subscriptions = [ + sub.author.username + for sub in Subscription.query.filter_by(user_id=current_user.id).all() + ] + + query = get_content_query(Image, subscriptions, search_query) + all_images = query.all() + image_ids = [image.id for image in all_images] + + current_index = image_ids.index(id) + prev_index = (current_index - 1) % len(image_ids) + next_index = (current_index + 1) % len(image_ids) + + prev_content = all_images[prev_index] + next_content = all_images[next_index] + random_content = random.choice(all_images) + + elif content_type == 'video': + content = Video.query.get_or_404(id) + comments = Comments.query.filter_by(video_id=id).order_by(Comments.comment_date.desc()).all() + + if current_user.is_authenticated: + existing_view = Views.query.filter_by(video_id=id, username=current_user.username).first() + if not existing_view: + new_view = Views(video_id=id, username=current_user.username) + db.session.add(new_view) + db.session.commit() + + all_videos = Video.query.order_by(Video.id).all() + video_ids = [video.id for video in all_videos] + current_index = video_ids.index(id) + prev_index = (current_index - 1) % len(all_videos) + next_index = (current_index + 1) % len(all_videos) + + prev_content = all_videos[prev_index] + next_content = all_videos[next_index] + + random_content = random.choice(all_videos) + + elif content_type == 'comic': + content = Comic.query.get_or_404(id) + comments = Comments.query.filter_by(comic_id=id).order_by(Comments.comment_date.desc()).all() + + if current_user.is_authenticated: + existing_view = Views.query.filter_by(image_id=id, username=current_user.username).first() + if not existing_view: + new_view = Views(image_id=id, username=current_user.username) + db.session.add(new_view) + db.session.commit() + + comic_pages = ComicPage.query.filter_by(comic_id=id).order_by(ComicPage.page_number).all() + + if not comic_pages: + return render_template('error.html', message="Comic pages not found") + + all_comics = Comic.query.order_by(Comic.id).all() + comic_ids = [comic.id for comic in all_comics] + current_index = comic_ids.index(id) + prev_index = (current_index - 1) % len(all_comics) + next_index = (current_index + 1) % len(all_comics) + + prev_content = all_comics[prev_index] + next_content = all_comics[next_index] + + random_content = random.choice(all_comics) + + else: + abort(404) + + if request.method == 'POST' and current_user.is_authenticated: + comment_text = request.form.get('comment') + max_comment_length = 44 + if comment_text and len(comment_text) > max_comment_length: + return redirect(url_for('view', content_type=content_type, id=id)) + + if comment_text: + new_comment = None + if content_type == 'art': + new_comment = Comments(username=cu, image_id=id, comment_text=comment_text) + elif content_type == 'video': + new_comment = Comments(username=cu, video_id=id, comment_text=comment_text) + elif content_type == 'comic': + new_comment = Comments(username=cu, comic_id=id, comment_text=comment_text) + + if new_comment: + db.session.add(new_comment) + db.session.commit() + return redirect(url_for('view', content_type=content_type, id=id)) + + return render_template( + 'view.html', + content=content, + content_type=content_type, + comments=comments, + avatars=avatars, + prev_content=prev_content, + next_content=next_content, + random_content=random_content, + user_cookies=user_cookies, + comic_pages=comic_pages if content_type == 'comic' else None + ) + +@app.route('/images') +def drawings(): + return render_template( + 'images.html' + ) + +@app.route('/tags_list/') +def tags_list(page_type): + comics_tags = [comic.tags for comic in Comic.query.all() if comic.tags] + images_tags = [image.tags for image in Image.query.all() if image.tags] + videos_tags = [video.tags for video in Video.query.all() if video.tags] + + if page_type == 'video': + all_tags = [tag.strip() for tags in videos_tags for tag in tags.split(',')] + elif page_type == 'comic': + all_tags = [tag.strip() for tags in comics_tags for tag in tags.split(',')] + elif page_type == 'image': + all_tags = [tag.strip() for tags in images_tags for tag in tags.split(',')] + else: + all_tags = set(tag.strip() for tags in (comics_tags + images_tags + videos_tags) for tag in tags.split(',')) + + sorted_tags = sorted(set(all_tags)) + + return render_template('tags_list.html', tags=sorted_tags, page_type=page_type) + +@app.route('/subnav') +def subnav(): + return render_template( + 'subnav.html' + ) + +@app.route('/image_edit/', methods=['GET', 'POST']) +@login_required +def image_edit(id): + + image = Image.query.get_or_404(id) + + if image.username != current_user.username: + return redirect(url_for('index')) + + form = EditTagsForm() + + if form.validate_on_submit(): + + image.tags = form.tags.data + db.session.commit() + return redirect(url_for('view', content_type='art', id=id)) + + if request.method == 'GET': + form.tags.data = image.tags + + image_preview_url = url_for('static', filename=f'arts/{image.image_file}') + + return render_template( + 'image_edit.html', + form=form, + image=image, + image_preview_url=image_preview_url + ) + +@app.route('/video_edit/', methods=['GET', 'POST']) +@login_required +async def video_edit(id): + video = Video.query.get_or_404(id) + + if video.username != current_user.username: + return redirect(url_for('index')) + + form = EditVideoForm() + + if request.method == 'GET': + form.video_name.data = video.video_name + form.description.data = video.description + form.tags.data = video.tags + + if form.validate_on_submit(): + video.video_name = form.video_name.data + video.description = form.description.data + video.tags = form.tags.data + + if form.video_thumbnail.data: + thumbnail_file = form.video_thumbnail.data + if allowed_file(thumbnail_file.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): + + thumbnail_filename = await generate_unique_filename( + app.config['UPLOAD_FOLDER']['thumbnails'], 'webp' + ) + thumbnail_path = os.path.join(app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename) + + webp_thumbnail = await convert_to_webp(thumbnail_file) + async with aiofiles.open(thumbnail_path, 'wb') as f: + await f.write(webp_thumbnail.read()) + + video.video_thumbnail_file = thumbnail_filename + + db.session.commit() + return redirect(url_for('view', content_type='video', id=id)) + + video_preview_url = url_for('static', filename=f'videos/{video.video_thumbnail_file}') + + return render_template( + 'video_edit.html', + form=form, + video=video, + video_preview_url=video_preview_url + ) + +@app.route('/navbar') +def navbar(): + return render_template( + 'navbar.html' +) + +@app.route('/card') +def card(): + return render_template( + 'card.html' + ) + +@app.route('/gifs') +def gifs(): + return render_template( + 'gifs.html' + ) + +@app.route('/autocomplete') +def autocomplete(): + search_query = request.args.get('search', '', type=str) + + if search_query: + + suggestions = get_autocomplete_suggestions(search_query) + else: + suggestions = [] + + return jsonify(suggestions) + +@app.route('/user_pubs//') +def user_pubs(pub_type, username): + p = request.args.get('page', 1, type=int) + search_query = request.args.get('search', '') + + if pub_type == 'arts': + query = Image.query.filter_by(username=username) + if search_query: + + query = query.filter(or_( + Image.image_file.ilike(f'%{search_query}%'), + Image.tags.ilike(f'%{search_query}%') + )) + items = query.order_by(Image.publication_date.desc()).paginate(page=p, per_page=25, error_out=False) + + elif pub_type == 'videos': + query = Video.query.filter_by(username=username) + if search_query: + + query = query.filter(Video.video_name.ilike(f'%{search_query}%')) + items = query.order_by(Video.publication_date.desc()).paginate(page=p, per_page=25, error_out=False) + + elif pub_type == 'comics': + query = Comic.query.filter_by(username=username) + if search_query: + + query = query.filter(Comic.name.ilike(f'%{search_query}%')) + items = query.order_by(Comic.publication_date.desc()).paginate(page=p, per_page=25, error_out=False) + + else: + abort(404) + + user_cookies = Cookies.query.filter_by(username=username).first() + cookies_count = user_cookies.cookies if user_cookies else 0 + + return render_template( + 'user_pubs.html', + items=items.items, + pagination=items, + pub_type=pub_type, + username=username, + cookies_count=cookies_count, + search_query=search_query + ) + +@app.route('/delete//', methods=['POST']) +@login_required +def delete(content_type, content_id): + if content_type == 'art': + content = Image.query.get_or_404(content_id) + file_path = os.path.join(app.config['UPLOAD_FOLDER']['arts'], content.image_file) + elif content_type == 'video': + content = Video.query.get_or_404(content_id) + file_path = os.path.join(app.config['UPLOAD_FOLDER']['videos'], content.video_file) + elif content_type == 'comic': + content = Comic.query.get_or_404(content_id) + file_path = os.path.join(app.config['UPLOAD_FOLDER']['comics'], content.comic_folder) + else: + abort(404, "Invalid content type") + + if content.username != current_user.username: + abort(403) + + if os.path.exists(file_path): + if os.path.isdir(file_path): + shutil.rmtree(file_path) + else: + os.remove(file_path) + + db.session.delete(content) + db.session.commit() + return redirect(url_for('profile', username=current_user.username)) + + +@app.route('/delete_comment/', methods=['POST']) +@login_required +def delete_comment(comment_id): + comment = db.session.get(Comments, comment_id) or abort(404) + + if comment.image_id: + content_type = 'art' + content_id = comment.image_id + elif comment.video_id: + content_type = 'video' + content_id = comment.video_id + else: + content_type = 'comic' + content_id = comment.comic_id + + if comment.username == current_user.username: + try: + if hasattr(comment, 'post_id') and comment.post_id: + post_id = comment.post_id + db.session.delete(comment) + db.session.commit() + return redirect(url_for('user_posts', username=current_user.username)) + + db.session.delete(comment) + db.session.commit() + return redirect(url_for('view', content_type=content_type, id=content_id)) + except IntegrityError: + db.session.rollback() + return redirect(url_for('view', content_type=content_type, id=content_id)) + + return redirect(request.referrer or url_for('index')) + +@app.route('/videos') +def videos(): + page = request.args.get('page', 1, type=int) + search_query = request.args.get('search') + + subscriptions = [] + if current_user.is_authenticated: + subscriptions = [sub.author_id for sub in Subscription.query.filter_by(user_id=current_user.id).all()] + + query = get_content_query(Video, subscriptions, search_query) + pagination = query.paginate(page=page, per_page=10, error_out=False) + + videos_with_views = [] + for video in pagination.items: + views_count = db.session.query(func.count(Views.id)).filter(Views.video_id == video.id).scalar() + videos_with_views.append({ + 'video': video, + 'views_count': views_count + }) + + popular_videos = [ + { + 'video': video[0], + 'views_count': video[1] + } + for video in db.session.query(Video, func.count(Views.id).label('views_count')) + .outerjoin(Views, Views.video_id == Video.id) + .group_by(Video.id) + .order_by(func.count(Views.id).desc(), Video.cookie_votes.desc()) + .limit(8) + .all() + ] + + most_viewed_videos = [ + { + 'video': video[0], + 'views_count': video[1] + } + for video in db.session.query(Video, func.count(Views.id).label('views_count')) + .outerjoin(Views, Views.video_id == Video.id) + .group_by(Video.id) + .order_by(func.count(Views.id).desc()) + .limit(8) + .all() + ] + + videos_tags = [video.tags for video in Video.query.all() if video.tags] + all_tags = [tag.strip() for tags in videos_tags for tag in tags.split(',')] + sorted_tags = sorted(set(all_tags)) + + user_cookies = 0 + if current_user.is_authenticated: + user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() + user_cookies = user_cookies_record.cookies if user_cookies_record else 0 + + return render_template( + 'videos.html', + videos=videos_with_views, + pagination=pagination, + user_cookies=user_cookies, + search_query=search_query, + content_type='video', + tags=sorted_tags, + popular_videos=popular_videos, + most_viewed_videos=most_viewed_videos + ) + + +@app.route('/vote_video/', methods=['POST']) +@login_required +def vote_video(video_id): + video = Video.query.get_or_404(video_id) + user_cookies = Cookies.query.filter_by(username=current_user.username).first() + + if video.username == current_user.username: + return redirect(url_for('view', content_type='video', id=video_id)) + + if user_cookies and user_cookies.cookies > 0: + existing_vote = VideoVotes.query.filter_by(username=current_user.username, video_id=video_id).first() + if not existing_vote: + user_cookies.cookies -= 1 + video.cookie_votes += 1 + new_vote = VideoVotes(username=current_user.username, video_id=video.id) + db.session.add(new_vote) + db.session.commit() + + return redirect(url_for('view', content_type='video', id=video_id)) + +@app.route('/comics') +def comics(): + page = request.args.get('page', 1, type=int) + search_query = request.args.get('search') + + subscriptions = [] + if current_user.is_authenticated: + subscriptions = [sub.author_id for sub in Subscription.query.filter_by(user_id=current_user.id).all()] + + query = get_content_query(Comic, subscriptions, search_query) + pagination = query.paginate(page=page, per_page=10, error_out=False) + + user_cookies = 0 + if current_user.is_authenticated: + user_cookies_record = Cookies.query.filter_by(username=current_user.username).first() + user_cookies = user_cookies_record.cookies if user_cookies_record else 0 + + return render_template( + 'comics.html', + comics=pagination.items, + pagination=pagination, + user_cookies=user_cookies, + search_query=search_query, + content_type='comic' + ) + +@app.route('/vote_comic/', methods=['POST']) +@login_required +def vote_comic(comic_id): + comic = Comic.query.get_or_404(comic_id) + user_cookies = Cookies.query.filter_by(username=current_user.username).first() + + if comic.username == current_user.username: + return redirect(url_for('view', content_type='comic', id=comic_id)) + + if user_cookies and user_cookies.cookies > 0: + existing_vote = ComicVotes.query.filter_by(username=current_user.username, comic_id=comic_id).first() + if not existing_vote: + user_cookies.cookies -= 1 + comic.cookie_votes += 1 + new_vote = ComicVotes(username=current_user.username, comic_id=comic.id) + db.session.add(new_vote) + db.session.commit() + + return redirect(url_for('view', content_type='comic', id=comic_id)) + +@app.route('/comic_edit/', methods=['GET', 'POST']) +@login_required +async def comic_edit(comic_id): + comic = Comic.query.get_or_404(comic_id) + + if comic.username != current_user.username: + return redirect(url_for('index')) + + cfp = os.path.join(app.config['UPLOAD_FOLDER']['comics'], comic.name) + if not os.path.exists(cfp): + os.makedirs(cfp) + + form = EmptyForm() + + if request.method == 'POST' and form.validate_on_submit(): + action = request.form.get('action') + + if action == 'delete' and (page_id := request.form.get('page')): + page = ComicPage.query.get(page_id) + if page: + os.remove(page.file_path.replace('\\', '/')) + db.session.delete(page) + db.session.commit() + pages = ComicPage.query.filter_by(comic_id=comic.id).order_by(ComicPage.page_number).all() + for i, page in enumerate(pages, start=1): + page.page_number = i + db.session.commit() + + elif action == 'update' and (page_id := request.form.get('page')) and 'new_page' in request.files: + new_page = request.files['new_page'] + filename = f"{uuid.uuid4().hex}.webp" + file_path = os.path.join(cfp, filename).replace('\\', '/') + webp_image = await convert_to_webp(new_page) + async with aiofiles.open(file_path, 'wb') as f: + await f.write(webp_image.read()) + + page = ComicPage.query.get(page_id) + if page: + os.remove(page.file_path.replace('\\', '/')) + page.file_path = file_path + db.session.commit() + + elif action == 'add' and 'new_page' in request.files: + new_page = request.files['new_page'] + filename = f"{uuid.uuid4().hex}.webp" + file_path = os.path.join(cfp, filename).replace('\\', '/') + webp_image = await convert_to_webp(new_page) + async with aiofiles.open(file_path, 'wb') as f: + await f.write(webp_image.read()) + + page_number = (db.session.query(db.func.max(ComicPage.page_number)).filter_by(comic_id=comic.id).scalar() or 0) + 1 + db.session.add(ComicPage(comic_id=comic.id, page_number=page_number, file_path=file_path)) + db.session.commit() + + return redirect(url_for('comic_edit', comic_id=comic.id)) + + comic_pages = ComicPage.query.filter_by(comic_id=comic.id).order_by(ComicPage.page_number).all() + return render_template('comic_edit.html', comic=comic, comic_pages=comic_pages, form=form) + +def update_related_tables(old_username, new_username): + + Comments.query.filter_by(username=old_username).update({"username": new_username}) + + Image.query.filter_by(username=old_username).update({"username": new_username}) + + Video.query.filter_by(username=old_username).update({"username": new_username}) + + Comic.query.filter_by(username=old_username).update({"username": new_username}) + + Cookies.query.filter_by(username=old_username).update({"username": new_username}) + + Post.query.filter_by(username=old_username).update({"username": new_username}) + + Subscription.query.filter_by(user_id=old_username).update({"user_id": new_username}) + Subscription.query.filter_by(author_id=old_username).update({"author_id": new_username}) + + UserItem.query.filter_by(username=old_username).update({"username": new_username}) + + db.session.commit() + +@app.route('/profile_edit', methods=['GET', 'POST']) +@login_required +def profile_edit(): + if request.method == 'POST': + section = request.form.get('section') + + if section == 'avatar_banner': + + avatar = request.files.get('avatar') + banner = request.files.get('banner') + + if avatar and allowed_file(avatar.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): + if check_file_size(avatar, app.config['MAX_IMAGE_SIZE']): + avatar_filename = secure_filename(avatar.filename) + avatar_path = os.path.join(app.config['UPLOAD_FOLDER']['avatars'], avatar_filename) + avatar.save(avatar_path) + current_user.avatar_file = avatar_filename + else: + return redirect(url_for('profile_edit')) + + if banner and allowed_file(banner.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): + if check_file_size(banner, app.config['MAX_IMAGE_SIZE']): + banner_filename = secure_filename(banner.filename) + banner_path = os.path.join(app.config['UPLOAD_FOLDER']['banners'], banner_filename) + banner.save(banner_path) + current_user.banner_file = banner_filename + else: + return redirect(url_for('profile_edit')) + + db.session.commit() + + return redirect(url_for('profile', username=current_user.username)) + + elif section == 'username': + + new_username = request.form.get('new_username') + if new_username and new_username != current_user.username: + if len(new_username) < 3 or len(new_username) > 20: + pass + elif User.query.filter_by(username=new_username).first(): + pass + else: + old_username = current_user.username + current_user.username = new_username + update_related_tables(old_username, new_username) + return redirect(url_for('profile', username=new_username)) + + elif section == 'bio': + + bio = request.form.get('bio') + current_user.bio = bio + db.session.commit() + + elif section == 'password': + + current_password = request.form.get('current_password') + new_password = request.form.get('new_password') + confirm_password = request.form.get('confirm_new_password') + + if not current_user.check_password(current_password): + pass + elif new_password != confirm_password: + pass + elif len(new_password) < 6: + pass + else: + current_user.encrypted_password = bcrypt.generate_password_hash(new_password).decode('utf-8') + db.session.commit() + return redirect(url_for('profile_edit')) + + elif section == 'create_post': + + post_text = request.form.get('post_text') + post_media = request.files.get('post_media') + + if post_text: + new_post = Post( + username=current_user.username, + text=post_text + ) + db.session.add(new_post) + + if post_media and allowed_file(post_media.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): + if check_file_size(post_media, app.config['MAX_IMAGE_SIZE']): + media_filename = secure_filename(post_media.filename) + media_path = os.path.join(app.config['UPLOAD_FOLDER']['posts'], media_filename) + post_media.save(media_path) + new_post.media_file = media_filename + db.session.commit() + pass + else: + return redirect(url_for('profile_edit')) + + db.session.commit() + return redirect(url_for('user_posts', username=current_user.username)) + + elif section == 'decoration': + selected_item_id = request.form.get('selected_item') + if selected_item_id: + + selected_item = UserItem.query.filter_by(username=current_user.username, item_id=selected_item_id).first() + + if selected_item: + + current_user.current_item = selected_item.item.id + db.session.commit() + pass + else: + pass + return redirect(url_for('profile', username=current_user.username)) + + db.session.commit() + return redirect(url_for('profile_edit')) + + return render_template( + 'profile_edit.html', + user=current_user, + user_items=UserItem.query.filter_by(username=current_user.username).all() + ) + +@app.route('/profile/') +def profile(username): + user = User.query.filter_by(username=username).first_or_404() + + if not user.avatar_file: + user.avatar_file = 'default-avatar.png' + + if not user.banner_file: + user.banner_file = 'default-banner.png' + + total_arts = Image.query.filter_by(username=username).count() + total_videos = Video.query.filter_by(username=username).count() + total_comics = Comic.query.filter_by(username=username).count() + + arts = Image.query.filter_by(username=username).order_by(Image.cookie_votes.desc(), Image.publication_date.desc()).limit(3).all() + videos = Video.query.filter_by(username=username).order_by(Video.cookie_votes.desc(), Video.publication_date.desc()).limit(3).all() + comics = Comic.query.filter_by(username=username).order_by(Comic.cookie_votes.desc(), Comic.publication_date.desc()).limit(3).all() + + subscribed = ( + current_user.is_authenticated and + Subscription.query.filter_by(user_id=current_user.id, author_id=user.id).first() is not None + ) + + current_item = Item.query.get(user.current_item) if user.current_item else None + + is_current_user = current_user.is_authenticated and current_user.username == user.username + + subscriptions_count = Subscription.query.filter_by(author_id=user.id).count() + + return render_template( + 'profile.html', + user=user, + arts=arts, + videos=videos, + comics=comics, + total_arts=total_arts, + total_videos=total_videos, + total_comics=total_comics, + current_item=current_item, + is_current_user=is_current_user, + subscribed=subscribed, + subscriptions_count=subscriptions_count + ) + +@app.route('/posts/') +def user_posts(username): + user = User.query.filter_by(username=username).first_or_404() + posts = Post.query.filter_by(username=username).order_by(Post.post_date.desc()).all() + avatars = {user.username: user.avatar_file for user in User.query.all()} + return render_template('post.html', username=username, posts=posts, avatars=avatars) + +@app.route('/posts//add_comment/', methods=['POST']) +@login_required +def add_comment(username, post_id): + comment_text = request.form.get('comment_text') + if not comment_text: + return redirect(url_for('user_posts', username=username)) + + comment = Comments( + username=current_user.username, + post_id=post_id, + comment_text=comment_text, + comment_date=datetime.utcnow() + ) + db.session.add(comment) + db.session.commit() + return redirect(url_for('user_posts', username=username)) + +@app.route('/subscribe/', methods=['POST']) +@login_required +def subscribe(author_id): + if author_id == current_user.id: + return redirect(url_for('profile', username=current_user.username)) + + existing_subscription = Subscription.query.filter_by(user_id=current_user.id, author_id=author_id).first() + if existing_subscription: + return redirect(url_for('profile', username=current_user.username)) + + new_subscription = Subscription(user_id=current_user.id, author_id=author_id) + db.session.add(new_subscription) + db.session.commit() + + return redirect(url_for('profile', username=current_user.username)) + +@app.route('/unsubscribe/', methods=['POST']) +@login_required +def unsubscribe(author_id): + + subscription = Subscription.query.filter_by( + user_id=current_user.id, author_id=author_id).first() + if subscription: + db.session.delete(subscription) + db.session.commit() + return redirect(url_for('profile', username=User.query.get(author_id).username)) + +@app.route('/delete_post/', methods=['POST']) +@login_required +def delete_post(post_id): + try: + validate_csrf(request.form.get('csrf_token')) + except BadRequest: + return redirect(url_for('user_posts', username=current_user.username)) + + post = Post.query.get_or_404(post_id) + if post.username != current_user.username: + abort(403) + + if post.media_file: + media_path = os.path.join(app.config['UPLOAD_FOLDER']['posts'], post.media_file) + if os.path.exists(media_path): + os.remove(media_path) + db.session.delete(post) + db.session.commit() + return redirect(url_for('user_posts', username=current_user.username)) + +@app.route('/privacy_policy') +def privacy_policy(): + return render_template('privacy_policy.html') + +@app.route('/terms_of_use') +def terms_of_use(): + return render_template('terms_of_use.html') + +@app.route('/publication_rules') +def publication_rules(): + return render_template('publication_rules.html') + +@app.context_processor +def inject_forms(): + return dict(form=RegistrationForm()) + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + app.run(debug=True) \ No newline at end of file diff --git a/auth.py b/auth.py index 6076c15..00d47ec 100644 --- a/auth.py +++ b/auth.py @@ -1,66 +1,78 @@ -from flask import Blueprint, render_template, redirect, url_for, request -from flask_login import login_user, logout_user, login_required, current_user -from sqlalchemy.exc import IntegrityError -from models import db, User -from utils import get_client_ip -from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField -from flask_bcrypt import Bcrypt -from wtforms.validators import DataRequired, Length, EqualTo - -auth_bp = Blueprint('auth', __name__) -bcrypt = Bcrypt() - -password = PasswordField('Password', validators=[DataRequired(), Length(min=6)]) -confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) -recaptcha = RecaptchaField() -submit = SubmitField('Register') - -@auth_bp.route('/register', methods=['GET', 'POST']) -def register(): - form = RegistrationForm() - - if form.validate_on_submit(): - hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') - - ip_address = get_client_ip() - - existing_user = User.query.filter_by(ip_address=ip_address).first() - if existing_user: - return render_template('register.html', form=form) - - username = form.username.data.lower() - - user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address) - - try: - db.session.add(user) - db.session.commit() - return redirect(url_for('auth.login')) - except IntegrityError: - db.session.rollback() - - return render_template('register.html', form=form) - -@auth_bp.route('/login', methods=['GET', 'POST']) -def login(): - form = LoginForm() - - if form.validate_on_submit(): - user = User.query.filter_by(username=form.username.data).first() - - if user and user.check_password(form.password.data): - login_user(user) - - if user.ip_address is None: - ip_address = get_client_ip() - user.ip_address = ip_address - db.session.commit() - - return redirect(url_for('profile', username=user.username)) - - return render_template('login.html', form=form) - -@auth_bp.route('/logout') -def logout(): - logout_user() +from flask import Blueprint, render_template, redirect, url_for, request +from flask_login import login_user, logout_user, login_required, current_user +from sqlalchemy.exc import IntegrityError +from models import db, User +from utils import get_client_ip +from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField +from flask_bcrypt import Bcrypt +from wtforms.validators import DataRequired, Length, EqualTo +from config import Config +auth_bp = Blueprint('auth', __name__) +bcrypt = Bcrypt() + +password = PasswordField('Password', validators=[DataRequired(), Length(min=6)]) +confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) +recaptcha = RecaptchaField() +submit = SubmitField('Register') + +@auth_bp.route('/register', methods=['GET', 'POST']) +def register(): + form = RegistrationForm() + + if form.validate_on_submit(): + hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') + ip_address = get_client_ip() + username = form.username.data.lower() + + existing_user = User.query.filter_by(ip_address=ip_address).first() + user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address) + + try: + db.session.add(user) + db.session.commit() + return redirect(url_for('auth.login')) + except IntegrityError: + db.session.rollback() + + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return render_template('register-modal.html', form=form) + + return render_template('register.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY) + + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + form = LoginForm() + + if form.validate_on_submit(): + user = User.query.filter_by(username=form.username.data).first() + if user and user.check_password(form.password.data): + login_user(user) + if user.ip_address is None: + ip_address = get_client_ip() + user.ip_address = ip_address + db.session.commit() + return redirect(url_for('profile', username=user.username)) + + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return render_template('login-modal.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY) + + return render_template('login.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY) + + + +@auth_bp.route('/register-modal') +def register_modal(): + form = RegistrationForm() + return render_template('register-modal.html', form=form) + +@auth_bp.route('/login-modal') +def login_modal(): + form = LoginForm() + return render_template('login-modal.html', form=form) + + +@auth_bp.route('/logout') +def logout(): + logout_user() return redirect(url_for('index')) \ No newline at end of file diff --git a/config.py b/config.py index a97db24..d280d81 100644 --- a/config.py +++ b/config.py @@ -1,27 +1,27 @@ -import os -from dotenv import load_dotenv, find_dotenv - -dotenv_path = find_dotenv() -load_dotenv(dotenv_path, override=True) - -class Config: - SECRET_KEY = os.getenv('SECRET_KEY') - WTF_CSRF_ENABLED = True - RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY') - RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY') - SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI') - UPLOAD_FOLDER = { - 'images': 'static/arts/', - 'arts': 'static/arts/', - 'videos': 'static/videos/', - 'thumbnails': 'static/thumbnails/', - 'avatars': 'static/avatars/', - 'banners': 'static/banners/', - 'comics': 'static/comics', - 'comicthumbs': 'static/comicthumbs/', - 'posts': 'static/posts/' - } - ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} - ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'avi', 'mov'} - MAX_IMAGE_SIZE = 15 * 1024 * 1024 +import os +from dotenv import load_dotenv, find_dotenv + +dotenv_path = find_dotenv() +load_dotenv(dotenv_path, override=True) + +class Config: + SECRET_KEY = os.getenv('SECRET_KEY') + WTF_CSRF_ENABLED = True + RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY') + RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY') + SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI') + UPLOAD_FOLDER = { + 'images': 'static/arts/', + 'arts': 'static/arts/', + 'videos': 'static/videos/', + 'thumbnails': 'static/thumbnails/', + 'avatars': 'static/avatars/', + 'banners': 'static/banners/', + 'comics': 'static/comics', + 'comicthumbs': 'static/comicthumbs/', + 'posts': 'static/posts/' + } + ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} + ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'avi', 'mov'} + MAX_IMAGE_SIZE = 15 * 1024 * 1024 MAX_VIDEO_SIZE = 10 * 1024 * 1024 * 1024 \ No newline at end of file diff --git a/license b/license index 2a1c41e..154e876 100644 --- a/license +++ b/license @@ -1,9 +1,9 @@ -Copyright (c) 2025 artberry.xyz - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - +Copyright (c) 2025 artberry.xyz + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + The "artberry.xyz" brand, logo, and trademarks are protected and may not be used without explicit permission from the copyright holder. This license does not grant any rights to use the brand or trademarks associated with "artberry.xyz". \ No newline at end of file diff --git a/models.py b/models.py index 0eccffe..4673a7b 100644 --- a/models.py +++ b/models.py @@ -1,266 +1,264 @@ -import os -import io -import re -import uuid -import shutil -import random -import asyncio -from datetime import datetime -import requests -from PIL import Image as PILImage -from sqlalchemy.exc import IntegrityError -from werkzeug.exceptions import BadRequest -from werkzeug.security import generate_password_hash, check_password_hash -from werkzeug.utils import secure_filename -from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify -from flask_sqlalchemy import SQLAlchemy -from flask_bcrypt import Bcrypt -from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user -from flask_wtf import FlaskForm, RecaptchaField, CSRFProtect -from flask_wtf.file import FileAllowed -from flask_wtf.csrf import validate_csrf -from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField, RadioField, SelectField, TextAreaField -from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Regexp -from dotenv import load_dotenv, find_dotenv -import aiofiles.os -from sqlalchemy import func, or_ -import magic -from config import Config -from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField -from wtforms.validators import DataRequired -from flask_wtf.file import FileAllowed - -db = SQLAlchemy() -bcrypt = Bcrypt() - -class Comments(db.Model): - __tablename__ = 'comments' - - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String, nullable=False) - image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True) - video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True) - comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=True) - post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=True) - comment_text = db.Column(db.Text, nullable=False) - comment_date = db.Column(db.DateTime, default=datetime.utcnow) - - image = db.relationship('Image', back_populates='comments') - video = db.relationship('Video', back_populates='comments') - comic = db.relationship('Comic', back_populates='comments', overlaps="comic_link") - post = db.relationship('Post', backref='comments') - -class User(db.Model, UserMixin): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(20), unique=True, nullable=False) - encrypted_password = db.Column(db.String(60), nullable=False) - ip_address = db.Column(db.String(15), nullable=True) - avatar_file = db.Column(db.String(50), nullable=True) - banner_file = db.Column(db.String(50), nullable=True) - bio = db.Column(db.Text, nullable=True) - current_item = db.Column(db.String(30), nullable=True) - - def __repr__(self): - return f'' - - def check_password(self, password): - return bcrypt.check_password_hash(self.encrypted_password, password) - -class Image(db.Model): - __tablename__ = 'image' - - id = db.Column(db.Integer, primary_key=True) - image_file = db.Column(db.String(40), nullable=False) - username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) - publication_date = db.Column(db.DateTime, default=datetime.utcnow) - tags = db.Column(db.String(100), nullable=True) - cookie_votes = db.Column(db.Integer, default=0) - - comments = db.relationship('Comments', back_populates='image', cascade='all, delete-orphan') - -class Votes(db.Model): - __tablename__ = 'votes' - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(100), nullable=False) - image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=False) - - image = db.relationship('Image', backref=db.backref('votes', lazy=True)) - - def __repr__(self): - return f'' - -class VideoVotes(db.Model): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(120), nullable=False) - video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False) - -class Video(db.Model): - id = db.Column(db.Integer, primary_key=True) - video_file = db.Column(db.String(100), nullable=False) - video_name = db.Column(db.String(100), nullable=False) - video_thumbnail_file = db.Column(db.String(100), nullable=False) - username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) - publication_date = db.Column(db.DateTime, default=datetime.utcnow) - tags = db.Column(db.String(100), nullable=True) - description = db.Column(db.Text, nullable=True) - cookie_votes = db.Column(db.Integer, default=0) - - comments = db.relationship('Comments', back_populates='video') - -class Comic(db.Model): - __tablename__ = 'comics' - - id = db.Column(db.Integer, primary_key=True) - comic_folder = db.Column(db.String(100), nullable=False) - comic_thumbnail_file = db.Column(db.String(100), nullable=False) - username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) - name = db.Column(db.String(100), nullable=False, unique=True) - publication_date = db.Column(db.DateTime, default=datetime.utcnow) - tags = db.Column(db.String(100), nullable=True) - cookie_votes = db.Column(db.Integer, default=0) - - comments = db.relationship('Comments', back_populates='comic', overlaps="comic_link") - pages = db.relationship('ComicPage', back_populates='comic', cascade="all, delete-orphan") - -class ComicPage(db.Model): - __tablename__ = 'comic_pages' - - id = db.Column(db.Integer, primary_key=True) - comic_id = db.Column(db.Integer, db.ForeignKey('comics.id', ondelete='CASCADE'), nullable=False) - page_number = db.Column(db.Integer, nullable=False) - file_path = db.Column(db.String(200), nullable=False) - - comic = db.relationship('Comic', back_populates='pages') - -class ComicVotes(db.Model): - __tablename__ = 'comic_votes' - - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(100), nullable=False) - comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=False) - vote = db.Column(db.Integer) - - comic = db.relationship('Comic', backref='votes', lazy=True) - -class Cookies(db.Model): - username = db.Column(db.String(20), primary_key=True, nullable=False) - cookies = db.Column(db.Integer, default=0) - -class Views(db.Model): - id = db.Column(db.Integer, primary_key=True, autoincrement=True) - image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True) - video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True) - username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) - view_date = db.Column(db.DateTime, default=datetime.utcnow) - - __table_args__ = ( - db.UniqueConstraint('image_id', 'username', name='unique_image_view'), - db.UniqueConstraint('video_id', 'username', name='unique_video_view') - ) - -class Item(db.Model): - __tablename__ = 'items' - - id = db.Column(db.Integer, primary_key=True, autoincrement=True) - item_path = db.Column(db.String, nullable=False) - price = db.Column(db.Integer, nullable=False, default=0) - visible = db.Column(db.Boolean, default=True) - - def __repr__(self): - return f'' - -class UserItem(db.Model): - __tablename__ = 'user_items' - - username = db.Column(db.String, db.ForeignKey('user.username'), primary_key=True) - item_id = db.Column(db.Integer, db.ForeignKey('items.id'), primary_key=True) - - item = db.relationship('Item', backref=db.backref('user_items', lazy=True)) - - def __repr__(self): - return f'' - -class Post(db.Model): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) - post_date = db.Column(db.DateTime, default=datetime.utcnow) - text = db.Column(db.Text, nullable=False) - media_file = db.Column(db.String(100), nullable=True) - - user = db.relationship('User', backref=db.backref('posts', lazy=True)) - -class Subscription(db.Model): - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - - user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('subscriptions', lazy=True)) - author = db.relationship('User', foreign_keys=[author_id], backref=db.backref('followers', lazy=True)) - - def __repr__(self): - return f'' - -class UploadForm(FlaskForm): - image_file = FileField('Choose File', validators=[DataRequired()]) - tags = StringField('Tags (comma-separated)', validators=[DataRequired()]) - recaptcha = RecaptchaField() - agree_with_rules = BooleanField('I agree with the publication rules', - validators=[DataRequired(message="You must agree with the publication rules.")]) - submit = SubmitField('Upload') - -class UploadVideoForm(FlaskForm): - video_file = FileField('Video File', validators=[DataRequired()]) - thumbnail = FileField('Thumbnail', validators=[DataRequired(), FileAllowed(['jpg', 'png', 'jpeg'])]) - name = StringField('Video Name', validators=[DataRequired()]) - tags = StringField('Tags', validators=[DataRequired()]) - description = StringField('Description', validators=[DataRequired()]) - recaptcha = RecaptchaField() - submit = SubmitField('Upload') - agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired()]) - -class UploadComicForm(FlaskForm): - title = StringField('Comic Name', validators=[DataRequired()]) - thumbnail = FileField('Thumbnail', validators=[DataRequired()]) - tags = StringField('Tags (comma-separated)') - recaptcha = RecaptchaField() - agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired(message="You must agree with the publication rules.")]) - submit = SubmitField('Upload') - -class EditTagsForm(FlaskForm): - tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter tags"}) - submit = SubmitField('Save') - -class EditVideoForm(FlaskForm): - video_name = StringField('Title', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter video title"}) - video_thumbnail = FileField('Thumbnail', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Only images!')]) - description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)], render_kw={"placeholder": "Enter video description"}) - tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Tags"}) - submit = SubmitField('Save') - -class LoginForm(FlaskForm): - username = StringField('Username', validators=[DataRequired()]) - password = PasswordField('Password', validators=[DataRequired()]) - recaptcha = RecaptchaField() - submit = SubmitField('Login') - -class RegistrationForm(FlaskForm): - username = StringField( - 'Username', - validators=[ - DataRequired(), - Length(min=3, max=20), - Regexp(r'^[a-zA-Z0-9_]+$', message="Username can contain only letters, numbers, and underscores.") - ] - ) - password = PasswordField('Password', validators=[DataRequired(), Length(min=6)]) - confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) - recaptcha = RecaptchaField() - submit = SubmitField('Register') - -class EmptyForm(FlaskForm): - pass - -class UpdateCookiesForm(FlaskForm): - cookies = StringField('Количество печенек', validators=[DataRequired()]) +import os +import io +import re +import uuid +import shutil +import random +import asyncio +from datetime import datetime +import requests +from PIL import Image as PILImage +from sqlalchemy.exc import IntegrityError +from werkzeug.exceptions import BadRequest +from werkzeug.security import generate_password_hash, check_password_hash +from werkzeug.utils import secure_filename +from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify +from flask_sqlalchemy import SQLAlchemy +from flask_bcrypt import Bcrypt +from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user +from flask_wtf import FlaskForm, RecaptchaField, CSRFProtect +from flask_wtf.file import FileAllowed +from flask_wtf.csrf import validate_csrf +from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField, RadioField, SelectField, TextAreaField +from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Regexp +from dotenv import load_dotenv, find_dotenv +import aiofiles.os +from sqlalchemy import func, or_ +import magic +from config import Config +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, SubmitField, FileField, BooleanField +from wtforms.validators import DataRequired +from flask_wtf.file import FileAllowed + +db = SQLAlchemy() +bcrypt = Bcrypt() + +class Comments(db.Model): + __tablename__ = 'comments' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String, nullable=False) + image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True) + video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True) + comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=True) + post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=True) + comment_text = db.Column(db.Text, nullable=False) + comment_date = db.Column(db.DateTime, default=datetime.utcnow) + + image = db.relationship('Image', back_populates='comments') + video = db.relationship('Video', back_populates='comments') + comic = db.relationship('Comic', back_populates='comments', overlaps="comic_link") + post = db.relationship('Post', backref='comments') + +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(20), unique=True, nullable=False) + encrypted_password = db.Column(db.String(60), nullable=False) + ip_address = db.Column(db.String(15), nullable=True) + avatar_file = db.Column(db.String(50), nullable=True) + banner_file = db.Column(db.String(50), nullable=True) + bio = db.Column(db.Text, nullable=True) + current_item = db.Column(db.String(30), nullable=True) + + def __repr__(self): + return f'' + + def check_password(self, password): + return bcrypt.check_password_hash(self.encrypted_password, password) + +class Image(db.Model): + __tablename__ = 'image' + + id = db.Column(db.Integer, primary_key=True) + image_file = db.Column(db.String(40), nullable=False) + username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) + publication_date = db.Column(db.DateTime, default=datetime.utcnow) + tags = db.Column(db.String(100), nullable=True) + cookie_votes = db.Column(db.Integer, default=0) + + comments = db.relationship('Comments', back_populates='image', cascade='all, delete-orphan') + +class Votes(db.Model): + __tablename__ = 'votes' + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(100), nullable=False) + image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=False) + + image = db.relationship('Image', backref=db.backref('votes', lazy=True)) + + def __repr__(self): + return f'' + +class VideoVotes(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(120), nullable=False) + video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=False) + +class Video(db.Model): + id = db.Column(db.Integer, primary_key=True) + video_file = db.Column(db.String(100), nullable=False) + video_name = db.Column(db.String(100), nullable=False) + video_thumbnail_file = db.Column(db.String(100), nullable=False) + username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) + publication_date = db.Column(db.DateTime, default=datetime.utcnow) + tags = db.Column(db.String(100), nullable=True) + description = db.Column(db.Text, nullable=True) + cookie_votes = db.Column(db.Integer, default=0) + + comments = db.relationship('Comments', back_populates='video') + +class Comic(db.Model): + __tablename__ = 'comics' + + id = db.Column(db.Integer, primary_key=True) + comic_folder = db.Column(db.String(100), nullable=False) + comic_thumbnail_file = db.Column(db.String(100), nullable=False) + username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) + name = db.Column(db.String(100), nullable=False, unique=True) + publication_date = db.Column(db.DateTime, default=datetime.utcnow) + tags = db.Column(db.String(100), nullable=True) + cookie_votes = db.Column(db.Integer, default=0) + + comments = db.relationship('Comments', back_populates='comic', overlaps="comic_link") + pages = db.relationship('ComicPage', back_populates='comic', cascade="all, delete-orphan") + +class ComicPage(db.Model): + __tablename__ = 'comic_pages' + + id = db.Column(db.Integer, primary_key=True) + comic_id = db.Column(db.Integer, db.ForeignKey('comics.id', ondelete='CASCADE'), nullable=False) + page_number = db.Column(db.Integer, nullable=False) + file_path = db.Column(db.String(200), nullable=False) + + comic = db.relationship('Comic', back_populates='pages') + +class ComicVotes(db.Model): + __tablename__ = 'comic_votes' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(100), nullable=False) + comic_id = db.Column(db.Integer, db.ForeignKey('comics.id'), nullable=False) + vote = db.Column(db.Integer) + + comic = db.relationship('Comic', backref='votes', lazy=True) + +class Cookies(db.Model): + username = db.Column(db.String(20), primary_key=True, nullable=False) + cookies = db.Column(db.Integer, default=0) + +class Views(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True) + video_id = db.Column(db.Integer, db.ForeignKey('video.id'), nullable=True) + username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) + view_date = db.Column(db.DateTime, default=datetime.utcnow) + + __table_args__ = ( + db.UniqueConstraint('image_id', 'username', name='unique_image_view'), + db.UniqueConstraint('video_id', 'username', name='unique_video_view') + ) + +class Item(db.Model): + __tablename__ = 'items' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + item_path = db.Column(db.String, nullable=False) + price = db.Column(db.Integer, nullable=False, default=0) + visible = db.Column(db.Boolean, default=True) + + def __repr__(self): + return f'' + +class UserItem(db.Model): + __tablename__ = 'user_items' + + username = db.Column(db.String, db.ForeignKey('user.username'), primary_key=True) + item_id = db.Column(db.Integer, db.ForeignKey('items.id'), primary_key=True) + + item = db.relationship('Item', backref=db.backref('user_items', lazy=True)) + + def __repr__(self): + return f'' + +class Post(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False) + post_date = db.Column(db.DateTime, default=datetime.utcnow) + text = db.Column(db.Text, nullable=False) + media_file = db.Column(db.String(100), nullable=True) + + user = db.relationship('User', backref=db.backref('posts', lazy=True)) + +class Subscription(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + user = db.relationship('User', foreign_keys=[user_id], backref=db.backref('subscriptions', lazy=True)) + author = db.relationship('User', foreign_keys=[author_id], backref=db.backref('followers', lazy=True)) + + def __repr__(self): + return f'' + +class UploadForm(FlaskForm): + image_file = FileField('Choose File', validators=[DataRequired()]) + tags = StringField('Tags (comma-separated)', validators=[DataRequired()]) + recaptcha = RecaptchaField() + agree_with_rules = BooleanField('I agree with the publication rules', + validators=[DataRequired(message="You must agree with the publication rules.")]) + submit = SubmitField('Upload') + +class UploadVideoForm(FlaskForm): + video_file = FileField('Video File', validators=[DataRequired()]) + thumbnail = FileField('Thumbnail', validators=[DataRequired(), FileAllowed(['jpg', 'png', 'jpeg'])]) + name = StringField('Video Name', validators=[DataRequired()]) + tags = StringField('Tags', validators=[DataRequired()]) + description = StringField('Description', validators=[DataRequired()]) + recaptcha = RecaptchaField() + submit = SubmitField('Upload') + agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired()]) + +class UploadComicForm(FlaskForm): + title = StringField('Comic Name', validators=[DataRequired()]) + thumbnail = FileField('Thumbnail', validators=[DataRequired()]) + tags = StringField('Tags (comma-separated)') + recaptcha = RecaptchaField() + agree_with_rules = BooleanField('I agree with the publication rules', validators=[DataRequired(message="You must agree with the publication rules.")]) + submit = SubmitField('Upload') + +class EditTagsForm(FlaskForm): + tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter tags"}) + submit = SubmitField('Save') + +class EditVideoForm(FlaskForm): + video_name = StringField('Title', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter video title"}) + video_thumbnail = FileField('Thumbnail', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Only images!')]) + description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)], render_kw={"placeholder": "Enter video description"}) + tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Tags"}) + submit = SubmitField('Save') + +class LoginForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + recaptcha = RecaptchaField() + submit = SubmitField('Login') + + +class RegistrationForm(FlaskForm): + username = StringField('Username', + validators=[DataRequired(), Length(3,20), + Regexp('^[a-zA-Z0-9_]+$')]) + password = PasswordField('Password', + validators=[DataRequired(), Length(min=6)]) + confirm_password = PasswordField('Confirm Password', + validators=[DataRequired(), EqualTo('password')]) + recaptcha = RecaptchaField() + submit = SubmitField('Register') + +class EmptyForm(FlaskForm): + pass + +class UpdateCookiesForm(FlaskForm): + cookies = StringField('Количество печенек', validators=[DataRequired()]) submit = SubmitField('Применить') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f21c6fd..9843ad4 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/static/css/styles.scss b/static/css/styles.scss index 775ad6c..b80666b 100644 --- a/static/css/styles.scss +++ b/static/css/styles.scss @@ -1,977 +1,1195 @@ -$dark-violet: #0D0C1C; -$violet: #3C3882; -$light-violet: #8784C9; -@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&family=Playwrite+IT+Moderna:wght@100..400&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); - - -body { - background-color: #05040A; - font-family: Nunito, sans-serif; - } - -html, body { - overflow: auto; - scrollbar-width: none; - -ms-overflow-style: none; - } - - html::-webkit-scrollbar, - body::-webkit-scrollbar { - display: none; - } - - -.container { - position: absolute; - display: flex; - flex-wrap: wrap; - gap: 20px; - background-color: #05040A; - padding: 10px; - box-sizing: border-box; -} - -.new-content { - width: 1502px; - height: 340px; - top: 257px; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 20px; - position: absolute; - padding: 0; - box-sizing: border-box; -} - -.img-new-content { - width: 1502px; - height: 423px; - top: 257px; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 20px; - position: absolute; - padding: 0; - box-sizing: border-box; -} - -.popular-content { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 0px; - gap: 20px; - width: 1502px; - height: 631px; - top: 653px; - left: 50%; - transform: translateX(-50%); - position: absolute; - box-sizing: border-box; -} - -.img-popular-content { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 0px; - gap: 20px; - width: 1502px; - height: 817px; - left: 209px; - position: absolute; - top: 740px; -} - -.viewed-content { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 0px; - gap: 20px; - width: 1502px; - height: 631px; - top: 1340px; - left: 50%; - transform: translateX(-50%); - position: absolute; - box-sizing: border-box; -} - -.img-viewed-content { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 0px; - gap: 20px; - width: 1502px; - height: 631px; - left: 209px; - position: absolute; - top: 1617px; -} - -.popular-categories { - display: flex; - justify-content: space-between; - align-items: center; - width: 1500px; - height: 277px; - padding: 0; - box-sizing: border-box; - position: absolute; - top: 2095px; - left: 50%; - transform: translateX(-50%); - gap: 20px; -} - -.img-popular-categories { - display: flex; - justify-content: space-between; - align-items: center; - width: 1500px; - height: 277px; - padding: 0; - box-sizing: border-box; - position: absolute; - top: 2494px; - left: 210px; - gap: 20px; -} - -.new-content-text { - width: 123px; - height: 33px; - font-weight: 500; - font-size: 24px; - line-height: 100%; - letter-spacing: 0%; - position: absolute; - top: 10px; - color: $light-violet; -} - -.popular-content-text { - width: 123px; - height: 33px; - font-weight: 500; - font-size: 24px; - line-height: 100%; - letter-spacing: 0%; - position: absolute; - top: 10px; - color: $light-violet; -} - -.viewed-content-text { - width: 123px; - height: 33px; - font-weight: 500; - font-size: 24px; - line-height: 100%; - letter-spacing: 0%; - position: absolute; - top: 10px; - color: $light-violet; -} - -.popular-categories-text { - width: 321px; - height: 33px; - font-weight: 500; - font-size: 24px; - line-height: 100%; - letter-spacing: 0%; - position: absolute; - top: 10px; - color: $light-violet; -} - -.view-more-button { - width: 1500px; - height: 40px; - border-radius: 10px; - gap: 10px; - padding-top: 10px; - padding-right: 679px; - padding-bottom: 10px; - padding-left: 679px; - background-color: $light-violet; - border: none; - color: #fff; - font-weight: 500; - font-size: 16px; - cursor: pointer; - white-space: nowrap; - position: absolute; - bottom: -22px; - left: 50%; - transform: translateX(-50%); -} - -.view-more-button:hover { - background-color: $violet; - transition: background-color 0.3s ease, color 0.3s ease; -} - -.view-more-button:hover .new-context-button-text { - color: $light-violet; - transition: color 0.3s ease; - -} - -.img-view-more-button { - width: 1500px; - height: 40px; - border-radius: 10px; - gap: 10px; - padding-top: 10px; - padding-right: 679px; - padding-bottom: 10px; - padding-left: 679px; - background-color: $light-violet; - border: none; - color: #fff; - font-weight: 500; - font-size: 16px; - cursor: pointer; - white-space: nowrap; - position: absolute; - bottom: -22px; - left: 50%; - transform: translateX(-50%); -} - -.img-view-more-button:hover { - background-color: $violet; - transition: background-color 0.3s ease, color 0.3s ease; -} - -.img-view-more-button:hover .new-context-button-text { - color: $light-violet; - transition: color 0.3s ease; -} - -.new-context-button-text { - width: 134px; - height: 22px; - font-weight: 500; - font-size: 16px; - line-height: 100%; - letter-spacing: 0%; - color: $violet; -} - -/* navbar */ - -.navbar { - width: 100%; - max-width: 1500px; - height: 120px; - background: $dark-violet; - display: flex; - align-items: center; - padding: 0 20px; - gap: 20px; -} - -.navbar-wrapper { - width: 100%; - background: $dark-violet; - display: flex; - justify-content: center; -} - -.logo { - width: 307px; - height: 60px; -} - -.search-container { - display: flex; - align-items: center; - width: 452.5px; - height: 27px; - border: 1px solid $light-violet; - border-radius: 20px; - padding: 15px; - background: $dark-violet; - position: relative; - transition: border-color 0.3s ease; -} - -.search-icon-container { - position: relative; - width: 24px; - height: 24px; - margin-right: 10px; -} - -.search-icon, -.search-hover-icon { - width: 24px; - height: 24px; - position: absolute; - top: 0; - left: 0; - transition: opacity 0.3s ease; -} - -.search-hover-icon { - opacity: 0; -} - -.search-container:hover .search-hover-icon, -.search-container:focus-within .search-hover-icon { - opacity: 1; -} - -.search-container:hover .search-icon, -.search-container:focus-within .search-icon { - opacity: 0; -} - -.search-input { - border: none; - outline: none; - flex-grow: 1; - font-size: 16px; - height: 24px; - color: $light-violet; - background-color: $dark-violet; -} - -.search-container:hover { - border-color: $violet; -} - -.search-container:focus-within { - border-color: $violet; -} - -.icon-container { - display: flex; - align-items: center; - margin-left: 10px; -} - -.video-icon { - width: 24px; - height: 24px; - margin-left: 10px; -} - -.tray-icon { - width: 11px; - height: 7px; - margin-left: 5px; -} - -.translate-btn { - position: relative; - width: 23px; - height: 27px; - border: 1px solid $light-violet; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - background: $dark-violet; - cursor: pointer; - padding: 15px; - transition: border-color 0.3s ease, transform 0.3s ease, opacity 0.3s ease; -} - -.translate-icon, -.translate-hover-icon { - width: 24px; - height: 24px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - transition: opacity 0.3s ease; -} - -.translate-hover-icon { - opacity: 0; -} - -.translate-btn:hover .translate-hover-icon { - opacity: 1; -} - -.translate-btn:hover .translate-icon { - opacity: 0; -} - -.translate-btn:hover { - border-color: $violet; -} - -.overlay-icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 20px; - height: 20px; - opacity: 0.7; -} - -.menu { - display: flex; - gap: 22px; - width: 519.5px; - height: 22px; - justify-content: space-between; - padding-left: 60px; - padding-right: 60px; -} - -.menu a { - text-decoration: none; - color: $light-violet; - font-size: 16px; - transition: color 0.3s ease; -} - -.menu a:hover { - color: $violet; -} - -.auth-container { - display: flex; - align-items: center; - margin-left: auto; -} - -.discord-icon-container { - position: relative; - width: 42px; - height: 42px; -} - -.discord-icon, -.discord-hover-icon { - width: 42px; - height: 42px; - position: absolute; - top: 0; - left: 0; - transition: opacity 0.3s ease; -} - -.discord-hover-icon { - opacity: 0; -} - -.discord-icon-container:hover .discord-hover-icon { - opacity: 1; -} - -.discord-icon-container:hover .discord-icon { - opacity: 0; -} - -.login-btn { - width: 87px; - height: 42px; - border-radius: 20px; - background: $light-violet; - border: 1px solid $light-violet; - padding: 10px 15px; - font-size: 16px; - font-weight: 500; - line-height: 21.82px; - letter-spacing: 0%; - color: $dark-violet; - cursor: pointer; - margin-left: 10px; - transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; -} - -.login-btn:hover { - background-color: $violet; - color: $dark-violet; - border-color: $violet; -} - -.dropdown-menu { - position: absolute; - top: 100%; - right: 0; - background: $dark-violet; - border: 1px solid $violet; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - display: none; - flex-direction: column; - width: 150px; - z-index: 10; -} - -.dropdown-item { - padding: 10px 15px; - font-size: 14px; - border-radius: 4px; - color: $light-violet; - cursor: pointer; - transition: background 0.3s ease, color 0.3s ease; -} - -.dropdown-item:hover { - background: $violet; - color: white; -} - -/* tags-list */ - -.tags-container { - width: 1500px; - height: 35px; - position: absolute; - top: 192px; - left: 50%; - transform: translateX(-50%); - display: flex; - justify-content: flex-start; - align-items: center; - gap: 10px; - border: none; - background-color: #05040A; - overflow: hidden; - - > .list-button { - position: absolute; - right: 0; - z-index: 2; - } -} - -.tag { - height: 35px; - border-radius: 5px; - padding: 6px 10px; - display: inline-flex; - align-items: center; - justify-content: center; - background-color: $violet; - border: none; - color: $light-violet; - margin: 0; - visibility: visible; - transition: visibility 0.3s ease; -} - -.list-button { - width: 35px; - height: 35px; - border-radius: 10px; - padding: 5px; - display: flex; - align-items: center; - justify-content: center; - border: 2px solid $light-violet; - background-color: transparent; - color: $violet; - margin-left: 10px; - gap: 5px; - position: relative; - z-index: 2; -} - -.taglist-shadow { - width: 64px; - height: 35px; - position: absolute; - right: 0; - background: linear-gradient(to right, rgba(5, 4, 10, 0) 30%, rgba(5, 4, 10, 0.5) 60%, #05040A 100%); -} - -/* cards */ - -.img-cards-grid { - display: grid; - grid-template-columns: repeat(6, 1fr); - grid-gap: 30px; - width: 1500px; - height: 708px; - margin-top: 60px; -} - -.img-card { - width: 225px; - height: 344px; - display: flex; - flex-direction: column; - gap: 5px; -} - -.img-card-cover { - width: 100%; - height: 280px; - background: #1D1C2E; -} - -.cards-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - grid-gap: 30px; - width: 1500px; - height: 518px; - margin-top: 60px; -} - -.card { - width: 100%; - height: 244px; - display: flex; - flex-direction: column; - gap: 5px; - overflow: hidden; -} - -.card-cover { - width: 100%; - height: 180px; - background: #1D1C2E; - position: relative; - overflow: hidden; -} - -.card-cover img { - width: 100%; - height: 100%; - object-fit: cover; - position: absolute; - top: 0; - left: 0; -} - -.card-cover video.preview-video { - width: 100%; - height: 100%; - object-fit: cover; - position: absolute; - top: 0; - left: 0; - display: none; - z-index: 1; -} - -.card-info { - width: 100%; - height: 59px; - display: flex; - flex-direction: column; - gap: 5px; -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.card-stats { - display: flex; - gap: 10px; -} - -.stat { - display: flex; - align-items: center; - gap: 5px; -} - -.card-text { - font-size: 14px; - color: $light-violet; -} - -.img-small-cards-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - grid-gap: 30px; - width: 100%; - margin-top: 60px; -} - -.img-small-card-cover { - width: 189px; - height: 250px; - background: #1D1C2E; -} - - -.img-small-card { - width: 189px; - height: 314px; - display: flex; - flex-direction: column; - gap: 5px; -} - -.small-cards-grid { - display: grid; - grid-template-columns: repeat(5, 1fr); - grid-gap: 30px; - width: 100%; - margin-top: 60px; -} - -.small-card { - width: 100%; - height: 227px; - display: flex; - flex-direction: column; - gap: 5px; - overflow: hidden; -} - -.small-card-cover { - width: 100%; - height: 163px; - background: #1D1C2E; - position: relative; - overflow: hidden; -} - -.small-card-cover img { - width: 100%; - height: 100%; - object-fit: cover; - position: absolute; - top: 0; - left: 0; -} - -.small-card-cover video.preview-video { - width: 100%; - height: 100%; - object-fit: cover; - position: absolute; - top: 0; - left: 0; - display: none; - z-index: 1; -} - -.small-card-info { - width: 100%; - height: 59px; - display: flex; - flex-direction: column; - gap: 5px; -} - -.small-card-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.small-card-stats { - display: flex; - gap: 10px; -} - -.small-stat { - display: flex; - align-items: center; - gap: 5px; -} - -.small-card-text { - font-size: 14px; - color: $light-violet; -} - -.pc-card { - width: 233px; - height: 164px; - gap: 5px; -} - -.pc-card-cover { - width: 100%; - height: 140px; - background-color: #1D1C2E; -} - -.pc-card-stats { - width: 100%; - height: 19px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 2px; - box-sizing: border-box; -} - -.category-name-text { - width: auto; - max-width: 58px; - font-weight: 400; - font-size: 14px; - line-height: 100%; - letter-spacing: 0%; - color: $light-violet; - margin-left: -2px; -} - -.categories-number { - width: auto; - max-width: 34px; - font-weight: 400; - font-size: 14px; - line-height: 100%; - letter-spacing: 0%; - color: $violet; - text-align: right; - position: relative; - right: 5px; -} - -.ac-img-small-cards-grid { - display: grid; - grid-template-columns: repeat(6, 1fr); - grid-gap: 30px; - width: 100%; - margin-top: 60px; -} - -.ct-img-card { - width: 225px; - height: 344px; - display: flex; - flex-direction: column; - gap: 5px; -} - -.ct-img-card-cover { - width: 100%; - height: 280px; - background: #1D1C2E; -} - -.ct-small-card-info { - width: 100%; - height: 59px; - display: flex; - flex-direction: column; - gap: 5px; -} - -.ct-small-card-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.ct-small-card-stats { - display: flex; - gap: 10px; -} - -.ct-small-stat { - display: flex; - align-items: center; - gap: 5px; -} - -.ct-small-card-text { - font-size: 14px; - color: $light-violet; -} - -.ac-img-small-cards-grid { - display: grid; - grid-template-columns: repeat(6, 1fr); - grid-gap: 30px; - width: 100%; - margin-top: 60px; -} - -.most-new-button { - width: 276px; - height: 40px; - border-radius: 10px; - background-color: $violet; - color: white; - border: none; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - font-size: 14px; - text-align: center; - margin-left: -298px; -} - -.most-new-button-text { - font-weight: 500; - font-size: 16px; - line-height: 100%; - letter-spacing: 0%; - white-space: nowrap; -} - -/* pagination */ - -.pagination-container { - width: 626px; - height: 50px; - display: flex; - align-items: center; - gap: 10px; - position: absolute; - bottom: -180%; - left: 50%; - transform: translateX(-50%); - - .swipe-button { - width: 128px; - height: 50px; - border-radius: 5px; - padding: 7px 45px; - background-color: $violet; - border: none; - color: #fff; - font-weight: 500; - font-size: 16px; - cursor: pointer; - transform: rotate(0deg); - - } - - .page-buttons-container { - width: 350px; - height: 50px; - display: flex; - gap: 10px; - - .page-button { - width: 50px; - height: 50px; - border-radius: 5px; - padding: 1px 6px; - background-color: $violet; - border: none; - color: white; - font-weight: 500; - size: 16px; - cursor: pointer; - font-family: Inter; - } - } +$dark-violet: #0D0C1C; +$violet: #3C3882; +$light-violet: #8784C9; +@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&family=Playwrite+IT+Moderna:wght@100..400&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); + + +body { + background-color: #05040A; + font-family: Nunito, sans-serif; + } + +html, body { + overflow: auto; + scrollbar-width: none; + -ms-overflow-style: none; + } + + html::-webkit-scrollbar, + body::-webkit-scrollbar { + display: none; + } + + +.container { + position: absolute; + display: flex; + flex-wrap: wrap; + gap: 20px; + background-color: #05040A; + padding: 10px; + box-sizing: border-box; +} + +.new-content { + width: 1502px; + height: 340px; + top: 257px; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 20px; + position: absolute; + padding: 0; + box-sizing: border-box; +} + +.img-new-content { + width: 1502px; + height: 423px; + top: 257px; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 20px; + position: absolute; + padding: 0; + box-sizing: border-box; +} + +.popular-content { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 20px; + width: 1502px; + height: 631px; + top: 653px; + left: 50%; + transform: translateX(-50%); + position: absolute; + box-sizing: border-box; +} + +.img-popular-content { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 20px; + width: 1502px; + height: 817px; + left: 209px; + position: absolute; + top: 740px; +} + +.viewed-content { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 20px; + width: 1502px; + height: 631px; + top: 1340px; + left: 50%; + transform: translateX(-50%); + position: absolute; + box-sizing: border-box; +} + +.img-viewed-content { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 20px; + width: 1502px; + height: 631px; + left: 209px; + position: absolute; + top: 1617px; +} + +.popular-categories { + display: flex; + justify-content: space-between; + align-items: center; + width: 1500px; + height: 277px; + padding: 0; + box-sizing: border-box; + position: absolute; + top: 2095px; + left: 50%; + transform: translateX(-50%); + gap: 20px; +} + +.img-popular-categories { + display: flex; + justify-content: space-between; + align-items: center; + width: 1500px; + height: 277px; + padding: 0; + box-sizing: border-box; + position: absolute; + top: 2494px; + left: 210px; + gap: 20px; +} + +.new-content-text { + width: 123px; + height: 33px; + font-weight: 500; + font-size: 24px; + line-height: 100%; + letter-spacing: 0%; + position: absolute; + top: 10px; + color: $light-violet; +} + +.popular-content-text { + width: 123px; + height: 33px; + font-weight: 500; + font-size: 24px; + line-height: 100%; + letter-spacing: 0%; + position: absolute; + top: 10px; + color: $light-violet; +} + +.viewed-content-text { + width: 123px; + height: 33px; + font-weight: 500; + font-size: 24px; + line-height: 100%; + letter-spacing: 0%; + position: absolute; + top: 10px; + color: $light-violet; +} + +.popular-categories-text { + width: 321px; + height: 33px; + font-weight: 500; + font-size: 24px; + line-height: 100%; + letter-spacing: 0%; + position: absolute; + top: 10px; + color: $light-violet; +} + +.view-more-button { + width: 1500px; + height: 40px; + border-radius: 10px; + gap: 10px; + padding-top: 10px; + padding-right: 679px; + padding-bottom: 10px; + padding-left: 679px; + background-color: $light-violet; + border: none; + color: #fff; + font-weight: 500; + font-size: 16px; + cursor: pointer; + white-space: nowrap; + position: absolute; + bottom: -22px; + left: 50%; + transform: translateX(-50%); +} + +.view-more-button:hover { + background-color: $violet; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.view-more-button:hover .new-context-button-text { + color: $light-violet; + transition: color 0.3s ease; + +} + +.img-view-more-button { + width: 1500px; + height: 40px; + border-radius: 10px; + gap: 10px; + padding-top: 10px; + padding-right: 679px; + padding-bottom: 10px; + padding-left: 679px; + background-color: $light-violet; + border: none; + color: #fff; + font-weight: 500; + font-size: 16px; + cursor: pointer; + white-space: nowrap; + position: absolute; + bottom: -22px; + left: 50%; + transform: translateX(-50%); +} + +.img-view-more-button:hover { + background-color: $violet; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.img-view-more-button:hover .new-context-button-text { + color: $light-violet; + transition: color 0.3s ease; +} + +.new-context-button-text { + width: 134px; + height: 22px; + font-weight: 500; + font-size: 16px; + line-height: 100%; + letter-spacing: 0%; + color: $violet; +} + +/* navbar */ + +.navbar { + width: 100%; + max-width: 1500px; + height: 120px; + background: $dark-violet; + display: flex; + align-items: center; + padding: 0 20px; + gap: 20px; +} + +.navbar-wrapper { + width: 100%; + background: $dark-violet; + display: flex; + justify-content: center; +} + +.logo { + width: 307px; + height: 60px; +} + +.search-container { + display: flex; + align-items: center; + width: 452.5px; + height: 27px; + border: 1px solid $light-violet; + border-radius: 20px; + padding: 15px; + background: $dark-violet; + position: relative; + transition: border-color 0.3s ease; +} + +.search-icon-container { + position: relative; + width: 24px; + height: 24px; + margin-right: 10px; +} + +.search-icon, +.search-hover-icon { + width: 24px; + height: 24px; + position: absolute; + top: 0; + left: 0; + transition: opacity 0.3s ease; +} + +.search-hover-icon { + opacity: 0; +} + +.search-container:hover .search-hover-icon, +.search-container:focus-within .search-hover-icon { + opacity: 1; +} + +.search-container:hover .search-icon, +.search-container:focus-within .search-icon { + opacity: 0; +} + +.search-input { + border: none; + outline: none; + flex-grow: 1; + font-size: 16px; + height: 24px; + color: $light-violet; + background-color: $dark-violet; +} + +.search-container:hover { + border-color: $violet; +} + +.search-container:focus-within { + border-color: $violet; +} + +.icon-container { + display: flex; + align-items: center; + margin-left: 10px; +} + +.video-icon { + width: 24px; + height: 24px; + margin-left: 10px; +} + +.tray-icon { + width: 11px; + height: 7px; + margin-left: 5px; +} + +.translate-btn { + position: relative; + width: 23px; + height: 27px; + border: 1px solid $light-violet; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + background: $dark-violet; + cursor: pointer; + padding: 15px; + transition: border-color 0.3s ease, transform 0.3s ease, opacity 0.3s ease; +} + +.translate-icon, +.translate-hover-icon { + width: 24px; + height: 24px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transition: opacity 0.3s ease; +} + +.translate-hover-icon { + opacity: 0; +} + +.translate-btn:hover .translate-hover-icon { + opacity: 1; +} + +.translate-btn:hover .translate-icon { + opacity: 0; +} + +.translate-btn:hover { + border-color: $violet; +} + +.overlay-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 20px; + height: 20px; + opacity: 0.7; +} + +.menu { + display: flex; + gap: 22px; + width: 519.5px; + height: 22px; + justify-content: space-between; + padding-left: 60px; + padding-right: 60px; +} + +.menu a { + text-decoration: none; + color: $light-violet; + font-size: 16px; + transition: color 0.3s ease; +} + +.menu a:hover { + color: $violet; +} + +.auth-container { + display: flex; + align-items: center; + margin-left: auto; +} + +.discord-icon-container { + position: relative; + width: 42px; + height: 42px; +} + +.discord-icon, +.discord-hover-icon { + width: 42px; + height: 42px; + position: absolute; + top: 0; + left: 0; + transition: opacity 0.3s ease; +} + +.discord-hover-icon { + opacity: 0; +} + +.discord-icon-container:hover .discord-hover-icon { + opacity: 1; +} + +.discord-icon-container:hover .discord-icon { + opacity: 0; +} + +.login-btn { + width: 87px; + height: 42px; + border-radius: 20px; + background: $light-violet; + border: 1px solid $light-violet; + padding: 10px 15px; + font-size: 16px; + font-weight: 500; + line-height: 21.82px; + letter-spacing: 0%; + color: $dark-violet; + cursor: pointer; + margin-left: 10px; + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +.login-btn:hover { + background-color: $violet; + color: $dark-violet; + border-color: $violet; +} + +.dropdown-menu { + position: absolute; + top: 100%; + right: 0; + background: $dark-violet; + border: 1px solid $violet; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + display: none; + flex-direction: column; + width: 150px; + z-index: 10; +} + +.dropdown-item { + padding: 10px 15px; + font-size: 14px; + border-radius: 4px; + color: $light-violet; + cursor: pointer; + transition: background 0.3s ease, color 0.3s ease; +} + +.dropdown-item:hover { + background: $violet; + color: white; +} + +/* tags-list */ + +.tags-container { + width: 1500px; + height: 35px; + position: absolute; + top: 192px; + left: 50%; + transform: translateX(-50%); + display: flex; + justify-content: flex-start; + align-items: center; + gap: 10px; + border: none; + background-color: #05040A; + overflow: hidden; + + > .list-button { + position: absolute; + right: 0; + z-index: 2; + } +} + +.tag { + height: 35px; + border-radius: 5px; + padding: 6px 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background-color: $violet; + border: none; + color: $light-violet; + margin: 0; + visibility: visible; + transition: visibility 0.3s ease; +} + +.list-button { + width: 35px; + height: 35px; + border-radius: 10px; + padding: 5px; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid $light-violet; + background-color: transparent; + color: $violet; + margin-left: 10px; + gap: 5px; + position: relative; + z-index: 2; +} + +.taglist-shadow { + width: 64px; + height: 35px; + position: absolute; + right: 0; + background: linear-gradient(to right, rgba(5, 4, 10, 0) 30%, rgba(5, 4, 10, 0.5) 60%, #05040A 100%); +} + +/* cards */ + +.img-cards-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + grid-gap: 30px; + width: 1500px; + height: 708px; + margin-top: 60px; +} + +.img-card { + width: 225px; + height: 344px; + display: flex; + flex-direction: column; + gap: 5px; +} + +.img-card-cover { + width: 100%; + height: 280px; + background: #1D1C2E; +} + +.cards-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-gap: 30px; + width: 1500px; + height: 518px; + margin-top: 60px; +} + +.card { + width: 100%; + height: 244px; + display: flex; + flex-direction: column; + gap: 5px; + overflow: hidden; +} + +.card-cover { + width: 100%; + height: 180px; + background: #1D1C2E; + position: relative; + overflow: hidden; +} + +.card-cover img { + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; + top: 0; + left: 0; +} + +.card-cover video.preview-video { + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; + top: 0; + left: 0; + display: none; + z-index: 1; +} + +.card-info { + width: 100%; + height: 59px; + display: flex; + flex-direction: column; + gap: 5px; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.card-stats { + display: flex; + gap: 10px; +} + +.stat { + display: flex; + align-items: center; + gap: 5px; +} + +.card-text { + font-size: 14px; + color: $light-violet; +} + +.img-small-cards-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-gap: 30px; + width: 100%; + margin-top: 60px; +} + +.img-small-card-cover { + width: 189px; + height: 250px; + background: #1D1C2E; +} + + +.img-small-card { + width: 189px; + height: 314px; + display: flex; + flex-direction: column; + gap: 5px; +} + +.small-cards-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-gap: 30px; + width: 100%; + margin-top: 60px; +} + +.small-card { + width: 100%; + height: 227px; + display: flex; + flex-direction: column; + gap: 5px; + overflow: hidden; +} + +.small-card-cover { + width: 100%; + height: 163px; + background: #1D1C2E; + position: relative; + overflow: hidden; +} + +.small-card-cover img { + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; + top: 0; + left: 0; +} + +.small-card-cover video.preview-video { + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; + top: 0; + left: 0; + display: none; + z-index: 1; +} + +.small-card-info { + width: 100%; + height: 59px; + display: flex; + flex-direction: column; + gap: 5px; +} + +.small-card-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.small-card-stats { + display: flex; + gap: 10px; +} + +.small-stat { + display: flex; + align-items: center; + gap: 5px; +} + +.small-card-text { + font-size: 14px; + color: $light-violet; +} + +.pc-card { + width: 233px; + height: 164px; + gap: 5px; +} + +.pc-card-cover { + width: 100%; + height: 140px; + background-color: #1D1C2E; +} + +.pc-card-stats { + width: 100%; + height: 19px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 2px; + box-sizing: border-box; +} + +.category-name-text { + width: auto; + max-width: 58px; + font-weight: 400; + font-size: 14px; + line-height: 100%; + letter-spacing: 0%; + color: $light-violet; + margin-left: -2px; +} + +.categories-number { + width: auto; + max-width: 34px; + font-weight: 400; + font-size: 14px; + line-height: 100%; + letter-spacing: 0%; + color: $violet; + text-align: right; + position: relative; + right: 5px; +} + +.ac-img-small-cards-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + grid-gap: 30px; + width: 100%; + margin-top: 60px; +} + +.ct-img-card { + width: 225px; + height: 344px; + display: flex; + flex-direction: column; + gap: 5px; +} + +.ct-img-card-cover { + width: 100%; + height: 280px; + background: #1D1C2E; +} + +.ct-small-card-info { + width: 100%; + height: 59px; + display: flex; + flex-direction: column; + gap: 5px; +} + +.ct-small-card-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.ct-small-card-stats { + display: flex; + gap: 10px; +} + +.ct-small-stat { + display: flex; + align-items: center; + gap: 5px; +} + +.ct-small-card-text { + font-size: 14px; + color: $light-violet; +} + +.ac-img-small-cards-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + grid-gap: 30px; + width: 100%; + margin-top: 60px; +} + +.most-new-button { + width: 276px; + height: 40px; + border-radius: 10px; + background-color: $violet; + color: white; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + text-align: center; + margin-left: -298px; +} + +.most-new-button-text { + font-weight: 500; + font-size: 16px; + line-height: 100%; + letter-spacing: 0%; + white-space: nowrap; +} + +/* pagination */ + +.pagination-container { + width: 626px; + height: 50px; + display: flex; + align-items: center; + gap: 10px; + position: absolute; + bottom: -180%; + left: 50%; + transform: translateX(-50%); + + .swipe-button { + width: 128px; + height: 50px; + border-radius: 5px; + padding: 7px 45px; + background-color: $violet; + border: none; + color: #fff; + font-weight: 500; + font-size: 16px; + cursor: pointer; + transform: rotate(0deg); + + } + + .page-buttons-container { + width: 350px; + height: 50px; + display: flex; + gap: 10px; + + .page-button { + width: 50px; + height: 50px; + border-radius: 5px; + padding: 1px 6px; + background-color: $violet; + border: none; + color: white; + font-weight: 500; + size: 16px; + cursor: pointer; + font-family: Inter; + } + } +} + +#modal-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(13, 12, 28, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.modal { + width: 312px; + height: 400px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 10px; + border-width: thin; + border-color: #3C3882; + border-style: solid; + padding: 30px 40px; + position: fixed; + background-color: #0D0C1C; + display: none; + z-index: 1000; +} +.modal.active { + display: block; +} +.form-inner-container { + width: 312px; + height: 400px; + gap: 10px; + display: flex; + flex-direction: column; + align-items: center; + margin: 0 auto; +} +.reg-form-title { + width: 100%; + text-align: center; + margin: 0; + font-family: 'Nunito', sans-serif; + font-weight: 500; + font-size: 16px; + text-transform: uppercase; + color: #8784C9; + padding: 10px 0; +} +.modal-register-input-container { + width: 100%; + height: 39px; + border-radius: 10px; + border: 1px solid #8784C9; + opacity: 0.6; + position: relative; + background-color: transparent; +} +.modal-register-text-input { + width: calc(100% - 31px); + height: 16px; + position: absolute; + top: 11px; + left: 15.5px; + border: none; + outline: none; + background: transparent; + color: #8784C9; + font-family: 'Nunito', sans-serif; + font-size: 14px; +} +.modal-text-input::placeholder { + color: #8784C9; + opacity: 0.6; +} +.password-input { + -webkit-text-security: disc; +} +.modal-register-button { + width: 100%; + height: 38px; + border-radius: 10px; + background-color: #8784C9; + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.3s ease; +} +.modal-register-button:hover { +background-color: #3C3882; +} +.modal-register-button-text { +font-family: 'Nunito', sans-serif; +font-weight: 500; +font-size: 16px; +color: #3C3882; +text-align: center; +transition: background-color 0.3s ease; +} +.modal-register-button:hover .modal-register-button-text { +color: #8784C9; +} +.login-link-container { +width: 312px; +height: 16px; +margin-bottom: 10px; +} + +.login-prompt { +display: block; +width: 100%; +height: 16px; +font-family: 'Nunito', sans-serif; +font-weight: 400; +font-size: 12px; +line-height: 30px; +letter-spacing: 0; +color: #8784C9; +} + +.login-link { +text-decoration: underline solid 0px; +text-decoration-skip-ink: auto; +color: #8784C9; +} + +.login-form-title { + width: 100%; + text-align: center; + margin: 0; + font-family: 'Nunito', sans-serif; + font-weight: 500; + font-size: 16px; + text-transform: uppercase; + color: #8784C9; + padding: 10px 0; +} +.modal-login-input-container { + width: 100%; + height: 39px; + border-radius: 10px; + border: 1px solid #8784C9; + opacity: 0.6; + position: relative; + background-color: transparent; +} +.modal-login-text-input { + width: calc(100% - 31px); + height: 16px; + position: absolute; + top: 11px; + left: 15.5px; + border: none; + outline: none; + background: transparent; + color: #8784C9; + font-family: 'Nunito', sans-serif; + font-size: 14px; +} +.modal-text-input::placeholder { + color: #8784C9; + opacity: 0.6; +} +.password-input { + -webkit-text-security: disc; +} +.modal-login-button { + width: 100%; + height: 38px; + border-radius: 10px; + background-color: #8784C9; + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.3s ease; +} +.modal-login-button:hover { + background-color: #9d9ad4; +} +.modal-login-button-text { +font-family: 'Nunito', sans-serif; +font-weight: 500; +font-size: 16px; +color: #3C3882; +text-align: center; +} + +.login-link-container { +width: 312px; +height: 16px; +margin-bottom: 10px; +} + +.login-prompt { +display: block; +width: 100%; +height: 16px; +font-family: 'Nunito', sans-serif; +font-weight: 400; +font-size: 12px; +line-height: 30px; +letter-spacing: 0; +color: #8784C9; +} + +.login-link { +text-decoration: underline solid 0px; +text-decoration-skip-ink: auto; +color: #8784C9; } \ No newline at end of file diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..b6b38f2 --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/list-icon.svg b/static/icons/list-icon.svg index 4958270..4b29303 100644 --- a/static/icons/list-icon.svg +++ b/static/icons/list-icon.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/static/js/comic_upload.js b/static/js/comic_upload.js index 80b9ae5..215f2cb 100644 --- a/static/js/comic_upload.js +++ b/static/js/comic_upload.js @@ -1,15 +1,15 @@ -let pageCount = 1; -const maxPages = 64; - -function addPage() { - if (pageCount >= maxPages) { - alert(`You can't add more than ${maxPages} pages!`); - return; - } - - pageCount++; - const newPage = document.createElement("div"); - newPage.classList.add("form-group"); - newPage.innerHTML = ``; - document.getElementById("pages").appendChild(newPage); +let pageCount = 1; +const maxPages = 64; + +function addPage() { + if (pageCount >= maxPages) { + alert(`You can't add more than ${maxPages} pages!`); + return; + } + + pageCount++; + const newPage = document.createElement("div"); + newPage.classList.add("form-group"); + newPage.innerHTML = ``; + document.getElementById("pages").appendChild(newPage); } \ No newline at end of file diff --git a/static/js/hoverPreview.js b/static/js/hoverPreview.js index 3422376..f92e7d7 100644 --- a/static/js/hoverPreview.js +++ b/static/js/hoverPreview.js @@ -1,64 +1,63 @@ -document.addEventListener('DOMContentLoaded', () => { - const cardCovers = document.querySelectorAll('.small-card-cover, .card-cover'); - - cardCovers.forEach(cardCover => { - const thumbnail = cardCover.querySelector('.thumbnail'); - const video = cardCover.querySelector('.preview-video'); - const videoSource = video.querySelector('source'); - let hoverTimeout; - - cardCover.addEventListener('mouseenter', () => { - hoverTimeout = setTimeout(() => { - - if (!videoSource.src) { - videoSource.src = video.dataset.src; - video.load(); - } - - thumbnail.style.display = 'none'; - video.style.display = 'block'; - - video.addEventListener('loadedmetadata', () => { - - if (video.duration > 15) { - - video.currentTime = 10; - const loopSegment = () => { - if (video.currentTime >= 15) { - video.currentTime = 10; - video.play(); - } - }; - video.addEventListener('timeupdate', loopSegment); - cardCover.loopSegment = loopSegment; - } else { - - video.currentTime = 0; - const loopEntireVideo = () => { - if (video.currentTime >= video.duration) { - video.currentTime = 0; - video.play(); - } - }; - video.addEventListener('timeupdate', loopEntireVideo); - cardCover.loopSegment = loopEntireVideo; - } - - video.play(); - }); - }, 2000); - }); - - cardCover.addEventListener('mouseleave', () => { - clearTimeout(hoverTimeout); - video.pause(); - video.style.display = 'none'; - thumbnail.style.display = 'block'; - - if (cardCover.loopSegment) { - video.removeEventListener('timeupdate', cardCover.loopSegment); - delete cardCover.loopSegment; - } - }); - }); -}); \ No newline at end of file +document.addEventListener('DOMContentLoaded', () => { + const cardCovers = document.querySelectorAll('.small-card-cover, .card-cover'); + + cardCovers.forEach(cardCover => { + const thumbnail = cardCover.querySelector('.thumbnail'); + const video = cardCover.querySelector('.preview-video'); + const videoSource = video.querySelector('source'); + + const showPreview = () => { + thumbnail.style.display = 'none'; + video.style.display = 'block'; + + if (video.duration > 15) { + video.currentTime = 10; + const loopSegment = () => { + if (video.currentTime >= 15) { + video.currentTime = 10; + video.play(); + } + }; + video.addEventListener('timeupdate', loopSegment); + cardCover.loopSegment = loopSegment; + } else { + video.currentTime = 0; + const loopEntireVideo = () => { + if (video.currentTime >= video.duration) { + video.currentTime = 0; + video.play(); + } + }; + video.addEventListener('timeupdate', loopEntireVideo); + cardCover.loopSegment = loopEntireVideo; + } + + video.play(); + }; + + cardCover.addEventListener('mouseenter', () => { + video.pause(); + videoSource.src = video.dataset.src; + video.load(); + video.addEventListener('canplay', showPreview, { once: true }); + }); + + cardCover.addEventListener('mouseleave', (e) => { + const toElement = e.relatedTarget; + + if (!cardCover.contains(toElement)) { + video.pause(); + video.currentTime = 0; + video.style.display = 'none'; + thumbnail.style.display = 'block'; + + if (cardCover.loopSegment) { + video.removeEventListener('timeupdate', cardCover.loopSegment); + delete cardCover.loopSegment; + } + + videoSource.src = ''; + } + }); + }); +}); diff --git a/static/js/navbar.js b/static/js/navbar.js index ed4e2c3..0728067 100644 --- a/static/js/navbar.js +++ b/static/js/navbar.js @@ -1,17 +1,92 @@ -document.addEventListener('DOMContentLoaded', function () { - const iconContainer = document.querySelector('.icon-container'); - const dropdownMenu = document.querySelector('.dropdown-menu'); - - iconContainer.addEventListener('click', function (event) { - dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block'; - event.stopPropagation(); - }); - - document.addEventListener('click', function () { - dropdownMenu.style.display = 'none'; - }); - - dropdownMenu.addEventListener('click', function (event) { - event.stopPropagation(); - }); -}); \ No newline at end of file +document.addEventListener('DOMContentLoaded', function () { + const iconContainer = document.querySelector('.icon-container'); + const dropdownMenu = document.querySelector('.dropdown-menu'); + + iconContainer.addEventListener('click', function (event) { + dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block'; + event.stopPropagation(); + }); + + document.addEventListener('click', function () { + dropdownMenu.style.display = 'none'; + }); + + dropdownMenu.addEventListener('click', function (event) { + event.stopPropagation(); + }); + + // Делегируем клики для открытия логина/регистрации внутри любых модалок + document.addEventListener('click', function (e) { + const target = e.target.closest('[data-action]'); + if (!target) return; + + e.preventDefault(); + + const action = target.dataset.action; + if (action === 'open-register') { + openRegisterModal(); + } else if (action === 'open-login') { + openLoginModal(); + } + }); +}); + +function closeAllModals() { + const modalContainer = document.getElementById('modal-container'); + if (modalContainer) modalContainer.remove(); +} + +function openRegisterModal() { + closeAllModals(); + + const modalContainer = document.createElement('div'); + modalContainer.id = 'modal-container'; + document.body.appendChild(modalContainer); + + fetch('/register-modal') + .then(response => response.text()) + .then(html => { + modalContainer.innerHTML = html; + reloadRecaptcha(modalContainer); + }); +} + +function openLoginModal() { + closeAllModals(); + + const modalContainer = document.createElement('div'); + modalContainer.id = 'modal-container'; + document.body.appendChild(modalContainer); + + fetch('/login-modal') + .then(response => response.text()) + .then(html => { + modalContainer.innerHTML = html; + reloadRecaptcha(modalContainer); + }); +} + +function reloadRecaptcha(modalContainer) { + const recaptchaScript = modalContainer.querySelector('script[src*="recaptcha"]'); + if (recaptchaScript) recaptchaScript.remove(); + + const script = document.createElement('script'); + script.src = 'https://www.google.com/recaptcha/api.js?render=explicit&onload=recaptchaCallback'; + document.head.appendChild(script); +} + +window.recaptchaCallback = function () { + const recaptchaElement = document.querySelector('.g-recaptcha'); + if (recaptchaElement && typeof grecaptcha !== 'undefined') { + grecaptcha.render(recaptchaElement, { + theme: "dark", + size: "default" + }); + } +}; +document.addEventListener('click', function (e) { + const modal = document.querySelector('#modal-container .modal'); + if (modal && !modal.contains(e.target)) { + closeAllModals(); + } +}); diff --git a/static/js/taglist.js b/static/js/taglist.js index af5ea11..d5a1bdc 100644 --- a/static/js/taglist.js +++ b/static/js/taglist.js @@ -1,22 +1,22 @@ -window.addEventListener('DOMContentLoaded', function() { - const listButton = document.querySelector('.list-button'); - const tags = document.querySelectorAll('.tag'); - - const buttonRect = listButton.getBoundingClientRect(); - - function checkTagVisibility() { - tags.forEach(tag => { - const tagRect = tag.getBoundingClientRect(); - - if (tagRect.right > buttonRect.left) { - tag.style.visibility = 'hidden'; - } else { - tag.style.visibility = 'visible'; - } - }); - } - - document.querySelector('.tags-container').addEventListener('scroll', checkTagVisibility); - - checkTagVisibility(); +window.addEventListener('DOMContentLoaded', function() { + const listButton = document.querySelector('.list-button'); + const tags = document.querySelectorAll('.tag'); + + const buttonRect = listButton.getBoundingClientRect(); + + function checkTagVisibility() { + tags.forEach(tag => { + const tagRect = tag.getBoundingClientRect(); + + if (tagRect.right > buttonRect.left) { + tag.style.visibility = 'hidden'; + } else { + tag.style.visibility = 'visible'; + } + }); + } + + document.querySelector('.tags-container').addEventListener('scroll', checkTagVisibility); + + checkTagVisibility(); }); \ No newline at end of file diff --git a/templates/arts-catalogue.html b/templates/arts-catalogue.html index 1561b40..95971d5 100644 --- a/templates/arts-catalogue.html +++ b/templates/arts-catalogue.html @@ -1,40 +1,40 @@ - - - - - - - {% include 'navbar.html' %} - {% include 'subnav.html' %} -
- ПОПУЛЯРНОЕ -
- {% for i in range(36) %} -
-
-
-
- Totoka -
-
- Лайк - 134 -
-
- Просмотры - 32113 -
-
-
-

Big Brother Keep Hugging Me

-
-
- {% endfor %} -
- -
- {% include 'pagination.html' %} - + + + + + + + {% include 'navbar.html' %} + {% include 'subnav.html' %} +
+ ПОПУЛЯРНОЕ +
+ {% for i in range(36) %} +
+
+
+
+ Totoka +
+
+ Лайк + 134 +
+
+ Просмотры + 32113 +
+
+
+

Big Brother Keep Hugging Me

+
+
+ {% endfor %} +
+ +
+ {% include 'pagination.html' %} + \ No newline at end of file diff --git a/templates/arts.html b/templates/arts.html index f9c17f7..bb04dc6 100644 --- a/templates/arts.html +++ b/templates/arts.html @@ -1,113 +1,113 @@ - - - - - - - {% include 'navbar.html' %} - {% include 'subnav.html' %} - {% include 'tags_list.html' %} -
- НОВИНКИ -
- {% for i in range(7) %} -
-
-
-
- Totoka -
-
- Лайк - 134 -
-
- Просмотры - 32113 -
-
-
-

Big Brother Keep Hugging Me

-
-
- {% endfor %} -
- -
- -
- ПРОСМАТРИВАЕМОЕ -
- {% for i in range(12) %} -
-
-
-
- Totoka -
-
- Лайк - 134 -
-
- Просмотры - 32113 -
-
-
-

Big Brother Keep Hugging Me

-
-
- {% endfor %} -
- -
- - + + + + + + + {% include 'navbar.html' %} + {% include 'subnav.html' %} + {% include 'tags_list.html' %} +
+ НОВИНКИ +
+ {% for i in range(7) %} +
+
+
+
+ Totoka +
+
+ Лайк + 134 +
+
+ Просмотры + 32113 +
+
+
+

Big Brother Keep Hugging Me

+
+
+ {% endfor %} +
+ +
+ +
+ ПРОСМАТРИВАЕМОЕ +
+ {% for i in range(12) %} +
+
+
+
+ Totoka +
+
+ Лайк + 134 +
+
+ Просмотры + 32113 +
+
+
+

Big Brother Keep Hugging Me

+
+
+ {% endfor %} +
+ +
+ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 8e3afd0..a8b2157 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,14 +1,14 @@ -{% extends "content.html" %} -{% include 'subnav.html' %} -{% block title %}🫐videos - artberry🫐{% endblock %} - -{% block new_content %} -
- НОВИНКИ (Updated for Videos) - -
-{% endblock %} - -{% block content %} - {% include 'tags_list.html' %} +{% extends "content.html" %} +{% include 'subnav.html' %} +{% block title %}🫐videos - artberry🫐{% endblock %} + +{% block new_content %} +
+ НОВИНКИ (Updated for Videos) + +
+{% endblock %} + +{% block content %} + {% include 'tags_list.html' %} {% endblock %} \ No newline at end of file diff --git a/templates/login-modal.html b/templates/login-modal.html new file mode 100644 index 0000000..8cd65ea --- /dev/null +++ b/templates/login-modal.html @@ -0,0 +1,47 @@ + + + diff --git a/templates/login.html b/templates/login.html index 21f8a75..219e093 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,35 +1,60 @@ -{% extends "auth.html" %} -{% block title %}🫐login - artberry🫐{% endblock %} -{% set action = 'auth.login' %} + + + + + + + + + + +