From 8ccb3c634ff2847be26f83f36b678f30ef134d01 Mon Sep 17 00:00:00 2001 From: aneuhmanh Date: Sun, 16 Feb 2025 16:38:57 +0200 Subject: [PATCH] added files --- .gitignore | 8 + app.py | 1608 +++++++++++++++++++++++++++++ static/artberry.ico | Bin 0 -> 102894 bytes static/avatars/default-avatar.png | Bin 0 -> 3712 bytes static/banners/default-banner.png | Bin 0 -> 10644 bytes static/card/like-icon.svg | 3 + static/card/views-icon.svg | 3 + static/css/styles.css | 955 +++++++++++++++++ static/navbar/discord-hover.svg | 3 + static/navbar/discord-icon.svg | 3 + static/navbar/logo.svg | 39 + static/navbar/search-hover.svg | 3 + static/navbar/search-icon.svg | 3 + static/navbar/translate-hover.svg | 3 + static/navbar/translate-icon.svg | 3 + static/navbar/tray-icon.svg | 3 + static/navbar/video-icon.svg | 3 + templates/auth.html | 29 + templates/card.html | 79 ++ templates/comic_edit.html | 52 + templates/comic_upload.html | 59 ++ templates/comics.html | 21 + templates/content.html | 240 +++++ templates/error.html | 16 + templates/image_edit.html | 29 + templates/index.html | 16 + templates/login.html | 35 + templates/navbar.html | 308 ++++++ templates/panel.html | 164 +++ templates/post.html | 89 ++ templates/privacy_policy.html | 33 + templates/profile.html | 132 +++ templates/profile_edit.html | 79 ++ templates/publication_rules.html | 34 + templates/register.html | 31 + templates/shop.html | 39 + templates/terms_of_use.html | 40 + templates/upload.html | 63 ++ templates/upload_post.html | 25 + templates/upload_video.html | 59 ++ templates/user_pubs.html | 104 ++ templates/video_edit.html | 45 + templates/videos.html | 27 + templates/view.html | 150 +++ 44 files changed, 4638 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 static/artberry.ico create mode 100644 static/avatars/default-avatar.png create mode 100644 static/banners/default-banner.png create mode 100644 static/card/like-icon.svg create mode 100644 static/card/views-icon.svg create mode 100644 static/css/styles.css create mode 100644 static/navbar/discord-hover.svg create mode 100644 static/navbar/discord-icon.svg create mode 100644 static/navbar/logo.svg create mode 100644 static/navbar/search-hover.svg create mode 100644 static/navbar/search-icon.svg create mode 100644 static/navbar/translate-hover.svg create mode 100644 static/navbar/translate-icon.svg create mode 100644 static/navbar/tray-icon.svg create mode 100644 static/navbar/video-icon.svg create mode 100644 templates/auth.html create mode 100644 templates/card.html create mode 100644 templates/comic_edit.html create mode 100644 templates/comic_upload.html create mode 100644 templates/comics.html create mode 100644 templates/content.html create mode 100644 templates/error.html create mode 100644 templates/image_edit.html create mode 100644 templates/index.html create mode 100644 templates/login.html create mode 100644 templates/navbar.html create mode 100644 templates/panel.html create mode 100644 templates/post.html create mode 100644 templates/privacy_policy.html create mode 100644 templates/profile.html create mode 100644 templates/profile_edit.html create mode 100644 templates/publication_rules.html create mode 100644 templates/register.html create mode 100644 templates/shop.html create mode 100644 templates/terms_of_use.html create mode 100644 templates/upload.html create mode 100644 templates/upload_post.html create mode 100644 templates/upload_video.html create mode 100644 templates/user_pubs.html create mode 100644 templates/video_edit.html create mode 100644 templates/videos.html create mode 100644 templates/view.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73adaa5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/instance/ +/static/arts/ +/static/comics/ +/static/comicthumbs/ +/static/items/ +/static/posts/ +/static/thumbnails/ +/static/videos/ diff --git a/app.py b/app.py new file mode 100644 index 0000000..79fbfc4 --- /dev/null +++ b/app.py @@ -0,0 +1,1608 @@ +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, 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 + +app = Flask(__name__) +csrf = CSRFProtect(app) + +app.config.update( + SECRET_KEY='PASTE SOMETHING HERE', + WTF_CSRF_ENABLED=True, + RECAPTCHA_PUBLIC_KEY="PASTE RECAPTCHA KEY HERE", + RECAPTCHA_PRIVATE_KEY="PASTE RECAPTCHA KEY HERE", + SQLALCHEMY_DATABASE_URI='sqlite:///artberry.db', + 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', 'gif'}, + ALLOWED_VIDEO_EXTENSIONS={'mp4', 'avi', 'mov'}, + MAX_IMAGE_SIZE=15 * 1024 * 1024, + MAX_VIDEO_SIZE=1 * 1024 * 1024 * 1024 +) + +def allowed_file(filename, allowed_extensions): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions + +def check_file_size(file, max_size): + file.seek(0, os.SEEK_END) + file_size = file.tell() + file.seek(0) + return file_size <= max_size + +db = SQLAlchemy(app) +bcrypt = Bcrypt(app) +login_manager = LoginManager(app) +login_manager.login_view = 'login' + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + +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', backref='comic_link', lazy=True) + post = db.relationship('Post', backref='comments') + + def __repr__(self): + return f'' + +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) + show_publications = db.Column(db.Boolean, default=True) + hide_elements = db.Column(db.Boolean, default=False) + site_mask = db.Column(db.Boolean, default=False) + 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) + 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', backref='comic_link', lazy=True) + +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'' + +from sqlalchemy import func, or_ + +def get_content_query(model, subscriptions, search_query): + query = model.query + + if search_query: + + tags = [tag.strip().lower() for tag in search_query.replace(',', ' ').split()] + + filter_condition = [ + model.tags.like(f'%{tag}%') for tag in tags + ] + + query = query.filter( + or_(*filter_condition) + ) + + if subscriptions: + query = query.filter(or_( + model.username.in_(subscriptions), + model.username.notin_(subscriptions) + )) + + query = query.order_by( + func.coalesce(model.cookie_votes, 0).desc(), + model.publication_date.desc() + ) + + return query + +@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('/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']) + +@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() + + 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() + + 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() + + comic_pages_dir = os.path.join(app.config['UPLOAD_FOLDER']['comics'], content.comic_folder) + + if not os.path.exists(comic_pages_dir): + return render_template('error.html', message="Comic pages not found") + + comic_pages = os.listdir(comic_pages_dir) + comic_pages = sorted(comic_pages) + + 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 + ) + +class EditTagsForm(FlaskForm): + tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Enter tags"}) + submit = SubmitField('Save') + +@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 + ) + +class EditVideoForm(FlaskForm): + video_name = StringField('Title', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Введите название видео"}) + video_thumbnail = FileField('Thumbnail', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Только изображения!')]) + description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)], render_kw={"placeholder": "Введите описание видео"}) + tags = StringField('Tags', validators=[DataRequired(), Length(max=100)], render_kw={"placeholder": "Tags"}) + submit = SubmitField('Save') + +@app.route('/video_edit/', methods=['GET', 'POST']) +@login_required +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 = generate_unique_filename(thumbnail_file.filename, app.config['UPLOAD_FOLDER']['thumbnails']) + thumbnail_path = os.path.join(app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename) + + thumbnail_file.save(thumbnail_path) + 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('/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) + +def get_autocomplete_suggestions(query): + + last_tag = query.split(',')[-1].strip() + + all_tags = Image.query.with_entities(Image.tags).all() + + unique_tags = set(tag.strip() for tags in all_tags if tags.tags for tag in tags.tags.split(',')) + + filtered_tags = [tag for tag in unique_tags if last_tag.lower() in tag.lower()] + + return filtered_tags[:5] + +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') + +def convert_to_webp(image_file): + + img = PILImage.open(image_file) + output = io.BytesIO() + + img.convert("RGB").save(output, format="WEBP", quality=90, optimize=True) + output.seek(0) + return output + +@app.route('/upload', methods=['GET', 'POST']) +@login_required +def upload(): + form = UploadForm() + + if form.validate_on_submit(): + image_file = form.image_file.data + tags = form.tags.data + + if image_file: + + if not allowed_file(image_file.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): + return redirect(url_for('upload')) + + if not check_file_size(image_file, app.config['MAX_IMAGE_SIZE']): + return redirect(url_for('upload')) + + unique_filename = f"{uuid.uuid4().hex}.webp" + filepath = os.path.join(app.config['UPLOAD_FOLDER']['images'], unique_filename) + + if os.path.exists(filepath): + return redirect(url_for('upload')) + + webp_image = convert_to_webp(image_file) + + try: + with open(filepath, 'wb') as f: + f.write(webp_image.read()) + except Exception as e: + return redirect(url_for('upload')) + + img = Image(image_file=unique_filename, username=current_user.username, tags=tags, cookie_votes=0) + db.session.add(img) + + user_cookie = Cookies.query.filter_by(username=current_user.username).first() + if user_cookie: + user_cookie.cookies += 1 + else: + user_cookie = Cookies(username=current_user.username, cookies=1) + db.session.add(user_cookie) + + try: + db.session.commit() + except Exception as e: + db.session.rollback() + return redirect(url_for('upload')) + + return redirect(url_for('index')) + else: + return redirect(url_for('upload')) + + return render_template('upload.html', form=form) + +def allowed_file(filename, allowed_extensions): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions + +def check_file_size(file, max_size): + file.seek(0, os.SEEK_END) + file_size = file.tell() + file.seek(0) + return file_size <= max_size + +def generate_unique_filename(filename, upload_folder): + + base, ext = os.path.splitext(secure_filename(filename)) + unique_filename = f"{base}_{uuid.uuid4().hex}{ext}" + file_path = os.path.join(upload_folder, unique_filename) + + while os.path.exists(file_path): + unique_filename = f"{base}_{uuid.uuid4().hex}{ext}" + file_path = os.path.join(upload_folder, unique_filename) + + return unique_filename + +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()]) + +@app.route('/upload_video', methods=['GET', 'POST']) +@login_required +async def upload_video(): + form = UploadVideoForm() + if form.validate_on_submit(): + video_file = form.video_file.data + video_thumbnail = form.thumbnail.data + video_name = form.name.data + tags = form.tags.data + description = form.description.data + + if video_file and video_thumbnail: + + if not allowed_file(video_file.filename, app.config['ALLOWED_VIDEO_EXTENSIONS']): + return redirect(url_for('upload_video')) + if not check_file_size(video_file, app.config['MAX_VIDEO_SIZE']): + return redirect(url_for('upload_video')) + if not allowed_file(video_thumbnail.filename, app.config['ALLOWED_IMAGE_EXTENSIONS']): + return redirect(url_for('upload_video')) + + video_filename = generate_unique_filename(video_file.filename, app.config['UPLOAD_FOLDER']['videos']) + thumbnail_filename = generate_unique_filename(video_thumbnail.filename, app.config['UPLOAD_FOLDER']['thumbnails']) + + video_path = os.path.join(app.config['UPLOAD_FOLDER']['videos'], video_filename) + thumbnail_path = os.path.join(app.config['UPLOAD_FOLDER']['thumbnails'], thumbnail_filename) + + async with aiofiles.open(video_path, 'wb') as f: + await f.write(video_file.read()) + + async with aiofiles.open(thumbnail_path, 'wb') as f: + await f.write(video_thumbnail.read()) + + video = Video( + video_file=video_filename, + video_name=video_name, + video_thumbnail_file=thumbnail_filename, + username=current_user.username, + tags=tags, + description=description + ) + db.session.add(video) + db.session.commit() + + return redirect(url_for('videos')) + + return render_template('upload_video.html', form=form) + +@app.route('/comic_upload', methods=['GET', 'POST']) +@login_required +def comic_upload(): + if request.method == 'POST': + + ct = request.files['thumbnail'] + n = request.form['title'] + tags = request.form.get('tags', '') + + if Comic.query.filter_by(name=n).first(): + return render_template('comic_upload.html') + + if ct: + tf = secure_filename(ct.filename) + tp = os.path.join(app.config['UPLOAD_FOLDER']['comicthumbs'], tf) + ct.save(tp) + + cf = os.path.join(app.config['UPLOAD_FOLDER']['comics'], n) + os.makedirs(cf, exist_ok=True) + + new_comic = Comic( + comic_folder=n, + comic_thumbnail_file=tf, + username=current_user.username, + name=n, + tags=tags + ) + db.session.add(new_comic) + db.session.commit() + + for p in request.files.getlist('pages[]'): + if p: + p.save(os.path.join(cf, secure_filename(p.filename))) + + return redirect(url_for('comics')) + + return render_template('comic_upload.html') + +@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 + ) + +from wtforms import StringField, PasswordField, SubmitField +from wtforms.validators import DataRequired, EqualTo, Regexp +from flask_wtf import FlaskForm, RecaptchaField + +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') + + def validate_username(self, username): + user = User.query.filter_by(username=username.data).first() + if user: + return + + if not re.match(r'^[a-zA-Z0-9_]+$', username.data): + return + + def validate_username(self, username): + username.data = username.data.lower() + user = User.query.filter_by(username=username.data).first() + if user: + return + + if not re.match(r'^[a-z0-9]+$', username.data): + return + + def validate_ip(self): + ip_address = get_client_ip() + user_with_ip = User.query.filter_by(ip_address=ip_address).first() + if user_with_ip: + return + +class LoginForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + recaptcha = RecaptchaField() + submit = SubmitField('Login') + +def get_client_ip(): + if 'X-Forwarded-For' in request.headers: + forwarded_for = request.headers['X-Forwarded-For'] + ip_address = forwarded_for.split(',')[0] + else: + ip_address = request.remote_addr + + return ip_address + +@app.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('login')) + except IntegrityError: + db.session.rollback() + + return render_template('register.html', form=form) + +@app.route('/login', methods=['GET', 'POST']) +def login(): + form = LoginForm() + + if form.validate_on_submit(): + user = User.query.filter_by(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) + +@app.route('/logout') +def logout(): + logout_user() + return redirect(url_for('index')) + + +@app.route('/delete//', methods=['POST']) +@login_required +def delete(content_type, content_id): + if content_type == 'art': + content = Art.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 = Comments.query.get_or_404(comment_id) + + 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) + + 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=pagination.items, + pagination=pagination, + user_cookies=user_cookies, + search_query=search_query, + content_type='video' + ) + +@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)) + +class EmptyForm(FlaskForm): + pass + +@app.route('/comic_edit/', methods=['GET', 'POST']) +@login_required +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) + + comic_pages = sorted(os.listdir(cfp)) + form = EmptyForm() + + if request.method == 'POST' and form.validate_on_submit(): + action = request.form.get('action') + if action == 'delete' and (page_to_delete := request.form.get('page')): + os.remove(os.path.join(cfp, page_to_delete)) + elif action in ['update', 'add'] and 'new_page' in request.files: + new_page = request.files['new_page'] + new_page.save(os.path.join(cfp, secure_filename(new_page.filename))) + + return redirect(url_for('comic_edit', comic_id=comic.id)) + + return render_template('comic_edit.html', comic=comic, comic_pages=comic_pages, form=form) + +from flask import flash, redirect, url_for, render_template +from werkzeug.utils import secure_filename +import os + +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('/upload_post', methods=['GET', 'POST']) +@login_required +def upload_post(): + if request.method == 'POST': + post_text = request.form.get('post_text') + post_media = request.files.get('post_media') + + if post_text: + new_post = Post( + username=current_user.username, + text=post_text + ) + db.session.add(new_post) + + if post_media and allowed_file(post_media.filename, 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 + else: + return redirect(url_for('upload_post')) + + db.session.commit() + return redirect(url_for('user_posts', username=current_user.username)) + else: + return redirect(url_for('upload_post')) + + return render_template('upload_post.html') + +@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: + current_user.username = new_username + db.session.commit() + 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('/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')) + +class UpdateCookiesForm(FlaskForm): + cookies = StringField('Количество печенек', validators=[DataRequired()]) + submit = SubmitField('Применить') + +@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: + pass + 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')) + +@app.route('/publication_rules') +def publication_rules(): + return render_template('publication_rules.html') + +@app.route('/shop') +@login_required +def shop(): + + items = Item.query.filter_by(visible=True).all() + user_cookies = Cookies.query.filter_by(username=current_user.username).first().cookies if Cookies.query.filter_by(username=current_user.username).first() else 0 + user_item_ids = {ui.item_id for ui in UserItem.query.filter_by(username=current_user.username).all()} + return render_template('shop.html', items=items, user=current_user, user_cookies=user_cookies, user_item_ids=user_item_ids) + +@app.route('/buy_item/', methods=['POST']) +@login_required +def buy_item(item_id): + username = current_user.username + user_cookies = Cookies.query.filter_by(username=username).first() + item = Item.query.get(item_id) + + if not user_cookies or not item or not item.visible or user_cookies.cookies < item.price: + return redirect(url_for('shop')) + + if UserItem.query.filter_by(username=username, item_id=item.id).first(): + return redirect(url_for('shop')) + + user_cookies.cookies -= item.price + db.session.add(UserItem(username=username, item_id=item.id)) + db.session.commit() + return redirect(url_for('shop')) + + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + app.run(debug=False) \ No newline at end of file diff --git a/static/artberry.ico b/static/artberry.ico new file mode 100644 index 0000000000000000000000000000000000000000..eabcebc8ef24e65ef23d22dfa8fb63e9c8bdc852 GIT binary patch literal 102894 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 OC4;A{pUXO@geCw*gGOHf literal 0 HcmV?d00001 diff --git a/static/banners/default-banner.png b/static/banners/default-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..0180f09ab4eb9324939e277667f3f1ace22200e5 GIT binary patch literal 10644 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4aTa()7BevDDT6R$#Zvn+ z1_n(xPZ!6KiaBp@I`TFcFdR5w7%c4gLBD-wgj2>lkuUdO$DOwZY6XEGdw-TfSPTs5 z5(l76K{f*j%b|sl2f`9aWMGD{Tt=0F0%9~kz{z1Wg+Ow}XqEwo!)W0E4u;XfVYF~y z7|jc#d4XZHUKp(xM%zf>G%%VMAi*$NFO1d;qj_O8FEEVO3#0YI=r|H24UFc6(Y!F4 z7e?~}!|1RHBoT}bA%eqUv|bpk7e?!a(RyLDUKpK-2j+s&;dF3OFgk + + diff --git a/static/card/views-icon.svg b/static/card/views-icon.svg new file mode 100644 index 0000000..ad439f2 --- /dev/null +++ b/static/card/views-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..5024679 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,955 @@ +@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&display=swap'); +* { + font-family: 'Comfortaa', sans-serif; +} +html, body { + height: 100%; + margin: 0; +} +body { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + background-color: #3f3242; +} +.content { + flex: 1; +} +.preview-container { + margin-top: 20px; + text-align: center; +} +.preview-container img { + max-width: 100%; + max-height: 300px; + border: 1px solid #ccc; + border-radius: 5px; + margin-top: 10px; +} +#autocomplete-suggestions { + position: absolute; + top: 260px; + left: 42%; + width: 11.5%; + background-color: #86597f; + border: 1px solid #9a6fa0; + border-radius: 6px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 1000; + max-height: 250px; + overflow-y: auto; + font-family: 'Comfortaa', sans-serif; + padding: 0; +} + +.suggestion-item { + padding: 1px; + font-size: 14px; + color: #ffffff; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.suggestion-item:hover { + background-color: #3f3242; + color: #ffffff; +} + +.suggestion-item:active { + background-color: #e2e2e2; +} + +.suggestion-item:not(:last-child) { + border-bottom: 1px solid #f0f0f0; +} +.hidden { + display: none; +} +.button { + background-color: #6a4664; + border: none; + outline: none; + border-radius: 3px; + color:white; + transition: background-color 0.3s ease; + text-decoration: none; + padding: 7px 10px; + margin-top: 10px; + margin-bottom: 10px; +} +nav .button:hover { + background-color: #9a6fa0; +} +a { + color:yellow; + text-decoration: none; +} +.input-field { + display: flex; + align-items: center; + position: relative; + max-width: 100%; +} +label { + color:white +} +span { + color:white; +} +h2 { + color:white; +} +input { + color:white; +} +.blur { + filter: blur(15px); + transition: filter 0.3s ease; +} + +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + justify-content: center; + align-items: center; + z-index: 1050; +} + +.modal-content { + background: #3f3242; + padding: 20px; + text-align: center; + border-radius: 8px; + width: 300px; + z-index: 1060; +} + +.modal-buttons { + margin-top: 20px; +} + +.modal-button { + background: #6a4664; + color: white; + padding: 10px 20px; + border: none; + transition: background-color 0.3s ease; + cursor: pointer; + margin: 0 10px; + border-radius: 3px; +} + +.modal-button.cancel { + background: #6a4664; +} +.input-field { + font-size: 18px; + padding: 5px 10px; + width: 100%; + max-width: 400px; + outline: none; + background: #6a4664; + color: #FFFFFF; + border: none; + border-radius: 3px; + transition: .3s ease; +} +header { + margin-top: 120px; +} +.title { + display: flex; + justify-content: center; +} +.search-form { + display: flex; + justify-content: center; + gap: 5px; +} +.input-field:focus { + background: #86597f; + border: none; + border-radius: 3px; +} +.input-field::placeholder { + color: #DDDDDD; +} +nav { + display: flex; + justify-content: center; + align-items: center; + background-color: #6a4664; + width: 100%; + height: 60px; + box-sizing: border-box; + position: fixed; + top: 0; + left: 0; + z-index: 1000; +} +nav .button { + padding: 10px 45px; + text-decoration: none; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + background-color: #845e80; +} +footer { + display: flex; + justify-content: center; + align-items: center; + background-color: #6a4664; + width: 100%; + height: 60px; + box-sizing: border-box; +} +footer .button { + padding: 10px 20px; + text-decoration: none; + color: white; + background-color: #845e80; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} +footer .button:hover { + background-color: #9a6fa0; +} +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + margin: 20px 0; +} +.pagination .button { + padding: 5px 10px; + text-decoration: none; + color: white; + background-color: #6a4664; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} +.pagination .button:hover { + background-color: #845e80; +} +.pagination .button:focus, +.pagination .button:active { + outline: none; +} +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + box-sizing: border-box; + padding: 20px; + gap: 15px; +} +.video-player { + display: flex; + justify-content: center; + align-items: center; +} +.video-title { + display: flex; + margin-top: 80px; + justify-content: center; + text-align: center; +} +.video-details { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +} +.gallery { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 10px; + margin: 20px auto; + padding: 20px; + max-width: 1200px; +} +.card { + border-radius: 10px; + overflow: hidden; + background-color: #6a4664; +} +.card img { + width: 100%; + height: 200px; + object-fit: cover; +} +.card p { + margin: 10px; + font-size: 1rem; +} +.card:hover { + transform: scale(1.05); +} +.comic-viewer { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + gap: 20px; + flex-wrap: wrap; +} +.comic-card { + border-radius: 5px; + overflow: hidden; + transition: transform 0.3s; +} +.comic-card:hover { + transform: scale(1.05); +} +.comic-card img { + width: 100%; + height: auto; +} +.comic-card p { + margin: 10px; +} +.comicname { + display: flex; + justify-content: center; +} +.banner { + width: 100%; + height: 250px; + position: relative; + margin-top: 0; +} +.avatar, +.banner img { + width: 100%; + height: 100%; + object-fit: cover; +} +.avatar-container { + position: absolute; + top: 70%; + left: 50%; + transform: translateX(-50%); + z-index: 1; + width: 128px; + height: 128px; + overflow: hidden; + border-radius: 50%; + border: 3px solid #845e80; + margin-bottom:80px; +} +.current-item-image { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: auto; + border-radius: 10px; +} +h2 { + margin: 30px 0 10px; +} +.bio { + margin-top: 65px; + padding: 1px; + background-color: #845e80; + border-radius: 10px; + max-width: 60%; + width: 80%; + margin-left: auto; + margin-right: auto; + margin-bottom:10px; +} +.bio-content { + color: #fff; +} +.bio h2 { + margin: 0 0 10px; + font-size: 1.5rem; +} +.bio p { + font-size: 1rem; + line-height: 1.5; +} +.header, +body { + display: flex; +} + +.cookie-balance, +.home-button, +.cookie-balance, +h1 { + margin-top: 20px; + color: #fff; +} +.header { + flex-direction: column; + align-items: center; + justify-content: center; +} +.cookie-balance { + background-color: #6a4664; + border-radius: 5px; + padding: 10px 20px; + font-size: 1.2rem; + display: inline-block; +} +.item-container { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + padding: 20px; + max-width: 1200px; + width: 100%; + margin: 0 auto; + justify-content: center; +} +.item { + background-color: #6e4568; + border-radius: 10px; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + transition: transform 0.3s, box-shadow 0.3s; +} +.item:hover { + transform: scale(1.05); +} +.comic-pages { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + margin: 20px auto; + max-width: 1200px; +} +.comic-pages img { + max-width: 90%; + height: auto; + border: 2px solid #845e80; + border-radius: 5px; + transition: transform 0.3s; +} +.comic-pages img:hover { + transform: scale(1.05); + border-color: #9a6fa0; +} +.item img { + width: 100%; + max-width: 200px; + height: auto; + border-radius: 5px; + margin-bottom: 10px; + transition: transform 0.3s; +} +.home-button { + background-color: #6a4664; + border: none; + outline: none; + border-radius: 3px; + color:white; + transition: background-color 0.3s ease; + text-decoration: none; + padding: 7px 10px; + margin-top: 10px; + margin-bottom: 10px; +} +.home-button:hover { + background-color: #9a6fa0; +} +.item button { + background-color: #9a6fa0; + border: none; + outline: none; + border-radius: 3px; + color:white; + transition: background-color 0.3s ease; + text-decoration: none; + padding: 7px 10px; + margin-top: 10px; + margin-bottom: 10px; +} +.item-button:hover { + background-color: #6a4664; +} +.item img:hover { + transform: scale(1.1); +} +.item + { + border: none; + padding: 10px 20px; + border-radius: 5px; +} +.home-button:hover, +.item button:hover { + background-color: #9a6fa0; + filter: brightness(1.1); +} +.home-button { + display: inline-block; + margin: 30px auto 20px; + padding: 12px 25px; + text-decoration: none; + border-radius: 5px; + font-weight: 700; +} +.profile-gallery { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + margin: 20px auto; + padding: 20px; + max-width: 1200px; +} +.profile-gallery .card { + border-radius: 10px; + overflow: hidden; + background-color: #6a4664; + text-align: center; + width: 240px; + height: 210px; +} +.profile-gallery .card img { + width: 100%; + height: 200px; + object-fit: cover; +} +.profile-gallery .card .card-title { + margin: 10px; + font-size: 1rem; + color: white; +} +.profile-art-card { + background-color: #4a3b52; +} +.profile-art-card:hover { + transform: scale(1.05); +} +.profile-video-card { + background-color: #3b4a52; +} +.profile-video-card:hover { + transform: scale(1.05); +} +.profile-comic-card { + background-color: #4b523b; +} +.profile-comic-card:hover { + transform: scale(1.05); +} +.biotext { + display: flex; + justify-content: center; +} +p { + color:white; +} +::-webkit-scrollbar { + width: 10px; + height: 10px; +} +::-webkit-scrollbar-track { + background-color: #3f3242; + border-radius: 10px; +} +::-webkit-scrollbar-thumb { + background-color: #845e80; + border-radius: 10px; + border: 2px solid #3f3242; + transition: background-color 0.3s ease; +} +::-webkit-scrollbar-thumb:hover { + background-color: #9a6fa0; +} +.comment { + display: flex; + align-items: flex-start; + background-color: #6a4664; + padding: 15px; + margin: 10px 0; + border-radius: 3px; + position: relative; +} +.comment .avatar { + width: 50px; + height: 50px; + border-radius: 50%; + margin-right: 15px; + object-fit: cover; + border: 2px solid #845e80; +} +input[type="checkbox"]:checked { + background-color: #6a4664; + accent-color: #6a4664; +} +.comment .content { + flex: 1; +} +.comment .content p { + margin: 5px 0; +} +.comment .content p:first-child { + font-weight: bold; + color: #fff; +} +.comment .content p:last-child::before { + content: "\A"; + white-space: pre; + display: block; +} +.comment .content time { + font-size: 0.8rem; + color: #fff; +} +.delete-button { + background-color: #845e80; + color: #fff; + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.3s ease; + position: absolute; + bottom: 4px; + left: 10px; +} +.delete-button:hover { + background-color: #9e728f; +} +.delete-button:active { + background-color: #6e4c65; +} +.details img { + max-width: 85%; + height: auto; + display: block; + margin: 0 auto; +} +video { + max-width: 85%; + height: auto; + display: block; + margin: 0 auto; +} + +.button { + display: inline-block; + margin-left: 10px; +} +.comments-list { + max-height: none; + overflow: visible; +} +.comments-list p { + color: #fff; +} +.comment .content a.username-link { + color: #ffd700; + text-decoration: none; +} +.comment .content a.username-link:hover { + text-decoration: underline; +} + + +@media (min-width: 481px) and (max-width: 767px) { +.gallery { + grid-template-columns: repeat(1, 1fr); +} +nav .button { + padding: 8px 14px; +} +footer .button { + padding: 8px 20px; +} +nav { + margin-right: 10px; +} +.pagination .button { + padding: 25px 25px; + font-size: 20px + + } + +.input-field { + padding: 15px 16px; +} + +.modal-button { + padding: 15px 45px; +} +.navigation .button { + padding: 25px 35px; + +} +.profile-gallery{ + grid-template-columns: repeat(1, 1fr); +} +} + +@media (max-width: 480px) { + .gallery { + grid-template-columns: repeat(1, 1fr); + } + nav .button { + padding: 6px 6px; + } + footer .button { + padding: 6px 12px; + } + nav { + margin-right: 5px; + } + .pagination .button { + padding: 15px 15px; + font-size: 16px; + } + .input-field { + padding: 10px 12px; + } + .modal-button { + padding: 10px 30px; + } + .navigation .button { + padding: 15px 25px; + } + .profile-gallery { + grid-template-columns: repeat(1, 1fr); + } + .login-button { + width: 150px; + height: 50px; + } +} +/*iphone 14 Plus*/ +@media (max-width: 430px) { + .gallery { + grid-template-columns: repeat(1, 1fr); + } + nav .button { + padding: 12px 5px; + } + footer .button { + padding: 12px 5px; + } + nav { + margin-right: 5px; + } + .pagination .button { + padding: 15px 15px; + font-size: 16px; + } + .input-field { + padding: 10px 30px; + } + .modal-button { + padding: 10px 30px; + } + .navigation .button { + padding: 15px 25px; + } + .profile-gallery { + grid-template-columns: repeat(1, 1fr); + } + .login-button { + width: 150px; + height: 50px; + } +} +/*iphone 6/7/8 Plus*/ +@media (max-width: 414px) { + .gallery { + grid-template-columns: repeat(1, 1fr); + } + nav .button { + padding: 12px 5px; + } + footer .button { + padding: 12px 5px; + } + nav { + margin-right: 5px; + } + .pagination .button { + padding: 15px 15px; + font-size: 16px; + } + .input-field { + padding: 10px 12px; + } + .modal-button { + padding: 10px 30px; + } + .navigation .button { + padding: 15px 25px; + } + .profile-gallery { + grid-template-columns: repeat(1, 1fr); + } + .login-button { + width: 150px; + height: 50px; + } +} +/*iphone pixel 3*/ +@media (min-width: 403px) and (max-width: 450px) { + .gallery { + grid-template-columns: repeat(1, 1fr); + } + nav .button { + padding: 12px 4px; + } + footer .button { + padding: 12px 5px; + } + nav { + margin-right: 5px; + } + .pagination .button { + padding: 15px 15px; + font-size: 16px; + } + .input-field { + padding: 10px 12px; + } + .modal-button { + padding: 10px 30px; + } + .navigation .button { + padding: 15px 25px; + } + .profile-gallery { + grid-template-columns: repeat(1, 1fr); + } + .login-button { + width: 150px; + height: 50px; + } +} +/*iphone 12*/ +@media (min-width: 382px) and (max-width: 402px) { + .gallery { + grid-template-columns: repeat(1, 1fr); + } + nav .button { + padding: 12px 2px; + } + footer .button { + padding: 12px 5px; + } + nav { + margin-right: 5px; + } + .pagination .button { + padding: 15px 15px; + font-size: 16px; + } + .input-field { + padding: 10px 12px; + } + .modal-button { + padding: 10px 30px; + } + .navigation .button { + padding: 15px 25px; + } + .profile-gallery { + grid-template-columns: repeat(1, 1fr); + } + .login-button { + width: 150px; + height: 50px; + } +} +/*iphone 6/7/8/X/XR*/ +@media (min-width: 372px) and (max-width: 381px) { + .gallery { + grid-template-columns: repeat(1, 1fr); + } + nav .button { + padding: 10px 1px; + } + footer .button { + padding: 10px 1px; + } + nav { + margin-right: 5px; + } + .pagination .button { + padding: 15px 15px; + font-size: 16px; + } + .input-field { + padding: 10px 12px; + } + .modal-button { + padding: 10px 30px; + } + .navigation .button { + padding: 15px 25px; + } + .profile-gallery { + grid-template-columns: repeat(1, 1fr); + } + .login-button { + width: 150px; + height: 50px; + } +} +/*iphone 4/5/SE1*/ +@media (min-width: 200px) and (max-width: 371px) { + .gallery { + grid-template-columns: repeat(1, 1fr); + } + nav .button { + padding: 1px 1px; + } + footer .button { + padding: 1px 1px; + } + nav { + margin-right: 5px; + } + .pagination .button { + padding: 15px 15px; + font-size: 16px; + } + .input-field { + padding: 10px 1px; + } + .modal-button { + padding: 10x 30px; + } + .navigation .button { + padding: 15px 25px; + } + .profile-gallery { + grid-template-columns: repeat(1, 1fr); + } + .-button { + padding: 15px 25px; + } + .login-button { + width: 150px; + height: 50px; + } +} + diff --git a/static/navbar/discord-hover.svg b/static/navbar/discord-hover.svg new file mode 100644 index 0000000..d3f9e3e --- /dev/null +++ b/static/navbar/discord-hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/navbar/discord-icon.svg b/static/navbar/discord-icon.svg new file mode 100644 index 0000000..79365cd --- /dev/null +++ b/static/navbar/discord-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/navbar/logo.svg b/static/navbar/logo.svg new file mode 100644 index 0000000..02926a7 --- /dev/null +++ b/static/navbar/logo.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/navbar/search-hover.svg b/static/navbar/search-hover.svg new file mode 100644 index 0000000..5ea9002 --- /dev/null +++ b/static/navbar/search-hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/navbar/search-icon.svg b/static/navbar/search-icon.svg new file mode 100644 index 0000000..91f1379 --- /dev/null +++ b/static/navbar/search-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/navbar/translate-hover.svg b/static/navbar/translate-hover.svg new file mode 100644 index 0000000..ae87dea --- /dev/null +++ b/static/navbar/translate-hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/navbar/translate-icon.svg b/static/navbar/translate-icon.svg new file mode 100644 index 0000000..5813c97 --- /dev/null +++ b/static/navbar/translate-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/navbar/tray-icon.svg b/static/navbar/tray-icon.svg new file mode 100644 index 0000000..bf84034 --- /dev/null +++ b/static/navbar/tray-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/navbar/video-icon.svg b/static/navbar/video-icon.svg new file mode 100644 index 0000000..2817cb2 --- /dev/null +++ b/static/navbar/video-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/templates/auth.html b/templates/auth.html new file mode 100644 index 0000000..17f9b67 --- /dev/null +++ b/templates/auth.html @@ -0,0 +1,29 @@ + + + + + + {% block title %}{{ title }}{% endblock %} + + + + + +
+

