Host2 / app.py
Eluza133's picture
Update app.py
26f447d verified
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'
# Загрузка на Hugging Face
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)