|
|
from flask import Flask, render_template_string, request, redirect, url_for, session, flash |
|
|
import json |
|
|
import os |
|
|
import logging |
|
|
import threading |
|
|
import time |
|
|
from datetime import datetime |
|
|
from huggingface_hub import HfApi, hf_hub_download |
|
|
from werkzeug.utils import secure_filename |
|
|
import random |
|
|
|
|
|
app = Flask(__name__) |
|
|
app.secret_key = 'supersecretkey' |
|
|
DATA_FILE = 'datatest.json' |
|
|
REPO_ID = "Eluza133/w1f9" |
|
|
HF_TOKEN_WRITE = os.getenv("HF_TOKEN") |
|
|
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.DEBUG) |
|
|
|
|
|
|
|
|
def load_data(): |
|
|
try: |
|
|
download_db_from_hf() |
|
|
with open(DATA_FILE, 'r', encoding='utf-8') as file: |
|
|
data = json.load(file) |
|
|
if not isinstance(data, dict): |
|
|
logging.warning("Данные не в формате dict, инициализация пустой базы") |
|
|
return {'posts': [], 'users': {}} |
|
|
if 'posts' not in data: |
|
|
data['posts'] = [] |
|
|
if 'users' not in data: |
|
|
data['users'] = {} |
|
|
logging.info("Данные успешно загружены") |
|
|
return data |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка загрузки данных: {e}") |
|
|
return {'posts': [], 'users': {}} |
|
|
|
|
|
def save_data(data): |
|
|
try: |
|
|
with open(DATA_FILE, 'w', encoding='utf-8') as file: |
|
|
json.dump(data, file, ensure_ascii=False, indent=4) |
|
|
upload_db_to_hf() |
|
|
logging.info("Данные сохранены и загружены на HF") |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка сохранения данных: {e}") |
|
|
raise |
|
|
|
|
|
def upload_db_to_hf(): |
|
|
try: |
|
|
api = HfApi() |
|
|
api.upload_file( |
|
|
path_or_fileobj=DATA_FILE, |
|
|
path_in_repo=DATA_FILE, |
|
|
repo_id=REPO_ID, |
|
|
repo_type="dataset", |
|
|
token=HF_TOKEN_WRITE, |
|
|
commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" |
|
|
) |
|
|
logging.info("База данных загружена на Hugging Face") |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка загрузки базы: {e}") |
|
|
|
|
|
def download_db_from_hf(): |
|
|
try: |
|
|
hf_hub_download( |
|
|
repo_id=REPO_ID, |
|
|
filename=DATA_FILE, |
|
|
repo_type="dataset", |
|
|
token=HF_TOKEN_READ, |
|
|
local_dir=".", |
|
|
local_dir_use_symlinks=False |
|
|
) |
|
|
logging.info("База данных скачана с Hugging Face") |
|
|
except Exception as e: |
|
|
logging.error(f"Ошибка скачивания базы: {e}") |
|
|
if not os.path.exists(DATA_FILE): |
|
|
with open(DATA_FILE, 'w', encoding='utf-8') as f: |
|
|
json.dump({'posts': [], 'users': {}}, f) |
|
|
|
|
|
def periodic_backup(): |
|
|
while True: |
|
|
upload_db_to_hf() |
|
|
time.sleep(15) |
|
|
|
|
|
|
|
|
NAV_HTML = ''' |
|
|
<aside class="sidebar" id="sidebar"> |
|
|
<div class="sidebar-header"> |
|
|
<span class="nav-brand">Контент Хост</span> |
|
|
</div> |
|
|
<nav class="nav-links"> |
|
|
<a href="{{ url_for('feed') }}" class="nav-link"><span>📜</span> Лента</a> |
|
|
{% if is_authenticated %} |
|
|
<a href="{{ url_for('profile') }}" class="nav-link"><span>👤</span> Профиль ({{ username }})</a> |
|
|
<a href="{{ url_for('upload') }}" class="nav-link"><span>⬆️</span> Загрузить</a> |
|
|
<a href="{{ url_for('logout') }}" class="nav-link logout-btn"><span>🚪</span> Выйти</a> |
|
|
{% else %} |
|
|
<a href="{{ url_for('login') }}" class="nav-link"><span>🔑</span> Войти</a> |
|
|
<a href="{{ url_for('register') }}" class="nav-link"><span>✨</span> Регистрация</a> |
|
|
{% endif %} |
|
|
</nav> |
|
|
</aside> |
|
|
''' |
|
|
|
|
|
|
|
|
@app.route('/register', methods=['GET', 'POST']) |
|
|
def register(): |
|
|
if request.method == 'POST': |
|
|
username = request.form.get('username') |
|
|
password = request.form.get('password') |
|
|
data = load_data() |
|
|
|
|
|
if username in data['users']: |
|
|
flash('Пользователь уже существует!') |
|
|
return redirect(url_for('register')) |
|
|
|
|
|
data['users'][username] = {'password': password, 'bio': '', 'link': '', 'avatar': None} |
|
|
save_data(data) |
|
|
logging.info(f"Пользователь {username} зарегистрирован") |
|
|
flash('Регистрация успешна! Войдите в систему.') |
|
|
return redirect(url_for('login')) |
|
|
|
|
|
is_authenticated = 'username' in session |
|
|
username = session.get('username', None) |
|
|
html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ru"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Регистрация</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); margin: 0; padding: 0; min-height: 100vh; } |
|
|
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; } |
|
|
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } |
|
|
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; } |
|
|
.nav-links { display: flex; flex-direction: column; gap: 10px; } |
|
|
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: #3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; } |
|
|
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); } |
|
|
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; } |
|
|
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; } |
|
|
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } |
|
|
.container { max-width: 400px; margin: 20px auto 20px 270px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 20px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: margin-left 0.3s ease; } |
|
|
input { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #e2e8f0; border-radius: 8px; box-sizing: border-box; background: rgba(255, 255, 255, 0.8); } |
|
|
button { padding: 12px; background: #3b82f6; color: white; border: none; border-radius: 8px; cursor: pointer; width: 100%; transition: background 0.3s ease; } |
|
|
button:hover { background: #2563eb; } |
|
|
.flash { color: #ef4444; margin-bottom: 10px; text-align: center; } |
|
|
@media (max-width: 768px) { |
|
|
.sidebar { transform: translateX(-100%); width: 200px; } |
|
|
.sidebar.active { transform: translateX(0); } |
|
|
.menu-btn { display: block; } |
|
|
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); } |
|
|
input, button { font-size: 14px; padding: 10px; } |
|
|
.nav-brand { font-size: 1.2em; } |
|
|
.nav-link { padding: 10px; font-size: 14px; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container"> |
|
|
<h1>Регистрация</h1> |
|
|
{% with messages = get_flashed_messages() %} |
|
|
{% if messages %} |
|
|
{% for message in messages %} |
|
|
<div class="flash">{{ message }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
<form method="POST"> |
|
|
<input type="text" name="username" placeholder="Логин" required> |
|
|
<input type="password" name="password" placeholder="Пароль" required> |
|
|
<button type="submit">Зарегистрироваться</button> |
|
|
</form> |
|
|
<p>Уже есть аккаунт? <a href="{{ url_for('login') }}">Войти</a></p> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
document.getElementById('sidebar').classList.toggle('active'); |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(html, is_authenticated=is_authenticated, username=username) |
|
|
|
|
|
|
|
|
@app.route('/login', methods=['GET', 'POST']) |
|
|
def login(): |
|
|
if request.method == 'POST': |
|
|
username = request.form.get('username') |
|
|
password = request.form.get('password') |
|
|
data = load_data() |
|
|
|
|
|
if username in data['users'] and data['users'][username]['password'] == password: |
|
|
session['username'] = username |
|
|
session.permanent = True |
|
|
logging.info(f"Успешный вход: {username}") |
|
|
return redirect(url_for('feed')) |
|
|
flash('Неверный логин или пароль!') |
|
|
return redirect(url_for('login')) |
|
|
|
|
|
is_authenticated = 'username' in session |
|
|
username = session.get('username', None) |
|
|
html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ру"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Вход</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); margin: 0; padding: 0; min-height: 100vh; } |
|
|
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; } |
|
|
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } |
|
|
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; } |
|
|
.nav-links { display: flex; flex-direction: column; gap: 10px; } |
|
|
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: #3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; } |
|
|
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); } |
|
|
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; } |
|
|
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; } |
|
|
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } |
|
|
.container { max-width: 400px; margin: 20px auto 20px 270px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 20px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: margin-left 0.3s ease; } |
|
|
input { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #e2e8f0; border-radius: 8px; box-sizing: border-box; background: rgba(255, 255, 255, 0.8); } |
|
|
button { padding: 12px; background: #3b82f6; color: white; border: none; border-radius: 8px; cursor: pointer; width: 100%; transition: background 0.3s ease; } |
|
|
button:hover { background: #2563eb; } |
|
|
.flash { color: #ef4444; margin-bottom: 10px; text-align: center; } |
|
|
@media (max-width: 768px) { |
|
|
.sidebar { transform: translateX(-100%); width: 200px; } |
|
|
.sidebar.active { transform: translateX(0); } |
|
|
.menu-btn { display: block; } |
|
|
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); } |
|
|
input, button { font-size: 14px; padding: 10px; } |
|
|
.nav-brand { font-size: 1.2em; } |
|
|
.nav-link { padding: 10px; font-size: 14px; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container"> |
|
|
<h1>Вход</h1> |
|
|
{% with messages = get_flashed_messages() %} |
|
|
{% if messages %} |
|
|
{% for message in messages %} |
|
|
<div class="flash">{{ message }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
<form method="POST"> |
|
|
<input type="text" name="username" placeholder="Логин" required> |
|
|
<input type="password" name="password" placeholder="Пароль" required> |
|
|
<button type="submit">Войти</button> |
|
|
</form> |
|
|
<p>Нет аккаунта? <a href="{{ url_for('register') }}">Зарегистрироваться</a></p> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
document.getElementById('sidebar').classList.toggle('active'); |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(html, is_authenticated=is_authenticated, username=username) |
|
|
|
|
|
|
|
|
@app.route('/logout') |
|
|
def logout(): |
|
|
session.pop('username', None) |
|
|
logging.info("Пользователь вышел из системы") |
|
|
return redirect(url_for('feed')) |
|
|
|
|
|
|
|
|
@app.route('/', methods=['GET', 'POST']) |
|
|
def feed(): |
|
|
data = load_data() |
|
|
posts = sorted(data.get('posts', []), key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True) |
|
|
is_authenticated = 'username' in session |
|
|
username = session.get('username', None) |
|
|
|
|
|
search_query = request.form.get('search', '').strip().lower() if request.method == 'POST' else request.args.get('search', '').strip().lower() |
|
|
|
|
|
if search_query: |
|
|
posts = [post for post in posts if search_query in post['title'].lower() or search_query in post['description'].lower()] |
|
|
|
|
|
html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ru"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Лента</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); margin: 0; padding: 0; min-height: 100vh; } |
|
|
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; } |
|
|
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } |
|
|
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; } |
|
|
.nav-links { display: flex; flex-direction: column; gap: 10px; } |
|
|
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: #3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; } |
|
|
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); } |
|
|
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; } |
|
|
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; } |
|
|
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } |
|
|
.container { max-width: 1200px; margin: 20px auto 20px 270px; padding: 20px; transition: margin-left 0.3s ease; } |
|
|
.post-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } |
|
|
.post-item { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); text-decoration: none; color: #2d3748; transition: transform 0.3s ease; } |
|
|
.post-item:hover { transform: scale(1.02); } |
|
|
.post-preview { width: 100%; border-radius: 8px; height: 200px; object-fit: cover; cursor: pointer; } |
|
|
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; } |
|
|
.modal img { max-width: 90%; max-height: 90%; object-fit: contain; transform: scale(1); transition: transform 0.2s ease; } |
|
|
.stats { font-size: 0.9em; color: #666; margin-top: 5px; } |
|
|
.username-link { color: #3b82f6; text-decoration: none; font-weight: 600; } |
|
|
.username-link:hover { text-decoration: underline; } |
|
|
.search-container { margin-bottom: 20px; } |
|
|
.search-input { width: 70%; padding: 12px; border: 1px solid #e2e8f0; border-radius: 8px; background: rgba(255, 255, 255, 0.8); } |
|
|
.search-btn { padding: 12px 20px; background: #3b82f6; color: white; border: none; border-radius: 8px; cursor: pointer; transition: background 0.3s ease; } |
|
|
.search-btn:hover { background: #2563eb; } |
|
|
@media (max-width: 768px) { |
|
|
.sidebar { transform: translateX(-100%); width: 200px; } |
|
|
.sidebar.active { transform: translateX(0); } |
|
|
.menu-btn { display: block; } |
|
|
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); } |
|
|
.post-grid { grid-template-columns: 1fr; gap: 15px; } |
|
|
.post-item { padding: 10px; } |
|
|
.post-preview { height: 150px; } |
|
|
h1 { font-size: 1.5em; } |
|
|
.search-input { width: 60%; padding: 10px; } |
|
|
.search-btn { padding: 10px 15px; font-size: 14px; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container"> |
|
|
<h1>Лента публикаций</h1> |
|
|
<div class="search-container"> |
|
|
<form method="POST"> |
|
|
<input type="text" name="search" class="search-input" placeholder="Поиск по названию или описанию" value="{{ search_query }}"> |
|
|
<button type="submit" class="search-btn">Искать</button> |
|
|
</form> |
|
|
</div> |
|
|
<div class="post-grid"> |
|
|
{% for post in posts %} |
|
|
<a href="{{ url_for('post_page', post_id=post['id']) }}" class="post-item"> |
|
|
{% if post['type'] == 'video' %} |
|
|
<video class="post-preview" preload="metadata" muted> |
|
|
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" type="video/mp4"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img class="post-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" alt="{{ post['title'] }}" onclick="openModal(this.src, event)"> |
|
|
{% endif %} |
|
|
<h2>{{ post['title'] }}</h2> |
|
|
<p>{{ post['description'] }}</p> |
|
|
<p>Загрузил: <a href="{{ url_for('user_profile', username=post['uploader']) }}" class="username-link">{{ post['uploader'] }}</a> | {{ post['upload_date'] }}</p> |
|
|
<p class="stats">Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p> |
|
|
</a> |
|
|
{% endfor %} |
|
|
</div> |
|
|
</div> |
|
|
<div class="modal" id="imageModal" onclick="closeModal(event)"> |
|
|
<img id="modalImage" src=""> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
document.getElementById('sidebar').classList.toggle('active'); |
|
|
} |
|
|
|
|
|
function openModal(src, event) { |
|
|
event.preventDefault(); |
|
|
const modal = document.getElementById('imageModal'); |
|
|
const modalImg = document.getElementById('modalImage'); |
|
|
modal.style.display = 'flex'; |
|
|
modalImg.src = src; |
|
|
modalImg.style.transform = 'scale(1)'; |
|
|
} |
|
|
|
|
|
function closeModal(event) { |
|
|
if (event.target.tagName !== 'IMG') { |
|
|
const modal = document.getElementById('imageModal'); |
|
|
modal.style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
const videos = document.querySelectorAll('.post-preview'); |
|
|
videos.forEach(video => { |
|
|
if (video.tagName === 'VIDEO') { |
|
|
video.addEventListener('loadedmetadata', function() { |
|
|
const duration = video.duration; |
|
|
const randomTime = Math.random() * duration; |
|
|
video.currentTime = randomTime; |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
const modalImg = document.getElementById('modalImage'); |
|
|
let scale = 1; |
|
|
let startDistance = 0; |
|
|
let lastTap = 0; |
|
|
|
|
|
modalImg.addEventListener('dblclick', function() { |
|
|
scale = scale === 1 ? 2 : 1; |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
}); |
|
|
|
|
|
modalImg.addEventListener('touchstart', function(e) { |
|
|
if (e.touches.length === 2) { |
|
|
startDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
} else if (e.touches.length === 1) { |
|
|
const now = new Date().getTime(); |
|
|
if (now - lastTap < 300) { |
|
|
scale = scale === 1 ? 2 : 1; |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
} |
|
|
lastTap = now; |
|
|
} |
|
|
}); |
|
|
|
|
|
modalImg.addEventListener('touchmove', function(e) { |
|
|
if (e.touches.length === 2) { |
|
|
e.preventDefault(); |
|
|
const currentDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
scale = Math.max(1, Math.min(3, scale * (currentDistance / startDistance))); |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
startDistance = currentDistance; |
|
|
} |
|
|
}); |
|
|
|
|
|
function getDistance(touch1, touch2) { |
|
|
const dx = touch1.pageX - touch2.pageX; |
|
|
const dy = touch1.pageY - touch2.pageY; |
|
|
return Math.sqrt(dx * dx + dy * dy); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(html, posts=posts, is_authenticated=is_authenticated, username=username, repo_id=REPO_ID, search_query=search_query) |
|
|
|
|
|
|
|
|
@app.route('/post/<post_id>', methods=['GET', 'POST']) |
|
|
def post_page(post_id): |
|
|
data = load_data() |
|
|
post = next((p for p in data['posts'] if p['id'] == post_id), None) |
|
|
if not post: |
|
|
return "Публикация не найдена", 404 |
|
|
|
|
|
is_authenticated = 'username' in session |
|
|
username = session.get('username', None) |
|
|
|
|
|
|
|
|
post['views'] = post.get('views', 0) + 1 |
|
|
save_data(data) |
|
|
|
|
|
if request.method == 'POST' and is_authenticated: |
|
|
if 'like' in request.form: |
|
|
if username not in post.get('likes', []): |
|
|
post['likes'] = post.get('likes', []) + [username] |
|
|
else: |
|
|
post['likes'] = [user for user in post.get('likes', []) if user != username] |
|
|
save_data(data) |
|
|
elif 'comment' in request.form: |
|
|
comment_text = request.form.get('comment') |
|
|
if comment_text: |
|
|
post['comments'] = post.get('comments', []) + [{'user': username, 'text': comment_text, 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] |
|
|
save_data(data) |
|
|
|
|
|
html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ру"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>{{ post['title'] }}</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); margin: 0; padding: 0; min-height: 100vh; } |
|
|
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; } |
|
|
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } |
|
|
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; } |
|
|
.nav-links { display: flex; flex-direction: column; gap: 10px; } |
|
|
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: #3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; } |
|
|
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); } |
|
|
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; } |
|
|
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; } |
|
|
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } |
|
|
.container { max-width: 800px; margin: 20px auto 20px 270px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 20px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: margin-left 0.3s ease; } |
|
|
video, img { width: 100%; border-radius: 8px; max-height: 400px; object-fit: cover; cursor: pointer; } |
|
|
.btn { display: inline-block; margin: 10px 0; padding: 12px 20px; background: rgba(59, 130, 246, 0.9); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; transition: all 0.3s ease; } |
|
|
.btn:hover { background: rgba(59, 130, 246, 1); transform: scale(1.05); } |
|
|
.like-btn.liked { background: rgba(239, 68, 68, 0.9); } |
|
|
.like-btn.liked:hover { background: rgba(239, 68, 68, 1); } |
|
|
textarea { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #e2e8f0; border-radius: 8px; resize: vertical; background: rgba(255, 255, 255, 0.8); } |
|
|
.comment { border-top: 1px solid #e2e8f0; padding: 10px 0; } |
|
|
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; } |
|
|
.modal img { max-width: 90%; max-height: 90%; object-fit: contain; transform: scale(1); transition: transform 0.2s ease; } |
|
|
.username-link { color: #3b82f6; text-decoration: none; font-weight: 600; } |
|
|
.username-link:hover { text-decoration: underline; } |
|
|
@media (max-width: 768px) { |
|
|
.sidebar { transform: translateX(-100%); width: 200px; } |
|
|
.sidebar.active { transform: translateX(0); } |
|
|
.menu-btn { display: block; } |
|
|
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); } |
|
|
video, img { max-height: 250px; } |
|
|
.btn, textarea { width: 100%; font-size: 14px; padding: 10px; margin: 5px 0; } |
|
|
h1 { font-size: 1.5em; } |
|
|
h3 { font-size: 1.2em; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container"> |
|
|
<h1>{{ post['title'] }}</h1> |
|
|
{% if post['type'] == 'video' %} |
|
|
<video controls> |
|
|
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" type="video/mp4"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" alt="{{ post['title'] }}" onclick="openModal(this.src)"> |
|
|
{% endif %} |
|
|
<p>{{ post['description'] }}</p> |
|
|
<p>Загрузил: <a href="{{ url_for('user_profile', username=post['uploader']) }}" class="username-link">{{ post['uploader'] }}</a> | {{ post['upload_date'] }}</p> |
|
|
<p>Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p> |
|
|
{% if is_authenticated %} |
|
|
<form method="POST" style="display: inline;"> |
|
|
<button type="submit" name="like" class="btn like-btn {% if username in post['likes'] %}liked{% endif %}"> |
|
|
{% if username in post['likes'] %}Убрать лайк{% else %}Лайк{% endif %} |
|
|
</button> |
|
|
</form> |
|
|
<form method="POST"> |
|
|
<textarea name="comment" placeholder="Оставьте комментарий" rows="3"></textarea> |
|
|
<button type="submit" class="btn">Отправить</button> |
|
|
</form> |
|
|
{% endif %} |
|
|
<h3>Комментарии</h3> |
|
|
{% for comment in post.get('comments', []) %} |
|
|
<div class="comment"> |
|
|
<p><strong><a href="{{ url_for('user_profile', username=comment['user']) }}" class="username-link">{{ comment['user'] }}</a></strong> ({{ comment['date'] }}): {{ comment['text'] }}</p> |
|
|
</div> |
|
|
{% endfor %} |
|
|
<a href="{{ url_for('feed') }}" class="btn">Назад к ленте</a> |
|
|
{% if not is_authenticated %} |
|
|
<p><a href="{{ url_for('login') }}">Войдите</a>, чтобы ставить лайки и комментировать.</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
<div class="modal" id="imageModal" onclick="closeModal(event)"> |
|
|
<img id="modalImage" src=""> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
document.getElementById('sidebar').classList.toggle('active'); |
|
|
} |
|
|
|
|
|
function openModal(src) { |
|
|
const modal = document.getElementById('imageModal'); |
|
|
const modalImg = document.getElementById('modalImage'); |
|
|
modal.style.display = 'flex'; |
|
|
modalImg.src = src; |
|
|
modalImg.style.transform = 'scale(1)'; |
|
|
} |
|
|
|
|
|
function closeModal(event) { |
|
|
if (event.target.tagName !== 'IMG') { |
|
|
const modal = document.getElementById('imageModal'); |
|
|
modal.style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
const modalImg = document.getElementById('modalImage'); |
|
|
let scale = 1; |
|
|
let startDistance = 0; |
|
|
let lastTap = 0; |
|
|
|
|
|
modalImg.addEventListener('dblclick', function() { |
|
|
scale = scale === 1 ? 2 : 1; |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
}); |
|
|
|
|
|
modalImg.addEventListener('touchstart', function(e) { |
|
|
if (e.touches.length === 2) { |
|
|
startDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
} else if (e.touches.length === 1) { |
|
|
const now = new Date().getTime(); |
|
|
if (now - lastTap < 300) { |
|
|
scale = scale === 1 ? 2 : 1; |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
} |
|
|
lastTap = now; |
|
|
} |
|
|
}); |
|
|
|
|
|
modalImg.addEventListener('touchmove', function(e) { |
|
|
if (e.touches.length === 2) { |
|
|
e.preventDefault(); |
|
|
const currentDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
scale = Math.max(1, Math.min(3, scale * (currentDistance / startDistance))); |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
startDistance = currentDistance; |
|
|
} |
|
|
}); |
|
|
|
|
|
function getDistance(touch1, touch2) { |
|
|
const dx = touch1.pageX - touch2.pageX; |
|
|
const dy = touch1.pageY - touch2.pageY; |
|
|
return Math.sqrt(dx * dx + dy * dy); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(html, post=post, repo_id=REPO_ID, is_authenticated=is_authenticated, username=username) |
|
|
|
|
|
|
|
|
@app.route('/profile', methods=['GET', 'POST']) |
|
|
def profile(): |
|
|
if 'username' not in session: |
|
|
flash('Войдите, чтобы просмотреть профиль!') |
|
|
return redirect(url_for('login')) |
|
|
|
|
|
data = load_data() |
|
|
username = session['username'] |
|
|
user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True) |
|
|
is_authenticated = 'username' in session |
|
|
|
|
|
|
|
|
total_views = sum(post.get('views', 0) for post in user_posts) |
|
|
total_likes = sum(len(post.get('likes', [])) for post in user_posts) |
|
|
|
|
|
|
|
|
user_data = data['users'].get(username, {}) |
|
|
bio = user_data.get('bio', '') |
|
|
link = user_data.get('link', '') |
|
|
avatar = user_data.get('avatar', None) |
|
|
|
|
|
if request.method == 'POST': |
|
|
if 'delete_post' in request.form: |
|
|
post_id = request.form.get('post_id') |
|
|
if post_id: |
|
|
data['posts'] = [p for p in data['posts'] if p['id'] != post_id or p['uploader'] != username] |
|
|
save_data(data) |
|
|
logging.info(f"Публикация {post_id} удалена пользователем {username}") |
|
|
return redirect(url_for('profile')) |
|
|
elif 'update_profile' in request.form: |
|
|
bio = request.form.get('bio', '') |
|
|
link = request.form.get('link', '') |
|
|
avatar_file = request.files.get('avatar') |
|
|
if avatar_file and avatar_file.filename: |
|
|
filename = secure_filename(avatar_file.filename) |
|
|
temp_path = os.path.join('uploads', filename) |
|
|
os.makedirs('uploads', exist_ok=True) |
|
|
avatar_file.save(temp_path) |
|
|
api = HfApi() |
|
|
avatar_path = f"avatars/{username}/{filename}" |
|
|
api.upload_file( |
|
|
path_or_fileobj=temp_path, |
|
|
path_in_repo=avatar_path, |
|
|
repo_id=REPO_ID, |
|
|
repo_type="dataset", |
|
|
token=HF_TOKEN_WRITE, |
|
|
commit_message=f"Добавлен аватар для {username}" |
|
|
) |
|
|
data['users'][username]['avatar'] = avatar_path |
|
|
if os.path.exists(temp_path): |
|
|
os.remove(temp_path) |
|
|
data['users'][username]['bio'] = bio |
|
|
data['users'][username]['link'] = link |
|
|
save_data(data) |
|
|
logging.info(f"Профиль {username} обновлен") |
|
|
return redirect(url_for('profile')) |
|
|
|
|
|
html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ру"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Профиль - {{ username }}</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); margin: 0; padding: 0; min-height: 100vh; } |
|
|
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; } |
|
|
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } |
|
|
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; } |
|
|
.nav-links { display: flex; flex-direction: column; gap: 10px; } |
|
|
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: #3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; } |
|
|
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); } |
|
|
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; } |
|
|
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; } |
|
|
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } |
|
|
.container { max-width: 800px; margin: 20px auto 20px 270px; padding: 20px; transition: margin-left 0.3s ease; } |
|
|
.post-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } |
|
|
.post-item { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: transform 0.3s ease; } |
|
|
.post-item:hover { transform: scale(1.02); } |
|
|
.post-preview { width: 100%; border-radius: 8px; height: 200px; object-fit: cover; cursor: pointer; } |
|
|
.btn { display: inline-block; margin: 10px 0; padding: 12px 20px; background: rgba(59, 130, 246, 0.9); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; transition: all 0.3s ease; } |
|
|
.btn:hover { background: rgba(59, 130, 246, 1); transform: scale(1.05); } |
|
|
.delete-btn { background: rgba(239, 68, 68, 0.9); } |
|
|
.delete-btn:hover { background: rgba(239, 68, 68, 1); } |
|
|
.stats { font-size: 0.9em; color: #666; margin-top: 5px; } |
|
|
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; } |
|
|
.modal img { max-width: 90%; max-height: 90%; object-fit: contain; transform: scale(1); transition: transform 0.2s ease; } |
|
|
.avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; } |
|
|
.profile-info { margin-bottom: 20px; } |
|
|
textarea { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #e2e8f0; border-radius: 8px; resize: vertical; background: rgba(255, 255, 255, 0.8); } |
|
|
input[type="text"] { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #e2e8f0; border-radius: 8px; box-sizing: border-box; background: rgba(255, 255, 255, 0.8); } |
|
|
.share-btn { background: rgba(16, 185, 129, 0.9); } |
|
|
.share-btn:hover { background: rgba(16, 185, 129, 1); } |
|
|
@media (max-width: 768px) { |
|
|
.sidebar { transform: translateX(-100%); width: 200px; } |
|
|
.sidebar.active { transform: translateX(0); } |
|
|
.menu-btn { display: block; } |
|
|
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); } |
|
|
.post-grid { grid-template-columns: 1fr; gap: 15px; } |
|
|
.post-item { padding: 10px; } |
|
|
.post-preview { height: 150px; } |
|
|
.btn { font-size: 14px; padding: 10px; } |
|
|
h1, h2 { font-size: 1.5em; } |
|
|
.avatar { width: 80px; height: 80px; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container"> |
|
|
<h1>Профиль: {{ username }}</h1> |
|
|
<div class="profile-info"> |
|
|
{% if avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Аватар" class="avatar"> |
|
|
{% endif %} |
|
|
<p>{{ bio }}</p> |
|
|
{% if link %} |
|
|
<p><a href="{{ link }}" target="_blank">{{ link }}</a></p> |
|
|
{% endif %} |
|
|
<p>Всего просмотров: {{ total_views }} | Всего лайков: {{ total_likes }}</p> |
|
|
<button class="btn share-btn" onclick="copyProfileLink()">Поделиться профилем</button> |
|
|
</div> |
|
|
<h2>Редактировать профиль</h2> |
|
|
<form method="POST" enctype="multipart/form-data"> |
|
|
<textarea name="bio" placeholder="Описание профиля" rows="3">{{ bio }}</textarea> |
|
|
<input type="text" name="link" placeholder="Ссылка" value="{{ link }}"> |
|
|
<input type="file" name="avatar" accept="image/*"> |
|
|
<button type="submit" name="update_profile" class="btn">Сохранить</button> |
|
|
</form> |
|
|
<a href="{{ url_for('upload') }}" class="btn">Добавить публикацию</a> |
|
|
<h2>Ваши публикации</h2> |
|
|
<div class="post-grid"> |
|
|
{% if user_posts %} |
|
|
{% for post in user_posts %} |
|
|
<div class="post-item"> |
|
|
<a href="{{ url_for('post_page', post_id=post['id']) }}"> |
|
|
{% if post['type'] == 'video' %} |
|
|
<video class="post-preview" preload="metadata" muted> |
|
|
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" type="video/mp4"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img class="post-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" alt="{{ post['title'] }}" onclick="openModal(this.src, event)"> |
|
|
{% endif %} |
|
|
<h3>{{ post['title'] }}</h3> |
|
|
</a> |
|
|
<p>{{ post['description'] }}</p> |
|
|
<p>{{ post['upload_date'] }}</p> |
|
|
<p class="stats">Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p> |
|
|
<form method="POST"> |
|
|
<input type="hidden" name="post_id" value="{{ post['id'] }}"> |
|
|
<button type="submit" name="delete_post" class="btn delete-btn">Удалить</button> |
|
|
</form> |
|
|
</div> |
|
|
{% endfor %} |
|
|
{% else %} |
|
|
<p>Вы пока не загрузили ни одной публикации.</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
<div class="modal" id="imageModal" onclick="closeModal(event)"> |
|
|
<img id="modalImage" src=""> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
document.getElementById('sidebar').classList.toggle('active'); |
|
|
} |
|
|
|
|
|
function openModal(src, event) { |
|
|
event.preventDefault(); |
|
|
const modal = document.getElementById('imageModal'); |
|
|
const modalImg = document.getElementById('modalImage'); |
|
|
modal.style.display = 'flex'; |
|
|
modalImg.src = src; |
|
|
modalImg.style.transform = 'scale(1)'; |
|
|
} |
|
|
|
|
|
function closeModal(event) { |
|
|
if (event.target.tagName !== 'IMG') { |
|
|
const modal = document.getElementById('imageModal'); |
|
|
modal.style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
function copyProfileLink() { |
|
|
const url = window.location.href; |
|
|
navigator.clipboard.writeText(url).then(() => { |
|
|
alert('Ссылка на профиль скопирована в буфер обмена!'); |
|
|
}); |
|
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
const videos = document.querySelectorAll('.post-preview'); |
|
|
videos.forEach(video => { |
|
|
if (video.tagName === 'VIDEO') { |
|
|
video.addEventListener('loadedmetadata', function() { |
|
|
const duration = video.duration; |
|
|
const randomTime = Math.random() * duration; |
|
|
video.currentTime = randomTime; |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
const modalImg = document.getElementById('modalImage'); |
|
|
let scale = 1; |
|
|
let startDistance = 0; |
|
|
let lastTap = 0; |
|
|
|
|
|
modalImg.addEventListener('dblclick', function() { |
|
|
scale = scale === 1 ? 2 : 1; |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
}); |
|
|
|
|
|
modalImg.addEventListener('touchstart', function(e) { |
|
|
if (e.touches.length === 2) { |
|
|
startDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
} else if (e.touches.length === 1) { |
|
|
const now = new Date().getTime(); |
|
|
if (now - lastTap < 300) { |
|
|
scale = scale === 1 ? 2 : 1; |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
} |
|
|
lastTap = now; |
|
|
} |
|
|
}); |
|
|
|
|
|
modalImg.addEventListener('touchmove', function(e) { |
|
|
if (e.touches.length === 2) { |
|
|
e.preventDefault(); |
|
|
const currentDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
scale = Math.max(1, Math.min(3, scale * (currentDistance / startDistance))); |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
startDistance = currentDistance; |
|
|
} |
|
|
}); |
|
|
|
|
|
function getDistance(touch1, touch2) { |
|
|
const dx = touch1.pageX - touch2.pageX; |
|
|
const dy = touch1.pageY - touch2.pageY; |
|
|
return Math.sqrt(dx * dx + dy * dy); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(html, username=username, user_posts=user_posts, total_views=total_views, total_likes=total_likes, bio=bio, link=link, avatar=avatar, repo_id=REPO_ID, is_authenticated=is_authenticated) |
|
|
|
|
|
|
|
|
@app.route('/profile/<username>') |
|
|
def user_profile(username): |
|
|
data = load_data() |
|
|
if username not in data['users']: |
|
|
return "Пользователь не найден", 404 |
|
|
|
|
|
user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True) |
|
|
is_authenticated = 'username' in session |
|
|
current_user = session.get('username', None) |
|
|
|
|
|
|
|
|
total_views = sum(post.get('views', 0) for post in user_posts) |
|
|
total_likes = sum(len(post.get('likes', [])) for post in user_posts) |
|
|
|
|
|
|
|
|
user_data = data['users'].get(username, {}) |
|
|
bio = user_data.get('bio', '') |
|
|
link = user_data.get('link', '') |
|
|
avatar = user_data.get('avatar', None) |
|
|
|
|
|
html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ру"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Профиль - {{ username }}</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); margin: 0; padding: 0; min-height: 100vh; } |
|
|
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; } |
|
|
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } |
|
|
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; } |
|
|
.nav-links { display: flex; flex-direction: column; gap: 10px; } |
|
|
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: #3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; } |
|
|
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); } |
|
|
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; } |
|
|
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; } |
|
|
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } |
|
|
.container { max-width: 800px; margin: 20px auto 20px 270px; padding: 20px; transition: margin-left 0.3s ease; } |
|
|
.post-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } |
|
|
.post-item { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: transform 0.3s ease; } |
|
|
.post-item:hover { transform: scale(1.02); } |
|
|
.post-preview { width: 100%; border-radius: 8px; height: 200px; object-fit: cover; cursor: pointer; } |
|
|
.btn { display: inline-block; margin: 10px 0; padding: 12px 20px; background: rgba(59, 130, 246, 0.9); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; transition: all 0.3s ease; } |
|
|
.btn:hover { background: rgba(59, 130, 246, 1); transform: scale(1.05); } |
|
|
.stats { font-size: 0.9em; color: #666; margin-top: 5px; } |
|
|
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; } |
|
|
.modal img { max-width: 90%; max-height: 90%; object-fit: contain; transform: scale(1); transition: transform 0.2s ease; } |
|
|
.username-link { color: #3b82f6; text-decoration: none; font-weight: 600; } |
|
|
.username-link:hover { text-decoration: underline; } |
|
|
.avatar { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; } |
|
|
.profile-info { margin-bottom: 20px; } |
|
|
.share-btn { background: rgba(16, 185, 129, 0.9); } |
|
|
.share-btn:hover { background: rgba(16, 185, 129, 1); } |
|
|
@media (max-width: 768px) { |
|
|
.sidebar { transform: translateX(-100%); width: 200px; } |
|
|
.sidebar.active { transform: translateX(0); } |
|
|
.menu-btn { display: block; } |
|
|
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); } |
|
|
.post-grid { grid-template-columns: 1fr; gap: 15px; } |
|
|
.post-item { padding: 10px; } |
|
|
.post-preview { height: 150px; } |
|
|
.btn { font-size: 14px; padding: 10px; } |
|
|
h1, h2 { font-size: 1.5em; } |
|
|
.avatar { width: 80px; height: 80px; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container"> |
|
|
<h1>Профиль: {{ username }}</h1> |
|
|
<div class="profile-info"> |
|
|
{% if avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Аватар" class="avatar"> |
|
|
{% endif %} |
|
|
<p>{{ bio }}</p> |
|
|
{% if link %} |
|
|
<p><a href="{{ link }}" target="_blank">{{ link }}</a></p> |
|
|
{% endif %} |
|
|
<p>Всего просмотров: {{ total_views }} | Всего лайков: {{ total_likes }}</p> |
|
|
<button class="btn share-btn" onclick="copyProfileLink()">Поделиться профилем</button> |
|
|
</div> |
|
|
<h2>Публикации</h2> |
|
|
<div class="post-grid"> |
|
|
{% if user_posts %} |
|
|
{% for post in user_posts %} |
|
|
<div class="post-item"> |
|
|
<a href="{{ url_for('post_page', post_id=post['id']) }}"> |
|
|
{% if post['type'] == 'video' %} |
|
|
<video class="post-preview" preload="metadata" muted> |
|
|
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" type="video/mp4"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img class="post-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" alt="{{ post['title'] }}" onclick="openModal(this.src, event)"> |
|
|
{% endif %} |
|
|
<h3>{{ post['title'] }}</h3> |
|
|
</a> |
|
|
<p>{{ post['description'] }}</p> |
|
|
<p>{{ post['upload_date'] }}</p> |
|
|
<p class="stats">Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p> |
|
|
</div> |
|
|
{% endfor %} |
|
|
{% else %} |
|
|
<p>Этот пользователь пока не загрузил ни одной публикации.</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
<div class="modal" id="imageModal" onclick="closeModal(event)"> |
|
|
<img id="modalImage" src=""> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
document.getElementById('sidebar').classList.toggle('active'); |
|
|
} |
|
|
|
|
|
function openModal(src, event) { |
|
|
event.preventDefault(); |
|
|
const modal = document.getElementById('imageModal'); |
|
|
const modalImg = document.getElementById('modalImage'); |
|
|
modal.style.display = 'flex'; |
|
|
modalImg.src = src; |
|
|
modalImg.style.transform = 'scale(1)'; |
|
|
} |
|
|
|
|
|
function closeModal(event) { |
|
|
if (event.target.tagName !== 'IMG') { |
|
|
const modal = document.getElementById('imageModal'); |
|
|
modal.style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
function copyProfileLink() { |
|
|
const url = window.location.href; |
|
|
navigator.clipboard.writeText(url).then(() => { |
|
|
alert('Ссылка на профиль скопирована в буфер обмена!'); |
|
|
}); |
|
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
const videos = document.querySelectorAll('.post-preview'); |
|
|
videos.forEach(video => { |
|
|
if (video.tagName === 'VIDEO') { |
|
|
video.addEventListener('loadedmetadata', function() { |
|
|
const duration = video.duration; |
|
|
const randomTime = Math.random() * duration; |
|
|
video.currentTime = randomTime; |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
const modalImg = document.getElementById('modalImage'); |
|
|
let scale = 1; |
|
|
let startDistance = 0; |
|
|
let lastTap = 0; |
|
|
|
|
|
modalImg.addEventListener('dblclick', function() { |
|
|
scale = scale === 1 ? 2 : 1; |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
}); |
|
|
|
|
|
modalImg.addEventListener('touchstart', function(e) { |
|
|
if (e.touches.length === 2) { |
|
|
startDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
} else if (e.touches.length === 1) { |
|
|
const now = new Date().getTime(); |
|
|
if (now - lastTap < 300) { |
|
|
scale = scale === 1 ? 2 : 1; |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
} |
|
|
lastTap = now; |
|
|
} |
|
|
}); |
|
|
|
|
|
modalImg.addEventListener('touchmove', function(e) { |
|
|
if (e.touches.length === 2) { |
|
|
e.preventDefault(); |
|
|
const currentDistance = getDistance(e.touches[0], e.touches[1]); |
|
|
scale = Math.max(1, Math.min(3, scale * (currentDistance / startDistance))); |
|
|
modalImg.style.transform = `scale(${scale})`; |
|
|
startDistance = currentDistance; |
|
|
} |
|
|
}); |
|
|
|
|
|
function getDistance(touch1, touch2) { |
|
|
const dx = touch1.pageX - touch2.pageX; |
|
|
const dy = touch1.pageY - touch2.pageY; |
|
|
return Math.sqrt(dx * dx + dy * dy); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(html, username=username, user_posts=user_posts, total_views=total_views, total_likes=total_likes, bio=bio, link=link, avatar=avatar, repo_id=REPO_ID, is_authenticated=is_authenticated, current_user=current_user) |
|
|
|
|
|
|
|
|
@app.route('/upload', methods=['GET', 'POST']) |
|
|
def upload(): |
|
|
if 'username' not in session: |
|
|
flash('Войдите, чтобы загрузить контент!') |
|
|
return redirect(url_for('login')) |
|
|
|
|
|
if request.method == 'POST': |
|
|
title = request.form.get('title') |
|
|
description = request.form.get('description') |
|
|
file = request.files.get('file') |
|
|
uploader = session['username'] |
|
|
|
|
|
if not title or not file: |
|
|
return "Укажите название и выберите файл", 400 |
|
|
|
|
|
filename = secure_filename(file.filename) |
|
|
temp_path = os.path.join('uploads', filename) |
|
|
os.makedirs('uploads', exist_ok=True) |
|
|
file.save(temp_path) |
|
|
|
|
|
|
|
|
file_type = 'video' if filename.lower().endswith(('.mp4', '.mov', '.avi')) else 'photo' |
|
|
|
|
|
|
|
|
api = HfApi() |
|
|
api.upload_file( |
|
|
path_or_fileobj=temp_path, |
|
|
path_in_repo=f"{file_type}s/{filename}", |
|
|
repo_id=REPO_ID, |
|
|
repo_type="dataset", |
|
|
token=HF_TOKEN_WRITE, |
|
|
commit_message=f"Добавлена публикация: {title} пользователем {uploader}" |
|
|
) |
|
|
|
|
|
|
|
|
data = load_data() |
|
|
post_id = f"post_{len(data['posts']) + 1}" |
|
|
while any(p['id'] == post_id for p in data['posts']): |
|
|
post_id = f"post_{len(data['posts']) + 1}_{int(time.time())}" |
|
|
data['posts'].append({ |
|
|
'id': post_id, |
|
|
'title': title, |
|
|
'description': description, |
|
|
'filename': filename, |
|
|
'type': file_type, |
|
|
'uploader': uploader, |
|
|
'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), |
|
|
'views': 0, |
|
|
'likes': [], |
|
|
'comments': [] |
|
|
}) |
|
|
save_data(data) |
|
|
logging.info(f"Публикация {title} ({file_type}) загружена пользователем {uploader} с ID {post_id}") |
|
|
|
|
|
if os.path.exists(temp_path): |
|
|
os.remove(temp_path) |
|
|
|
|
|
return redirect(url_for('profile')) |
|
|
|
|
|
is_authenticated = 'username' in session |
|
|
username = session.get('username', None) |
|
|
html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ру"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Загрузка контента</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); margin: 0; padding: 0; min-height: 100vh; } |
|
|
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; } |
|
|
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } |
|
|
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; } |
|
|
.nav-links { display: flex; flex-direction: column; gap: 10px; } |
|
|
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: # 3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; } |
|
|
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); } |
|
|
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; } |
|
|
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; } |
|
|
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } |
|
|
.container { max-width: 600px; margin: 20px auto 20px 270px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 20px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: margin-left 0.3s ease; } |
|
|
input, textarea { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #e2e8f0; border-radius: 8px; box-sizing: border-box; background: rgba(255, 255, 255, 0.8); } |
|
|
button { padding: 12px; background: #3b82f6; color: white; border: none; border-radius: 8px; cursor: pointer; width: 100%; transition: background 0.3s ease; } |
|
|
button:hover { background: #2563eb; } |
|
|
#progress-container { margin-top: 10px; } |
|
|
#progress-bar { width: 0%; height: 20px; background: #3b82f6; border-radius: 8px; transition: width 0.3s ease; } |
|
|
@media (max-width: 768px) { |
|
|
.sidebar { transform: translateX(-100%); width: 200px; } |
|
|
.sidebar.active { transform: translateX(0); } |
|
|
.menu-btn { display: block; } |
|
|
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); } |
|
|
input, textarea, button { font-size: 14px; padding: 10px; margin: 5px 0; } |
|
|
h1 { font-size: 1.5em; } |
|
|
#progress-container { height: 15px; } |
|
|
#progress-bar { height: 15px; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container"> |
|
|
<h1>Загрузить контент</h1> |
|
|
<form id="upload-form" enctype="multipart/form-data"> |
|
|
<input type="text" name="title" placeholder="Название" required> |
|
|
<textarea name="description" placeholder="Описание" rows="4"></textarea> |
|
|
<input type="file" name="file" accept="video/*,image/*" required> |
|
|
<button type="submit">Загрузить</button> |
|
|
</form> |
|
|
<div id="progress-container"> |
|
|
<div id="progress-bar"></div> |
|
|
<span id="progress-text">0%</span> |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
document.getElementById('upload-form').onsubmit = async function(e) { |
|
|
e.preventDefault(); |
|
|
const formData = new FormData(this); |
|
|
const progressBar = document.getElementById('progress-bar'); |
|
|
const progressText = document.getElementById('progress-text'); |
|
|
progressBar.style.display = 'block'; |
|
|
|
|
|
const xhr = new XMLHttpRequest(); |
|
|
xhr.open('POST', '/upload', true); |
|
|
|
|
|
xhr.upload.onprogress = function(event) { |
|
|
if (event.lengthComputable) { |
|
|
const percent = Math.round((event.loaded / event.total) * 100); |
|
|
progressBar.style.width = percent + '%'; |
|
|
progressText.textContent = percent + '%'; |
|
|
} |
|
|
}; |
|
|
|
|
|
xhr.onload = function() { |
|
|
if (xhr.status === 200) { |
|
|
window.location = '/profile'; |
|
|
} else { |
|
|
alert('Ошибка загрузки'); |
|
|
} |
|
|
}; |
|
|
|
|
|
xhr.send(formData); |
|
|
}; |
|
|
function toggleSidebar() { |
|
|
document.getElementById('sidebar').classList.toggle('active'); |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(html, username=username, is_authenticated=is_authenticated) |
|
|
|
|
|
|
|
|
@app.route('/admhosto', methods=['GET', 'POST']) |
|
|
def admin_panel(): |
|
|
data = load_data() |
|
|
posts = sorted(data.get('posts', []), key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True) |
|
|
is_authenticated = 'username' in session |
|
|
username = session.get('username', None) |
|
|
|
|
|
search_query = request.form.get('search', '').strip().lower() if request.method == 'POST' and 'search' in request.form else request.args.get('search', '').strip().lower() |
|
|
|
|
|
if search_query: |
|
|
posts = [post for post in posts if search_query in post['title'].lower() or search_query in post['description'].lower()] |
|
|
|
|
|
if request.method == 'POST' and 'delete' in request.form: |
|
|
post_id = request.form.get('post_id') |
|
|
if post_id: |
|
|
data['posts'] = [p for p in data['posts'] if p['id'] != post_id] |
|
|
save_data(data) |
|
|
logging.info(f"Удален пост с ID {post_id} через админ-панель") |
|
|
return redirect(url_for('admin_panel')) |
|
|
|
|
|
html = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="ру"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Админ-панель</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
body { font-family: 'Poppins', sans-serif; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); margin: 0; padding: 0; min-height: 100vh; } |
|
|
.sidebar { position: fixed; left: 0; top: 0; width: 250px; height: 100%; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(10px); padding: 20px; box-shadow: 2px 0 15px rgba(0,0,0,0.1); transition: transform 0.3s ease; z-index: 1000; } |
|
|
.sidebar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } |
|
|
.nav-brand { font-size: 1.5em; font-weight: 600; color: #3b82f6; } |
|
|
.nav-links { display: flex; flex-direction: column; gap: 10px; } |
|
|
.nav-link { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(59, 130, 246, 0.1); color: #3b82f6; text-decoration: none; border-radius: 8px; transition: all 0.3s ease; } |
|
|
.nav-link:hover { background: rgba(59, 130, 246, 0.3); color: #2563eb; transform: translateX(5px); } |
|
|
.logout-btn { background: rgba(239, 68, 68, 0.1); color: #ef4444; } |
|
|
.logout-btn:hover { background: rgba(239, 68, 68, 0.3); color: #dc2626; } |
|
|
.menu-btn { display: none; font-size: 28px; background: rgba(255, 255, 255, 0.9); border: none; color: #3b82f6; cursor: pointer; position: fixed; top: 15px; left: 15px; z-index: 1001; padding: 5px 10px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } |
|
|
.container { max-width: 1200px; margin: 20px auto 20px 270px; padding: 20px; transition: margin-left 0.3s ease; } |
|
|
.post-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } |
|
|
.post-item { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(5px); padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); transition: transform 0.3s ease; } |
|
|
.post-item:hover { transform: scale(1.02); } |
|
|
.post-preview { width: 100%; border-radius: 8px; height: 200px; object-fit: cover; } |
|
|
.delete-btn { padding: 10px; background: #ef4444; color: white; border: none; border-radius: 8px; cursor: pointer; width: 100%; transition: background 0.3s ease; } |
|
|
.delete-btn:hover { background: #dc2626; } |
|
|
.search-container { margin-bottom: 20px; } |
|
|
.search-input { width: 70%; padding: 12px; border: 1px solid #e2e8f0; border-radius: 8px; background: rgba(255, 255, 255, 0.8); } |
|
|
.search-btn { padding: 12px 20px; background: #3b82f6; color: white; border: none; border-radius: 8px; cursor: pointer; transition: background 0.3s ease; } |
|
|
.search-btn:hover { background: #2563eb; } |
|
|
.stats { font-size: 0.9em; color: #666; margin-top: 5px; } |
|
|
.username-link { color: #3b82f6; text-decoration: none; font-weight: 600; } |
|
|
.username-link:hover { text-decoration: underline; } |
|
|
@media (max-width: 768px) { |
|
|
.sidebar { transform: translateX(-100%); width: 200px; } |
|
|
.sidebar.active { transform: translateX(0); } |
|
|
.menu-btn { display: block; } |
|
|
.container { margin: 60px 10px 20px 10px; max-width: calc(100% - 20px); } |
|
|
.post-grid { grid-template-columns: 1fr; gap: 15px; } |
|
|
.post-item { padding: 10px; } |
|
|
.post-preview { height: 150px; } |
|
|
.delete-btn, .search-input { padding: 10px; font-size: 14px; } |
|
|
.search-btn { padding: 10px 15px; font-size: 14px; } |
|
|
h1 { font-size: 1.5em; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container"> |
|
|
<h1>Админ-панель: Все посты</h1> |
|
|
<div class="search-container"> |
|
|
<form method="POST"> |
|
|
<input type="text" name="search" class="search-input" placeholder="Поиск по названию или описанию" value="{{ search_query }}"> |
|
|
<button type="submit" class="search-btn">Искать</button> |
|
|
</form> |
|
|
</div> |
|
|
<div class="post-grid"> |
|
|
{% if posts %} |
|
|
{% for post in posts %} |
|
|
<div class="post-item"> |
|
|
<a href="{{ url_for('post_page', post_id=post['id']) }}"> |
|
|
{% if post['type'] == 'video' %} |
|
|
<video class="post-preview" preload="metadata" muted> |
|
|
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" type="video/mp4"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img class="post-preview" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" alt="{{ post['title'] }}"> |
|
|
{% endif %} |
|
|
<h2>{{ post['title'] }}</h2> |
|
|
</a> |
|
|
<p>{{ post['description'] }}</p> |
|
|
<p>Загрузил: <a href="{{ url_for('user_profile', username=post['uploader']) }}" class="username-link">{{ post['uploader'] }}</a> | {{ post['upload_date'] }}</p> |
|
|
<p class="stats">Просмотров: {{ post['views'] }} | Лайков: {{ post['likes']|length }}</p> |
|
|
<form method="POST"> |
|
|
<input type="hidden" name="post_id" value="{{ post['id'] }}"> |
|
|
<button type="submit" name="delete" class="delete-btn">Удалить</button> |
|
|
</form> |
|
|
</div> |
|
|
{% endfor %} |
|
|
{% else %} |
|
|
<p>Посты не найдены.</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
document.getElementById('sidebar').classList.toggle('active'); |
|
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
const videos = document.querySelectorAll('.post-preview'); |
|
|
videos.forEach(video => { |
|
|
if (video.tagName === 'VIDEO') { |
|
|
video.addEventListener('loadedmetadata', function() { |
|
|
const duration = video.duration; |
|
|
const randomTime = Math.random() * duration; |
|
|
video.currentTime = randomTime; |
|
|
}); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(html, posts=posts, is_authenticated=is_authenticated, username=username, repo_id=REPO_ID, search_query=search_query) |
|
|
|
|
|
if __name__ == '__main__': |
|
|
backup_thread = threading.Thread(target=periodic_backup, daemon=True) |
|
|
backup_thread.start() |
|
|
app.run(debug=True, host='0.0.0.0', port=7860) |