🫐artberry🫐

+

{{ title }}

+ + {% block content %}{% endblock %} + + +
+ + diff --git a/templates/card.html b/templates/card.html new file mode 100644 index 0000000..a0f3c55 --- /dev/null +++ b/templates/card.html @@ -0,0 +1,79 @@ + + + + + Navbar + + + {% include 'navbar.html' %} + +
+
+
+
+ Netwide Assembler +
+
+ Лайк + 1344 +
+
+ Просмотры + 321132 +
+
+
+

The Netwide Assembler, NASM, is an 80x86 and x86-64 assembler designed for portability and + modularity. It supports a range of object file formats, including Linux and *BSD a.out, ELF, Mach-O, + 16-bit and 32-bit .obj (OMF) format, COFF (including its Win32 and Win64 variants.)

+
+
+ + + diff --git a/templates/comic_edit.html b/templates/comic_edit.html new file mode 100644 index 0000000..1b55426 --- /dev/null +++ b/templates/comic_edit.html @@ -0,0 +1,52 @@ + + + + + + + + + + + 🫐comic edit - artberry🫐 + + +

Edit Comic: {{ comic.name }}

+
+ {{ form.hidden_tag() }} +

Add a New Page:

+
+ + +
+ +
+
+

Existing Pages:

+
    + {% for page in comic_pages %} +
  • + Comic Page {{ loop.index }} +

    Page {{ loop.index }}

    +
    +
    + {{ form.hidden_tag() }} + + +
    +
    +
    +
    + {{ form.hidden_tag() }} + + + +
    +
    +
  • + {% endfor %} +
+
+ Return to Comics List + + diff --git a/templates/comic_upload.html b/templates/comic_upload.html new file mode 100644 index 0000000..30766fe --- /dev/null +++ b/templates/comic_upload.html @@ -0,0 +1,59 @@ + + + + + + 🫐comic upload - artberry🫐 + + + + + + + +

Upload Comic

+
+ + +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+ + + diff --git a/templates/comics.html b/templates/comics.html new file mode 100644 index 0000000..69b996f --- /dev/null +++ b/templates/comics.html @@ -0,0 +1,21 @@ + + +{% extends "content.html" %} + +{% block title %}🫐comics - artberry🫐{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/templates/content.html b/templates/content.html new file mode 100644 index 0000000..05c2b26 --- /dev/null +++ b/templates/content.html @@ -0,0 +1,240 @@ + + + + + + {% block title %}🫐artberry🫐{% endblock %} + + + + {% if extra_css %} + + {% endif %} + + + + + + +
+

🫐artberry🫐

+
+ + +
+
+ +
+
+ {% block content %} + + {% if content_type == 'art' or content_type == 'video' or content_type == 'comic' %} +
+

{{ content.title }}

+ {% if content_type == 'art' %} + {{ content.title }} + {% elif content_type == 'video' %} + + {% elif content_type == 'comic' %} +
+ {% for page in content.pages %} + Comic Page {{ loop.index }} + {% endfor %} +
+ {% endif %} +

Uploaded by: {{ content.username }}

+

Description: {{ content.description }}

+
+ +
+

Votes: {{ content.cookie_votes }} 🍪

+ {% if current_user.is_authenticated %} +
+ + +
+ {% else %} +

You need to log in to vote.

+ {% endif %} +
+ + {% else %} +

Unknown content type.

+ {% endif %} + +
+

Comments

+ {% if comments %} +
    + {% for comment in comments %} +
  • + Avatar + {{ comment.username }}: {{ comment.comment_text }} +
  • + {% endfor %} +
+ {% else %} +

No comments yet. Be the first to comment!

+ {% endif %} + {% if current_user.is_authenticated %} +
+ + +
+ {% else %} +

You need to log in to comment.

+ {% endif %} +
+ + {% endblock %} +
+ + {% if pagination %} + + {% endif %} + + + + + + + + + \ No newline at end of file diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..60c55c7 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,16 @@ + + + + + + + + + Error {{ error_code }} + + +

Error {{ error_code }}

+

{{ error_message }}

+

:(

+ + diff --git a/templates/image_edit.html b/templates/image_edit.html new file mode 100644 index 0000000..2bd52c6 --- /dev/null +++ b/templates/image_edit.html @@ -0,0 +1,29 @@ + + + + + + 🫐Edit tags - artberry🫐 + + + +
+

Edit image tags:

+ +
+ Image preview +
+ +
+ {{ form.hidden_tag() }} +
+ + {{ form.tags(class="input-field") }} +
+ +
+ + Cancel +
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..13b4ff5 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,16 @@ +{% extends "content.html" %} + +{% block title %}🫐arts - artberry🫐{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..de0c504 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,35 @@ +{% extends "auth.html" %} +{% block title %}🫐login - artberry🫐{% endblock %} +{% set action = 'login' %} + +{% block content %} +
+ {{ form.hidden_tag() }} + + {% for field, errors in form.errors.items() %} +
    + {% for error in errors %} +
  • {{ field.label }}: {{ error }}
  • + {% endfor %} +
+ {% endfor %} + + + {{ form.username(class="input-field", placeholder="Enter username") }}
+ + + {{ form.password(class="input-field", placeholder="Enter password") }}
+ +
+ {{ form.recaptcha.label }} + {{ form.recaptcha() }}
+
+ + {{ form.submit(class="login-button button", value="Login") }} +
+ + +{% endblock %} diff --git a/templates/navbar.html b/templates/navbar.html new file mode 100644 index 0000000..3097494 --- /dev/null +++ b/templates/navbar.html @@ -0,0 +1,308 @@ + + + + + + Navbar + + + + + + + + \ No newline at end of file diff --git a/templates/panel.html b/templates/panel.html new file mode 100644 index 0000000..1cecabb --- /dev/null +++ b/templates/panel.html @@ -0,0 +1,164 @@ + + + + + + Админ Панель + + + +

Админ Панель

+

Управление Артами

+ + + + + + + + + + + + {% for art in arts %} + + + + + + + + {% endfor %} + +
IDФайлПользовательТегиДействия
{{ art.id }}{{ art.image_file }}{{ art.username }} +
+ + + +
+
+
+ + +
+
+

Управление Комментариями

+ + + + + + + + + + + + {% for comment in comments %} + + + + + + + + {% endfor %} + +
IDТекст комментарияПользовательДатаДействия
{{ comment.id }} +
+ + + +
+
{{ comment.username }}{{ comment.comment_date }} +
+ + +
+
+

Управление Видео

+ + + + + + + + + + + + + {% for video in videos %} + + + + + + + + + {% endfor %} + +
IDНазваниеПользовательТегиОписаниеДействия
{{ video.id }}{{ video.video_name }}{{ video.username }} +
+ + + +
+
+
+ + + + +
+
+
+ + +
+
+

Управление Пользователями

+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
IDИмя пользователяIP-адресДата созданияПеченькиДействия
{{ user.id }}{{ user.username }}{{ user.ip_address }}{{ user.creation_date }}{{ user_cookies[user.id] if user.id in user_cookies else 0 }} +
+ + + + +
+
+ + + +
+
+ + +
+
+ + diff --git a/templates/post.html b/templates/post.html new file mode 100644 index 0000000..bf261a0 --- /dev/null +++ b/templates/post.html @@ -0,0 +1,89 @@ + + + + + + + {{ username }} posts + + +

{{ username }} Posts

+ + {% if posts %} +
+ {% for post in posts %} +
+ +

{{ post.text }}

+ + {% if post.media_file %} +
+ {{ 'GIF' if post.media_file.endswith('.gif') else 'Image' }} +
+ {% endif %} + + {% if post.username == current_user.username %} +
+ + +
+ {% endif %} + +
+

Comments

+
+ {% for comment in post.comments %} +
+ + Avatar of {{ comment.username }} +
+

+ + {{ comment.username }} + + ({{ comment.comment_date.strftime('%Y-%m-%d %H:%M') }}): +

+

{{ comment.comment_text }}

+
+ + {% if comment.username == current_user.username %} +
+ + +
+ {% endif %} +
+ {% else %} +

No comments yet. Be the first to comment!

+ {% endfor %} +
+ + {% if current_user.is_authenticated %} +
+ + + +
+ {% else %} +

You need to log in to post a comment.

+ {% endif %} +
+
+ {% endfor %} +
+ {% else %} +

{{ username }} hasn't posted yet.

+ {% endif %} + + Back to Profile + + + + diff --git a/templates/privacy_policy.html b/templates/privacy_policy.html new file mode 100644 index 0000000..e7d7caf --- /dev/null +++ b/templates/privacy_policy.html @@ -0,0 +1,33 @@ + + + + + + + Privacy Policy + + + + + +

Privacy Policy

+

We value your privacy and are committed to keeping user data to a minimum. This privacy policy explains the information we collect, why we collect it, and how it is used.

+ +

Information Collection

+

• We do not collect any personal information, such as names, email addresses, or other identifiers, beyond what is necessary for site functionality.

+

• The only information we gather is the user’s IP address, which is stored solely for anti-bot protection purposes.

+

• We do not track user activities, browsing history, or any other data outside of what is required to operate the site securely.

+ +

Use of Collected Data

+

• IP addresses are used exclusively to detect and prevent automated (bot) traffic to ensure the site remains accessible for legitimate users.

+

• No data is shared with third parties, sold, or used for advertising purposes.

+ +

Data Storage

+

• Any data collected is stored securely and is only retained for as long as needed to perform anti-bot functions.

+

• Once data is no longer necessary, it is removed from our system.

+ +

Third-Party Links

+

• Our site may contain links to third-party content, but we are not responsible for the privacy practices of other websites.

+

• We encourage users to review the privacy policies of any third-party sites they visit.

+ + diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..1a023b6 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,132 @@ + + + + + + + + + + 🫐{{ user.username }} profile - artberry🫐 + + + + + + +
+

{{ user.username }}

+

{{ user.bio }}

+
+ Mainpage + Posts + {% if is_current_user %} + Edit Profile + Upload Post + {% endif %} + + {% if current_user.is_authenticated and current_user.username != user.username %} + {% if not subscribed %} +
+ + +
+ {% else %} +
+ + +
+ {% endif %} + {% endif %} + +

Latest Popular Arts

+ {% if arts %} + + View All Arts ({{ total_arts }}) + + {% endif %} + + +

Latest Popular Videos

+ {% if videos %} + + View All Videos ({{ total_videos }}) + + {% endif %} + + +

Latest Popular Comics

+ {% if comics %} + + View All Comics ({{ total_comics }}) + + {% endif %} + + + + + + \ No newline at end of file diff --git a/templates/profile_edit.html b/templates/profile_edit.html new file mode 100644 index 0000000..36c9d03 --- /dev/null +++ b/templates/profile_edit.html @@ -0,0 +1,79 @@ + + + + + + + + + 🫐 Profile Edit - Artberry 🫐 + + +

Edit Profile

+ +
+ + + + +
+ +
+ + +
+ +
+ + + +

Your Decorations

+ +
+ + +
+ +
+ + + +

Change Username

+ +
+ +
+ +
+ + + +

About Me

+
+ +
+ +
+ + + + + + + +

Change Password

+ +
+ +
+ +
+ +
+ + diff --git a/templates/publication_rules.html b/templates/publication_rules.html new file mode 100644 index 0000000..879816a --- /dev/null +++ b/templates/publication_rules.html @@ -0,0 +1,34 @@ + + + + + + + 🫐 Publication Rules - Artberry 🫐 + + + + + + +
+

Publication Rules

+
+ +
+

Guidelines for Publishing Content

+

To maintain a safe and respectful environment, all users must follow these rules when publishing content:

+ +

Content Restrictions

+

• Artwork with watermarks, links to external sites (e.g., Patreon, social media), or any form of censorship is strictly prohibited.

+

• Submissions must feature only human-like characters. Non-human characters, minors, or characters appearing underage are not allowed.

+

• Content depicting extreme or disturbing themes, including violent or offensive genres, is not permitted.

+ +

Community Standards

+

• All submissions must be original and should not violate any copyright or intellectual property rights.

+

• Respect the work of other creators and ensure that all content meets the community’s standards of respect and appropriateness.

+

• Use appropriate tags and descriptions for your content to help others find and understand your work.

+ +
+ + diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..c40ad0b --- /dev/null +++ b/templates/register.html @@ -0,0 +1,31 @@ + +{% extends "auth.html" %} +{% block title %}🫐register - artberry🫐{% endblock %} +{% set action = 'register' %} + +{% block content %} +
+ {{ form.hidden_tag() }} + + + {{ form.username(class="input-field", placeholder="Enter username") }}
+ + + {{ form.password(class="input-field", placeholder="Enter password") }}
+ + + {{ form.confirm_password(class="input-field", placeholder="Repeat password") }}
+ +
+ {{ form.recaptcha.label }} + {{ form.recaptcha() }}
+
+ + {{ form.submit(class="login-button button", value="Register") }} +
+ + +{% endblock %} diff --git a/templates/shop.html b/templates/shop.html new file mode 100644 index 0000000..7b107f0 --- /dev/null +++ b/templates/shop.html @@ -0,0 +1,39 @@ + + + + + + + + + + 🫐shop - artberry🫐 + + +
+

Welcome to the Shop!

+ +
+ +
+ {% for item in items %} +
+ Item {{ item.id }} +
+ + {% if item.id in user_item_ids %} + + {% elif user_cookies < item.price %} + + {% else %} + + {% endif %} +
+
+ {% endfor %} +
+ MainPage + + diff --git a/templates/terms_of_use.html b/templates/terms_of_use.html new file mode 100644 index 0000000..e47dd26 --- /dev/null +++ b/templates/terms_of_use.html @@ -0,0 +1,40 @@ + + + + + + + 🫐 Terms of Service - Artberry 🫐 + + + + + + +
+

Terms of Service

+

By accessing or using Artberry, you agree to comply with these Terms of Service.

+
+ +
+

Prohibited Actions

+

• Unauthorized copying, mirroring, or redistributing any part of the site or its content is strictly prohibited.

+

• Exploiting or attempting to exploit vulnerabilities or flaws in the website’s security to harm, disable, or manipulate the site, its data, or its users is not allowed.

+

• Circumventing security measures or using automated tools, such as bots, scripts, or scraping software, to interact with the site in unauthorized ways is prohibited.

+

• Any effort to disrupt site functionality, alter the user experience, or perform denial-of-service attacks will result in immediate termination of access.

+ +

Content Guidelines

+

• Users must adhere to all Publication Rules. Posting of illegal content, non-compliant images, or other prohibited material is not allowed.

+

• Users are solely responsible for the content they publish on Artberry. Artberry reserves the right to remove or restrict access to any content that it deems harmful, offensive, or in violation of these Terms of Service.

+ +

Liability Disclaimer

+

• Artberry provides a platform for users to share artwork. We are not responsible for any content posted by users, nor do we endorse or verify any user-submitted content.

+

• Artberry is not liable for any misuse of the platform or for any damages resulting from unauthorized or harmful activities conducted by users.

+

• Users agree to use the platform at their own risk, acknowledging that Artberry disclaims all liability related to content hosted on the site and any user interactions that may occur.

+ +

Compliance with Laws

+

• Users are required to comply with all applicable local, state, national, and international laws while using Artberry.

+

• Artberry reserves the right to report any illegal activity conducted on the platform if necessary.

+
+ + diff --git a/templates/upload.html b/templates/upload.html new file mode 100644 index 0000000..6d05119 --- /dev/null +++ b/templates/upload.html @@ -0,0 +1,63 @@ + + + + + + Upload Art + + + + + + +
+

Upload Art

+
+
+
+ {{ form.hidden_tag() }} +
+ + {{ form.image_file(class="file-input", id="imageInput") }} +
+
+ + {{ form.tags(placeholder="tag1, tag2", class="input-field") }} +
+
+ {{ form.recaptcha() }} +
+
+ +
+ + +
+
+ + + diff --git a/templates/upload_post.html b/templates/upload_post.html new file mode 100644 index 0000000..b688554 --- /dev/null +++ b/templates/upload_post.html @@ -0,0 +1,25 @@ + + + + + + + + + 🫐 Upload Post - Artberry 🫐 + + +

Create New Post

+
+ + +
+
+ +
+
+ + +
+ + diff --git a/templates/upload_video.html b/templates/upload_video.html new file mode 100644 index 0000000..fbb888e --- /dev/null +++ b/templates/upload_video.html @@ -0,0 +1,59 @@ + + + + + + Upload Video + + + + + + + + +
+

Upload Video

+
+
+
+ {{ form.hidden_tag() }} + +
+ + {{ form.video_file(class="file-input") }} +
+ +
+ + {{ form.thumbnail(class="file-input") }} +
+ +
+ + {{ form.name(placeholder="Video name", class="input-field") }} +
+ +
+ + {{ form.tags(placeholder="tag1, tag2", class="input-field") }} +
+
+ + {{ form.description(placeholder="Write a description...", class="input-field") }} +
+
+ {{ form.recaptcha() }} +
+ +
+ +
+ +
+
+ + diff --git a/templates/user_pubs.html b/templates/user_pubs.html new file mode 100644 index 0000000..32b6625 --- /dev/null +++ b/templates/user_pubs.html @@ -0,0 +1,104 @@ + + + + + + 🫐{{ pub_type | capitalize }} - {{ username }} - Artberry🫐 + + + + + + + +
+

{{ pub_type | capitalize }} by {{ username }}

+
+ + +
+
+ +
+ + + + + + + diff --git a/templates/video_edit.html b/templates/video_edit.html new file mode 100644 index 0000000..d733cc5 --- /dev/null +++ b/templates/video_edit.html @@ -0,0 +1,45 @@ + + + + + + 🫐Edit video - artberry🫐 + + + +
+

Edit video: {{ video.video_name }}

+ +
+ {{ form.hidden_tag() }} + +
+ + {{ form.video_name(class="input-field") }} +
+ +
+ + {{ form.video_thumbnail(class="file-input") }} + {% if video.video_thumbnail_file %} + Thumbnail preview + {% endif %} +
+ +
+ + {{ form.description(class="input-field") }} +
+ +
+ + {{ form.tags(class="input-field") }} +
+ + +
+ + Cancel +
+ + diff --git a/templates/videos.html b/templates/videos.html new file mode 100644 index 0000000..c231258 --- /dev/null +++ b/templates/videos.html @@ -0,0 +1,27 @@ +{% extends "content.html" %} + +{% block title %}🫐videos - artberry🫐{% endblock %} + +{% block content %} + + + +{% endblock %} diff --git a/templates/view.html b/templates/view.html new file mode 100644 index 0000000..6525cc6 --- /dev/null +++ b/templates/view.html @@ -0,0 +1,150 @@ + + + + + + 🫐Content View - Artberry🫐 + + + + + + + + {% if content_type == 'art' %} +

Image

+
+ Art Image +

Author: {{ content.username }}

+

Publication Date: {{ content.publication_date }}

+

Tags: + {% for tag in content.tags.split(',') %} + {{ tag.strip() }}{% if not loop.last %}, {% endif %} + {% endfor %} +

+
+ {% elif content_type == 'video' %} +

Video

+
+ +

Author: {{ content.username }}

+

Publication Date: {{ content.publication_date }}

+

Description: {{ content.description }}

+

Tags: + {% for tag in content.tags.split(',') %} + {{ tag.strip() }}{% if not loop.last %}, {% endif %} + {% endfor %} +

+
+ {% elif content_type == 'comic' %} +

{{ content.name }}

+
+ {% if comic_pages %} + {% for page in comic_pages %} + Page {{ loop.index }} + {% endfor %} + {% else %} +

No pages available for this comic.

+ {% endif %} +
+ {% endif %} + +
+

Votes: {{ content.cookie_votes }} 🍪

+ {% if current_user.is_authenticated %} +
+ + +
+ {% else %} +

You need to log in to vote.

+ {% endif %} +
+ + {% if current_user.is_authenticated and current_user.username == content.username %} +
+ + +
+ {% endif %} + + {% if content_type != 'comic' %} +
+

Comments

+
+ {% for comment in comments %} +
+ + Avatar of {{ comment.username }} + {% if current_user.is_authenticated and comment.username == current_user.username %} +
+ + +
+ {% endif %} +
+
+

+ + {{ comment.username }} + + ({{ comment.comment_date.strftime('%Y-%m-%d %H:%M') }}): +

+

{{ comment.comment_text }}

+
+
+ {% else %} +

No comments yet. Be the first to comment!

+ {% endfor %} +
+ + {% if current_user.is_authenticated %} +
+ + + +
+ {% else %} +

You need to log in to post a comment.

+ {% endif %} +
+ {% endif %} + {% if content_type != 'comic' %} + + {% endif %} + +