Hubtest / app.py
Eluza133's picture
Update app.py
ae01937 verified
from flask import Flask, render_template_string, request, redirect, url_for, session, flash, jsonify
from flask_caching import Cache
import json
import os
import logging
import threading
import time
from datetime import datetime, timedelta
from huggingface_hub import HfApi, hf_hub_download
from werkzeug.utils import secure_filename
import random
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey")
DATA_FILE = 'data_adusis.json'
REPO_ID = "Eluza133/A12d12s12"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
cache = Cache(app, config={'CACHE_TYPE': 'simple'})
logging.basicConfig(level=logging.INFO)
@cache.memoize(timeout=300)
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': {}, 'general_chat': [], 'private_chats': {}}
data.setdefault('posts', [])
data.setdefault('users', {})
data.setdefault('general_chat', [])
data.setdefault('private_chats', {})
for user in data['users']:
data['users'][user].setdefault('last_chat_visit', '1970-01-01 00:00:00')
data['users'][user].setdefault('last_private_visit', '1970-01-01 00:00:00')
data['users'][user].setdefault('last_seen', '1970-01-01 00:00:00')
logging.info("Данные успешно загружены")
return data
except Exception as e:
logging.error(f"Ошибка при загрузке данных: {e}")
return {'posts': [], 'users': {}, 'general_chat': [], 'private_chats': {}}
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()
cache.clear()
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"Бэкап {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': {}, 'general_chat': [], 'private_chats': {}}, f)
def periodic_backup():
while True:
upload_db_to_hf()
time.sleep(1800)
def get_unread_count(data, username):
if username not in data['users']:
return 0
last_visit = datetime.strptime(data['users'][username]['last_chat_visit'], '%Y-%m-%d %H:%M:%S')
return sum(1 for msg in data['general_chat'] if datetime.strptime(msg['time'], '%Y-%m-%d %H:%M:%S') > last_visit)
def get_private_unread_count(data, username):
if username not in data['users']:
return 0
last_visit = datetime.strptime(data['users'][username]['last_private_visit'], '%Y-%m-%d %H:%M:%S')
unread = 0
for chat_key, messages in data['private_chats'].items():
users = chat_key.split('_')
if username in users:
unread += sum(1 for msg in messages if datetime.strptime(msg['time'], '%Y-%m-%d %H:%M:%S') > last_visit and msg['sender'] != username)
return unread
def is_user_online(data, username):
if username not in data['users']:
return False
last_seen = datetime.strptime(data['users'][username]['last_seen'], '%Y-%m-%d %H:%M:%S')
return (datetime.now() - last_seen).total_seconds() < 300
def update_last_seen(data, username):
if username in data['users']:
data['users'][username]['last_seen'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
save_data(data)
BASE_STYLE = '''
:root {
--primary: #ff4d6d;
--secondary: #00ddeb;
--accent: #8b5cf6;
--background-light: #f7f9fc;
--background-dark: #1e1b2e;
--card-bg: rgba(255, 255, 255, 0.97);
--card-bg-dark: rgba(40, 35, 60, 0.97);
--text-light: #2a1e5a;
--text-dark: #e8e1ff;
--shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
--glass-bg: rgba(255, 255, 255, 0.2);
--transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
--online: #34c759;
--offline: #ff3b30;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', sans-serif;
background: var(--background-light);
color: var(--text-light);
line-height: 1.7;
overflow-x: hidden;
}
body.dark {
background: var(--background-dark);
color: var(--text-dark);
}
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 300px;
height: 100%;
background: var(--glass-bg);
backdrop-filter: blur(15px);
padding: 30px;
box-shadow: var(--shadow);
z-index: 1000;
transition: transform var(--transition);
}
.sidebar.hidden {
transform: translateX(-100%);
}
.sidebar-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 40px;
}
.nav-brand {
font-size: 2em;
font-weight: 900;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
.logo {
width: 45px;
height: 45px;
border-radius: 16px;
box-shadow: var(--shadow);
transition: transform var(--transition);
}
.logo:hover {
transform: scale(1.1);
}
.nav-links {
display: flex;
flex-direction: column;
gap: 15px;
}
.nav-link {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 25px;
background: var(--card-bg);
color: var(--text-light);
text-decoration: none;
border-radius: 16px;
font-size: 1.1em;
font-weight: 600;
transition: var(--transition);
position: relative;
}
body.dark .nav-link {
background: var(--card-bg-dark);
color: var(--text-dark);
}
.nav-link:hover {
transform: translateX(8px);
background: var(--primary);
color: white;
box-shadow: 0 8px 25px rgba(255, 77, 109, 0.5);
}
.nav-link .badge {
position: absolute;
right: 15px;
background: var(--secondary);
color: white;
padding: 5px 12px;
border-radius: 14px;
font-size: 0.85em;
font-weight: 700;
}
.logout-btn {
background: var(--secondary);
color: white;
}
.logout-btn:hover {
background: #00b8c5;
}
.menu-btn {
display: none;
font-size: 30px;
background: var(--glass-bg);
border: none;
color: var(--primary);
cursor: pointer;
position: fixed;
top: 20px;
left: 20px;
z-index: 1001;
padding: 12px;
border-radius: 50%;
box-shadow: var(--shadow);
transition: var(--transition);
}
.menu-btn:hover {
background: var(--primary);
color: white;
}
.container {
margin: 20px auto 20px 320px;
max-width: 1200px;
padding: 30px;
transition: var(--transition);
}
.btn {
padding: 15px 30px;
background: var(--primary);
color: white;
border: none;
border-radius: 16px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: 10px;
box-shadow: var(--shadow);
}
.btn:hover {
transform: scale(1.05);
background: #e6415f;
box-shadow: 0 10px 30px rgba(255, 77, 109, 0.6);
}
input, textarea, select {
width: 100%;
padding: 15px;
margin: 12px 0;
border: none;
border-radius: 16px;
background: var(--glass-bg);
color: var(--text-light);
font-size: 1.1em;
transition: var(--transition);
box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.1);
}
body.dark input, body.dark textarea, body.dark select {
color: var(--text-dark);
}
input:focus, textarea:focus, select:focus {
outline: none;
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 0 0 4px var(--primary);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: 2000;
justify-content: center;
align-items: center;
transition: opacity var(--transition);
}
.modal img, .modal video {
max-width: 95%;
max-height: 95%;
object-fit: contain;
border-radius: 20px;
box-shadow: var(--shadow);
animation: zoomIn 0.4s ease;
}
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
background: var(--glass-bg);
border: none;
padding: 12px;
border-radius: 50%;
cursor: pointer;
font-size: 26px;
box-shadow: var(--shadow);
transition: var(--transition);
}
.theme-toggle:hover {
transform: rotate(90deg);
background: var(--accent);
color: white;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-left: 8px;
}
.online { background: var(--online); }
.offline { background: var(--offline); }
@keyframes zoomIn {
from { opacity: 0; transform: scale(0.85); }
to { opacity: 1; transform: scale(1); }
}
@media (max-width: 900px) {
.sidebar {
width: 100%;
max-width: 300px;
transform: translateX(-100%);
}
.sidebar.active {
transform: translateX(0);
}
.menu-btn {
display: block;
}
.container {
margin: 80px 20px 20px 20px;
max-width: calc(100% - 40px);
}
.theme-toggle {
top: 80px;
}
}
@media (max-width: 480px) {
.nav-brand { font-size: 1.6em; }
.nav-link { font-size: 1em; padding: 12px 20px; }
.btn { padding: 12px 20px; font-size: 1em; }
.logo { width: 40px; height: 40px; }
}
'''
NAV_HTML = '''
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="https://cdn-avatars.huggingface.co/v1/production/uploads/673b00f35373479538ac373c/W_dumUND8K6IlMxVmpUgS.jpeg" alt="Logo" class="logo">
<span class="nav-brand">ADUSIS QoSHUB</span>
</div>
<nav class="nav-links">
<a href="{{ url_for('feed') }}" class="nav-link"><span>📜</span> Feed</a>
{% if is_authenticated %}
<a href="{{ url_for('profile') }}" class="nav-link"><span>👤</span> Profile ({{ username }}) <span class="status-dot {{ 'online' if is_online else 'offline' }}"></span></a>
<a href="{{ url_for('upload') }}" class="nav-link"><span>⬆️</span> Upload</a>
<a href="{{ url_for('chat') }}" class="nav-link"><span>💬</span> Chat {% if unread_count > 0 %}<span class="badge">{{ unread_count }}</span>{% endif %}</a>
<a href="{{ url_for('users') }}" class="nav-link"><span>👥</span> Users <span class="badge">{{ user_count }}</span></a>
<a href="{{ url_for('messages') }}" class="nav-link"><span>✉️</span> Messages {% if private_unread_count > 0 %}<span class="badge">{{ private_unread_count }}</span>{% endif %}</a>
<a href="{{ url_for('logout') }}" class="nav-link logout-btn"><span>🚪</span> Logout</a>
{% else %}
<a href="{{ url_for('login') }}" class="nav-link"><span>🔑</span> Login</a>
<a href="{{ url_for('register') }}" class="nav-link"><span>✨</span> Register</a>
<a href="{{ url_for('chat') }}" class="nav-link"><span>💬</span> Chat (View Only)</a>
<a href="{{ url_for('users') }}" class="nav-link"><span>👥</span> Users <span class="badge">{{ user_count }}</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,
'last_chat_visit': '1970-01-01 00:00:00',
'last_private_visit': '1970-01-01 00:00:00',
'last_seen': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
save_data(data)
flash('Регистрация успешна! Пожалуйста, войдите.')
return redirect(url_for('login'))
is_authenticated = 'username' in session
username = session.get('username', None)
data = load_data()
unread_count = get_unread_count(data, username) if is_authenticated else 0
private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0
user_count = len(data['users'])
is_online = is_user_online(data, username) if is_authenticated else False
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<meta name="google-site-verification" content="V6EqDTA9Oj9V1OfNbnBHj5RKrdcXlABD8tqfEFVUHJY" />
<title>Register - Content Hub</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 500px;
background: var(--card-bg);
padding: 40px;
border-radius: 20px;
box-shadow: var(--shadow);
animation: slideUp 0.4s ease;
}
body.dark .container {
background: var(--card-bg-dark);
}
h1 {
font-size: 2.2em;
font-weight: 800;
text-align: center;
margin-bottom: 30px;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
.flash {
color: var(--secondary);
text-align: center;
margin-bottom: 15px;
font-size: 1.1em;
font-weight: 600;
}
.link {
text-align: center;
margin-top: 20px;
color: var(--primary);
font-size: 1.1em;
text-decoration: none;
font-weight: 600;
transition: var(--transition);
}
.link:hover {
color: var(--accent);
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<h1>Register</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="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit" class="btn">Register</button>
</form>
<p class="link"><a href="{{ url_for('login') }}">Already have an account? Login</a></p>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
};
</script>
</body>
</html>
'''
return render_template_string(html, is_authenticated=is_authenticated, username=username, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online)
@app.route('/login', methods=['GET', 'POST'])
def login():
data = load_data()
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if username in data['users'] and data['users'][username]['password'] == password:
session['username'] = username
session.permanent = True
update_last_seen(data, username)
return redirect(url_for('feed'))
flash('Неверное имя пользователя или пароль!')
return redirect(url_for('login'))
is_authenticated = 'username' in session
username = session.get('username', None)
if is_authenticated:
update_last_seen(data, username)
unread_count = get_unread_count(data, username) if is_authenticated else 0
private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0
user_count = len(data['users'])
is_online = is_user_online(data, username) if is_authenticated else False
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<title>Login - Content Hub</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 500px;
background: var(--card-bg);
padding: 40px;
border-radius: 20px;
box-shadow: var(--shadow);
animation: slideUp 0.4s ease;
}
body.dark .container {
background: var(--card-bg-dark);
}
h1 {
font-size: 2.2em;
font-weight: 800;
text-align: center;
margin-bottom: 30px;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
.flash {
color: var(--secondary);
text-align: center;
margin-bottom: 15px;
font-size: 1.1em;
font-weight: 600;
}
.link {
text-align: center;
margin-top: 20px;
color: var(--primary);
font-size: 1.1em;
text-decoration: none;
font-weight: 600;
transition: var(--transition);
}
.link:hover {
color: var(--accent);
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<h1>Login</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="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit" class="btn">Login</button>
</form>
<p class="link"><a href="{{ url_for('register') }}">Don't have an account? Register</a></p>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
};
</script>
</body>
</html>
'''
return render_template_string(html, is_authenticated=is_authenticated, username=username, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online)
@app.route('/logout')
def logout():
data = load_data()
username = session.get('username', None)
if username:
update_last_seen(data, username)
session.pop('username', None)
return redirect(url_for('feed'))
@app.route('/toggle_like/<post_id>', methods=['POST'])
def toggle_like(post_id):
if 'username' not in session:
return jsonify({'error': 'Not authenticated'}), 401
data = load_data()
username = session['username']
post = next((p for p in data['posts'] if p['id'] == post_id), None)
if not post:
return jsonify({'error': 'Post not found'}), 404
likes = post.get('likes', [])
if username in likes:
post['likes'] = [u for u in likes if u != username]
liked = False
else:
post['likes'] = likes + [username]
liked = True
save_data(data)
return jsonify({'liked': liked, 'likes_count': len(post['likes'])})
@app.route('/increment_view/<post_id>', methods=['POST'])
def increment_view(post_id):
data = load_data()
post = next((p for p in data['posts'] if p['id'] == post_id), None)
if post:
post['views'] = post.get('views', 0) + 1
save_data(data)
return jsonify({'views': post['views']})
return jsonify({'error': 'Post not found'}), 404
@app.route('/', methods=['GET', 'POST'])
def feed():
data = load_data()
username = session.get('username', None)
if username:
update_last_seen(data, username)
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
unread_count = get_unread_count(data, username) if is_authenticated else 0
private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0
user_count = len(data['users'])
is_online = is_user_online(data, username) if is_authenticated else False
if request.method == 'POST' and is_authenticated:
post_id = request.form.get('post_id')
post = next((p for p in data['posts'] if p['id'] == post_id), None)
if not post:
return "Пост не найден", 404
if '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)
return redirect(url_for('feed'))
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<meta name="google-site-verification" content="V6EqDTA9Oj9V1OfNbnBHj5RKrdcXlABD8tqfEFVUHJY" />
<title>Adusis - QoS, BBC, BNWO HUB</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
body, html {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.feed-container {
height: 100vh;
width: 100%;
overflow-y: scroll;
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
margin-left: 0;
padding: 0;
-webkit-overflow-scrolling: touch;
}
.post-container {
height: 100vh;
width: 100%;
position: relative;
scroll-snap-align: center;
display: flex;
align-items: center;
justify-content: center;
background: var(--background-light);
overflow: hidden;
transition: background var(--transition);
}
body.dark .post-container {
background: var(--background-dark);
}
.post-media {
max-height: 100vh;
max-width: 100%;
object-fit: contain;
border-radius: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
transition: filter var(--transition);
loading: lazy;
}
.post-media:hover {
filter: brightness(1.1);
}
.play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
border: none;
border-radius: 50%;
width: 80px;
height: 80px;
font-size: 40px;
color: var(--primary);
cursor: pointer;
z-index: 2;
transition: var(--transition);
}
.play-btn:hover {
background: var(--primary);
color: white;
transform: translate(-50%, -50%) scale(1.1);
}
.post-overlay {
position: absolute;
bottom: 90px;
left: 25px;
right: 25px;
z-index: 2;
color: white;
background: linear-gradient(to top, rgba(0,0,0,0.85), transparent);
padding: 25px;
border-radius: 16px;
pointer-events: none;
}
.post-overlay h2 {
font-size: 1.6em;
font-weight: 800;
margin-bottom: 12px;
text-shadow: 0 4px 8px rgba(0,0,0,0.9);
}
.post-overlay p {
font-size: 1.1em;
text-shadow: 0 4px 8px rgba(0,0,0,0.9);
}
.username-link {
color: var(--primary);
font-weight: 700;
text-decoration: none;
pointer-events: all;
transition: var(--transition);
}
.username-link:hover {
color: var(--accent);
}
.post-actions {
position: absolute;
right: 25px;
bottom: 100px;
display: flex;
flex-direction: column;
gap: 25px;
z-index: 3;
}
.action-btn {
background: rgba(255, 255, 255, 0.3);
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
color: white;
font-size: 28px;
backdrop-filter: blur(12px);
box-shadow: var(--shadow);
}
.action-btn:hover {
background: var(--primary);
transform: scale(1.15);
}
.action-btn.liked {
color: var(--primary);
background: rgba(255, 77, 109, 0.6);
}
.action-count {
color: white;
text-align: center;
font-size: 16px;
margin-top: 8px;
text-shadow: 0 4px 8px rgba(0,0,0,0.9);
}
.comment-section {
display: none;
position: absolute;
bottom: 180px;
left: 25px;
right: 25px;
background: rgba(0, 0, 0, 0.9);
padding: 20px;
border-radius: 16px;
max-height: 60vh;
overflow-y: auto;
z-index: 4;
box-shadow: var(--shadow);
}
.comment-section.active {
display: block;
}
.comment {
margin-bottom: 12px;
font-size: 14px;
color: white;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
}
.comment-form {
display: flex;
gap: 12px;
margin-top: 12px;
}
.comment-form textarea {
flex-grow: 1;
height: 45px;
background: rgba(255, 255, 255, 0.3);
color: white;
border: none;
border-radius: 12px;
padding: 12px;
font-size: 14px;
resize: none;
}
.comment-form .btn {
padding: 12px 20px;
font-size: 14px;
}
@media (max-width: 900px) {
.feed-container {
padding: 0;
}
.post-overlay {
bottom: 70px;
left: 15px;
right: 15px;
padding: 20px;
}
.post-actions {
bottom: 80px;
right: 15px;
gap: 20px;
}
.action-btn {
width: 50px;
height: 50px;
font-size: 24px;
}
.action-count {
font-size: 14px;
}
.comment-section {
bottom: 150px;
left: 15px;
right: 15px;
}
}
@media (max-width: 480px) {
.post-overlay h2 {
font-size: 1.3em;
}
.post-overlay p {
font-size: 0.95em;
}
.play-btn {
width: 60px;
height: 60px;
font-size: 30px;
}
.action-btn {
width: 45px;
height: 45px;
font-size: 20px;
}
.comment-section {
bottom: 130px;
}
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="feed-container" id="feed">
{% for post in posts %}
<div class="post-container" id="post-{{ post['id'] }}">
{% if post['type'] == 'video' %}
<video class="post-media" preload="metadata" muted loading="lazy">
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" type="video/mp4">
</video>
<button class="play-btn" onclick="playVideo('{{ post['id'] }}')">▶</button>
{% else %}
<img class="post-media" src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" alt="{{ post['title'] }}" loading="lazy">
{% endif %}
<div class="post-overlay">
<h2>{{ post['title'] }}</h2>
<p>{{ post['description']|truncate(150) }}</p>
<p>By: <a href="{{ url_for('user_profile', username=post['uploader']) }}" class="username-link">{{ post['uploader'] }}</a> <span class="status-dot {{ 'online' if is_user_online(post['uploader']) else 'offline' }}"></span></p>
</div>
<div class="post-actions">
<button class="action-btn {% if username in post['likes'] %}liked{% endif %}" onclick="toggleLike('{{ post['id'] }}')">
</button>
<span class="action-count" id="likes-{{ post['id'] }}">{{ post['likes']|length }}</span>
<button class="action-btn" onclick="toggleComments('{{ post['id'] }}')">💬</button>
<span class="action-count">{{ post['comments']|length }}</span>
<button class="action-btn" onclick="copyLink('{{ url_for('post_page', post_id=post['id'], _external=True) }}')">➤</button>
<span class="action-count" id="views-{{ post['id'] }}">{{ post['views'] }}</span>
</div>
<div class="comment-section" id="comments-{{ post['id'] }}">
{% for comment in post.get('comments', []) %}
<div class="comment">
<strong><a href="{{ url_for('user_profile', username=comment['user']) }}" class="username-link">{{ comment['user'] }}</a>:</strong> {{ comment['text'] }}
</div>
{% endfor %}
{% if is_authenticated %}
<form method="POST" class="comment-form" onsubmit="return submitComment(event, '{{ post['id'] }}')">
<input type="hidden" name="post_id" value="{{ post['id'] }}">
<textarea name="comment" placeholder="Add a comment"></textarea>
<button type="submit" class="btn">Send</button>
</form>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function toggleComments(postId) {
const comments = document.getElementById('comments-' + postId);
comments.classList.toggle('active');
}
function toggleLike(postId) {
fetch('/toggle_like/' + postId, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => response.json())
.then(data => {
const btn = document.querySelector(`#post-${postId} .action-btn:first-child`);
const count = document.getElementById('likes-' + postId);
btn.classList.toggle('liked', data.liked);
count.textContent = data.likes_count;
})
.catch(error => console.error('Error:', error));
}
function submitComment(event, postId) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
fetch('/', {
method: 'POST',
body: formData
}).then(() => {
window.location.reload();
});
return false;
}
function copyLink(url) {
navigator.clipboard.writeText(url).then(() => {
alert('Link copied to clipboard!');
});
}
function playVideo(postId) {
const video = document.querySelector(`#post-${postId} .post-media`);
const playBtn = document.querySelector(`#post-${postId} .play-btn`);
if (video.paused) {
video.play();
playBtn.style.display = 'none';
} else {
video.pause();
playBtn.style.display = 'block';
}
video.onended = () => {
playBtn.style.display = 'block';
};
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const feed = document.getElementById('feed');
const medias = document.querySelectorAll('.post-media');
medias.forEach(media => {
const postId = media.parentElement.id.split('-')[1];
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting && media.tagName === 'VIDEO') {
media.pause();
const playBtn = document.querySelector(`#post-${postId} .play-btn`);
if (playBtn) playBtn.style.display = 'block';
}
if (entry.isIntersecting) {
fetch('/increment_view/' + postId, { method: 'POST' })
.then(response => response.json())
.then(data => {
document.getElementById('views-' + postId).textContent = data.views;
});
}
});
}, { threshold: 0.7 });
observer.observe(media);
});
let lastScrollTop = 0;
feed.addEventListener('scroll', () => {
const scrollTop = feed.scrollTop;
const scrollHeight = feed.scrollHeight;
const clientHeight = feed.clientHeight;
const posts = document.querySelectorAll('.post-container');
posts.forEach(post => {
const rect = post.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= clientHeight) {
post.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
lastScrollTop = scrollTop;
}, { passive: true });
};
</script>
</body>
</html>
'''
return render_template_string(html,
posts=posts,
is_authenticated=is_authenticated,
username=username,
repo_id=REPO_ID,
unread_count=unread_count,
user_count=user_count,
private_unread_count=private_unread_count,
is_online=is_online,
is_user_online=lambda u: is_user_online(data, u))
@app.route('/post/<post_id>', methods=['GET', 'POST'])
def post_page(post_id):
data = load_data()
username = session.get('username', None)
if username:
update_last_seen(data, username)
post = next((p for p in data['posts'] if p['id'] == post_id), None)
if not post:
return "Пост не найден", 404
is_authenticated = 'username' in session
unread_count = get_unread_count(data, username) if is_authenticated else 0
private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0
user_count = len(data['users'])
is_online = is_user_online(data, username) if is_authenticated else False
post['views'] = post.get('views', 0) + 1
save_data(data)
if request.method == 'POST' and is_authenticated:
if '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="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<title>{{ post['title'] }} - Content Hub</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 900px;
background: var(--card-bg);
padding: 35px;
border-radius: 20px;
box-shadow: var(--shadow);
}
body.dark .container {
background: var(--card-bg-dark);
}
h1 {
font-size: 2.4em;
font-weight: 800;
margin-bottom: 25px;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
video, img {
width: 100%;
max-height: 500px;
object-fit: cover;
border-radius: 16px;
box-shadow: var(--shadow);
margin-bottom: 25px;
transition: transform var(--transition);
loading: lazy;
}
video:hover, img:hover {
transform: scale(1.02);
}
p {
font-size: 1.1em;
margin-bottom: 15px;
}
.like-btn.liked {
background: var(--secondary);
}
.like-btn.liked:hover {
background: #00b8c5;
}
.comment-section {
margin-top: 30px;
}
.comment {
background: var(--glass-bg);
padding: 15px;
border-radius: 14px;
margin-bottom: 15px;
font-size: 1em;
transition: var(--transition);
}
.comment:hover {
transform: translateY(-3px);
}
.username-link {
color: var(--primary);
font-weight: 600;
text-decoration: none;
}
.username-link:hover {
color: var(--accent);
}
@media (max-width: 480px) {
.container {
padding: 20px;
}
video, img {
max-height: 350px;
}
h1 {
font-size: 1.9em;
}
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<h1>{{ post['title'] }}</h1>
{% if post['type'] == 'video' %}
<video controls preload="metadata" loading="lazy">
<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'] }}" loading="lazy" onclick="openModal(this.src)">
{% endif %}
<p>{{ post['description'] }}</p>
<p>By: <a href="{{ url_for('user_profile', username=post['uploader']) }}" class="username-link">{{ post['uploader'] }}</a> <span class="status-dot {{ 'online' if is_user_online(post['uploader']) else 'offline' }}"></span> | {{ post['upload_date'] }}</p>
<p>Views: <span id="views-{{ post['id'] }}">{{ post['views'] }}</span> | Likes: <span id="likes-{{ post['id'] }}">{{ post['likes']|length }}</span></p>
{% if is_authenticated %}
<button class="btn like-btn {% if username in post['likes'] %}liked{% endif %}" onclick="toggleLike('{{ post['id'] }}')">
{% if username in post['likes'] %}Unlike{% else %}Like{% endif %}
</button>
<button class="btn" onclick="copyLink('{{ url_for('post_page', post_id=post['id'], _external=True) }}')">Share</button>
<form method="POST" class="comment-section">
<textarea name="comment" placeholder="Leave a comment" rows="3"></textarea>
<button type="submit" class="btn">Submit</button>
</form>
{% endif %}
<h3 style="margin-top: 30px; font-size: 1.6em;">Comments</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> <span class="status-dot {{ 'online' if is_user_online(comment['user']) else 'offline' }}"></span></strong> ({{ comment['date'] }}): {{ comment['text'] }}</p>
</div>
{% endfor %}
<a href="{{ url_for('feed') }}" class="btn">Back to Feed</a>
{% if not is_authenticated %}
<p style="margin-top: 15px;"><a href="{{ url_for('login') }}">Login</a> to like and comment.</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 toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function openModal(src) {
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
modal.style.display = 'flex';
modalImg.src = src;
}
function closeModal(event) {
if (event.target.tagName !== 'IMG') {
document.getElementById('imageModal').style.display = 'none';
}
}
function toggleLike(postId) {
fetch('/toggle_like/' + postId, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => response.json())
.then(data => {
const btn = document.querySelector('.like-btn');
btn.classList.toggle('liked', data.liked);
btn.textContent = data.liked ? 'Unlike' : 'Like';
document.getElementById('likes-' + postId).textContent = data.likes_count;
})
.catch(error => console.error('Error:', error));
}
function copyLink(url) {
navigator.clipboard.writeText(url).then(() => {
alert('Link copied to clipboard!');
});
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
};
</script>
</body>
</html>
'''
return render_template_string(html, post=post, repo_id=REPO_ID, is_authenticated=is_authenticated, username=username, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online, is_user_online=lambda u: is_user_online(data, u))
@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']
update_last_seen(data, 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
unread_count = get_unread_count(data, username)
private_unread_count = get_private_unread_count(data, username)
user_count = len(data['users'])
is_online = is_user_online(data, username)
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)
last_seen = user_data.get('last_seen', 'Never')
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)
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)
return redirect(url_for('profile'))
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<title>Profile - {{ username }} - Content Hub</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 1000px;
}
.profile-header {
display: flex;
gap: 30px;
background: var(--card-bg);
padding: 35px;
border-radius: 20px;
box-shadow: var(--shadow);
margin-bottom: 30px;
transition: var(--transition);
}
body.dark .profile-header {
background: var(--card-bg-dark);
}
.profile-header:hover {
transform: translateY(-5px);
}
.avatar {
width: 130px;
height: 130px;
border-radius: 50%;
object-fit: cover;
box-shadow: var(--shadow);
transition: var(--transition);
loading: lazy;
}
.avatar:hover {
transform: scale(1.05);
}
.profile-info h1 {
font-size: 2.4em;
font-weight: 800;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
.profile-info p {
font-size: 1.1em;
margin: 8px 0;
}
.post-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 25px;
}
.post-item {
background: var(--card-bg);
padding: 20px;
border-radius: 16px;
box-shadow: var(--shadow);
transition: var(--transition);
}
body.dark .post-item {
background: var(--card-bg-dark);
}
.post-item:hover {
transform: translateY(-5px);
box-shadow: 0 15px 45px rgba(0, 0, 0, 0.2);
}
.post-preview {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 12px;
margin-bottom: 15px;
transition: var(--transition);
loading: lazy;
}
.post-preview:hover {
transform: scale(1.03);
}
.post-item h3 {
font-size: 1.3em;
font-weight: 700;
margin-bottom: 10px;
color: var(--primary);
}
.post-item a {
text-decoration: none;
}
.delete-btn {
background: var(--secondary);
margin-top: 10px;
}
.delete-btn:hover {
background: #00b8c5;
}
@media (max-width: 900px) {
.profile-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.avatar {
width: 100px;
height: 100px;
}
.post-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
@media (max-width: 480px) {
.profile-info h1 {
font-size: 1.9em;
}
.post-grid {
grid-template-columns: 1fr;
}
.post-preview {
height: 180px;
}
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<div class="profile-header">
{% if avatar %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Avatar" class="avatar" loading="lazy" onclick="openModal(this.src)">
{% else %}
<div class="avatar" style="background: var(--primary);"></div>
{% endif %}
<div class="profile-info">
<h1>{{ username }} <span class="status-dot {{ 'online' if is_online else 'offline' }}"></span></h1>
<p>{{ bio }}</p>
{% if link %}
<p><a href="{{ link }}" target="_blank" class="username-link">{{ link }}</a></p>
{% endif %}
<p>Total Views: {{ total_views }} | Total Likes: {{ total_likes }}</p>
<p>Last Seen: {{ last_seen }}</p>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="update_profile" value="1">
<textarea name="bio" placeholder="Bio">{{ bio }}</textarea>
<input type="text" name="link" placeholder="Link" value="{{ link }}">
<input type="file" name="avatar" accept="image/*">
<button type="submit" class="btn">Update Profile</button>
</form>
</div>
</div>
<h2 style="font-size: 1.9em; margin-bottom: 25px;">Posts</h2>
<div class="post-grid">
{% 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 loading="lazy">
<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'] }}" loading="lazy">
{% endif %}
<h3>{{ post['title'] }}</h3>
</a>
<p>{{ post['description']|truncate(100) }}</p>
<p>{{ post['upload_date'] }} | Views: {{ post['views'] }} | Likes: {{ post['likes']|length }}</p>
<form method="POST">
<input type="hidden" name="delete_post" value="1">
<input type="hidden" name="post_id" value="{{ post['id'] }}">
<button type="submit" class="btn delete-btn">Delete</button>
</form>
</div>
{% endfor %}
{% if not user_posts %}
<p style="font-size: 1.1em;">This user hasn't uploaded any posts yet.</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 toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function openModal(src) {
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
modal.style.display = 'flex';
modalImg.src = src;
}
function closeModal(event) {
if (event.target.tagName !== 'IMG') {
document.getElementById('imageModal').style.display = 'none';
}
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const videos = document.querySelectorAll('.post-preview');
videos.forEach(video => {
if (video.tagName === 'VIDEO') {
video.addEventListener('loadedmetadata', () => {
video.currentTime = Math.random() * video.duration;
});
}
});
};
</script>
</body>
</html>
'''
return render_template_string(html,
username=username,
user_posts=user_posts,
bio=bio,
link=link,
avatar=avatar,
total_views=total_views,
total_likes=total_likes,
last_seen=last_seen,
is_authenticated=is_authenticated,
repo_id=REPO_ID,
unread_count=unread_count,
user_count=user_count,
private_unread_count=private_unread_count,
is_online=is_online)
@app.route('/user/<username>')
def user_profile(username):
data = load_data()
current_user = session.get('username', None)
if current_user:
update_last_seen(data, current_user)
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
unread_count = get_unread_count(data, current_user) if is_authenticated else 0
private_unread_count = get_private_unread_count(data, current_user) if is_authenticated else 0
user_count = len(data['users'])
is_online = is_user_online(data, current_user) if is_authenticated else False
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)
last_seen = user_data.get('last_seen', 'Never')
is_user_online_status = is_user_online(data, username)
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<title>{{ username }}'s Profile - Content Hub</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 1000px;
}
.profile-header {
display: flex;
gap: 30px;
background: var(--card-bg);
padding: 35px;
border-radius: 20px;
box-shadow: var(--shadow);
margin-bottom: 30px;
transition: var(--transition);
}
body.dark .profile-header {
background: var(--card-bg-dark);
}
.profile-header:hover {
transform: translateY(-5px);
}
.avatar {
width: 130px;
height: 130px;
border-radius: 50%;
object-fit: cover;
box-shadow: var(--shadow);
transition: var(--transition);
loading: lazy;
}
.avatar:hover {
transform: scale(1.05);
}
.profile-info h1 {
font-size: 2.4em;
font-weight: 800;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
.profile-info p {
font-size: 1.1em;
margin: 8px 0;
}
.post-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 25px;
}
.post-item {
background: var(--card-bg);
padding: 20px;
border-radius: 16px;
box-shadow: var(--shadow);
transition: var(--transition);
}
body.dark .post-item {
background: var(--card-bg-dark);
}
.post-item:hover {
transform: translateY(-5px);
box-shadow: 0 15px 45px rgba(0, 0, 0, 0.2);
}
.post-preview {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 12px;
margin-bottom: 15px;
transition: var(--transition);
loading: lazy;
}
.post-preview:hover {
transform: scale(1.03);
}
.post-item h3 {
font-size: 1.3em;
font-weight: 700;
margin-bottom: 10px;
color: var(--primary);
}
.post-item a {
text-decoration: none;
}
@media (max-width: 900px) {
.profile-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.avatar {
width: 100px;
height: 100px;
}
.post-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
@media (max-width: 480px) {
.profile-info h1 {
font-size: 1.9em;
}
.post-grid {
grid-template-columns: 1fr;
}
.post-preview {
height: 180px;
}
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<div class="profile-header">
{% if avatar %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Avatar" class="avatar" loading="lazy" onclick="openModal(this.src)">
{% else %}
<div class="avatar" style="background: var(--primary);"></div>
{% endif %}
<div class="profile-info">
<h1>{{ username }} <span class="status-dot {{ 'online' if is_user_online_status else 'offline' }}"></span></h1>
<p>{{ bio }}</p>
{% if link %}
<p><a href="{{ link }}" target="_blank" class="username-link">{{ link }}</a></p>
{% endif %}
<p>Total Views: {{ total_views }} | Total Likes: {{ total_likes }}</p>
<p>Last Seen: {{ last_seen }}</p>
{% if is_authenticated and username != current_user %}
<a href="{{ url_for('messages') }}#chat_{{ username }}" class="btn">Message</a>
{% endif %}
</div>
</div>
<h2 style="font-size: 1.9em; margin-bottom: 25px;">Posts</h2>
<div class="post-grid">
{% 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 loading="lazy">
<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'] }}" loading="lazy">
{% endif %}
<h3>{{ post['title'] }}</h3>
</a>
<p>{{ post['description']|truncate(100) }}</p>
<p>{{ post['upload_date'] }} | Views: {{ post['views'] }} | Likes: {{ post['likes']|length }}</p>
</div>
{% endfor %}
{% if not user_posts %}
<p style="font-size: 1.1em;">This user hasn't uploaded any posts yet.</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 toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function openModal(src) {
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
modal.style.display = 'flex';
modalImg.src = src;
}
function closeModal(event) {
if (event.target.tagName !== 'IMG') {
document.getElementById('imageModal').style.display = 'none';
}
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const videos = document.querySelectorAll('.post-preview');
videos.forEach(video => {
if (video.tagName === 'VIDEO') {
video.addEventListener('loadedmetadata', () => {
video.currentTime = Math.random() * video.duration;
});
}
});
};
</script>
</body>
</html>
'''
return render_template_string(html,
username=username,
current_user=current_user,
user_posts=user_posts,
bio=bio,
link=link,
avatar=avatar,
total_views=total_views,
total_likes=total_likes,
last_seen=last_seen,
is_authenticated=is_authenticated,
repo_id=REPO_ID,
unread_count=unread_count,
user_count=user_count,
private_unread_count=private_unread_count,
is_online=is_online,
is_user_online_status=is_user_online_status)
@app.route('/upload', methods=['GET', 'POST'])
def upload():
if 'username' not in session:
flash('Войдите, чтобы загрузить контент!')
return redirect(url_for('login'))
data = load_data()
username = session['username']
update_last_seen(data, username)
is_authenticated = 'username' in session
unread_count = get_unread_count(data, username)
private_unread_count = get_private_unread_count(data, username)
user_count = len(data['users'])
is_online = is_user_online(data, username)
if request.method == 'POST':
title = request.form.get('title')
description = request.form.get('description')
file = request.files.get('file')
if file and title:
filename = secure_filename(file.filename)
file_type = 'video' if filename.endswith(('.mp4', '.mov', '.avi')) else 'image'
temp_path = os.path.join('uploads', filename)
os.makedirs('uploads', exist_ok=True)
file.save(temp_path)
api = HfApi()
file_path_in_repo = f"{file_type}s/{username}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{filename}"
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=file_path_in_repo,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Загружен {file_type} от {username}"
)
post_id = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=10))
data['posts'].append({
'id': post_id,
'title': title,
'description': description,
'uploader': username,
'filename': file_path_in_repo,
'type': file_type,
'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'views': 0,
'likes': [],
'comments': []
})
save_data(data)
if os.path.exists(temp_path):
os.remove(temp_path)
flash('Контент успешно загружен!')
return redirect(url_for('profile'))
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<title>Upload Content - Content Hub</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 700px;
background: var(--card-bg);
padding: 40px;
border-radius: 20px;
box-shadow: var(--shadow);
animation: slideUp 0.4s ease;
}
body.dark .container {
background: var(--card-bg-dark);
}
h1 {
font-size: 2.2em;
font-weight: 800;
text-align: center;
margin-bottom: 30px;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
.flash {
color: var(--secondary);
text-align: center;
margin-bottom: 15px;
font-size: 1.1em;
font-weight: 600;
}
textarea {
height: 120px;
resize: vertical;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<h1>Upload Content</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" enctype="multipart/form-data">
<input type="text" name="title" placeholder="Title" required>
<textarea name="description" placeholder="Description"></textarea>
<input type="file" name="file" accept="image/*,video/*" required>
<button type="submit" class="btn">Upload</button>
</form>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
};
</script>
</body>
</html>
'''
return render_template_string(html,
is_authenticated=is_authenticated,
username=username,
unread_count=unread_count,
user_count=user_count,
private_unread_count=private_unread_count,
is_online=is_online)
@app.route('/chat', methods=['GET', 'POST'])
def chat():
data = load_data()
username = session.get('username', None)
if username:
update_last_seen(data, username)
data['users'][username]['last_chat_visit'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
save_data(data)
is_authenticated = 'username' in session
chat_messages = sorted(data['general_chat'], key=lambda x: datetime.strptime(x['time'], '%Y-%m-%d %H:%M:%S'))
unread_count = get_unread_count(data, username) if is_authenticated else 0
private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0
user_count = len(data['users'])
is_online = is_user_online(data, username) if is_authenticated else False
if request.method == 'POST' and is_authenticated:
message = request.form.get('message')
file = request.files.get('file')
post_id = request.form.get('post_id')
if message or file or post_id:
msg = {
'sender': username,
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
if message:
msg['text'] = message
if post_id:
msg['post_id'] = post_id
if file and file.filename:
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.endswith(('.mp4', '.mov', '.avi')) else 'image'
api = HfApi()
file_path = f"chat_files/{username}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{filename}"
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=file_path,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Загружен файл чата от {username}"
)
msg['file'] = file_path
msg['file_type'] = file_type
if os.path.exists(temp_path):
os.remove(temp_path)
data['general_chat'].append(msg)
save_data(data)
return redirect(url_for('chat'))
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<title>General Chat - Content Hub</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 1000px;
}
h1 {
font-size: 2.4em;
font-weight: 800;
margin-bottom: 25px;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
.messages {
max-height: 600px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 25px;
padding: 15px;
background: var(--glass-bg);
border-radius: 20px;
box-shadow: var(--shadow);
scroll-behavior: smooth;
}
.message {
display: flex;
gap: 15px;
transition: var(--transition);
}
.message:hover {
transform: translateY(-2px);
}
.message img, .message video {
max-width: 250px;
max-height: 250px;
border-radius: 12px;
box-shadow: var(--shadow);
cursor: pointer;
transition: var(--transition);
loading: lazy;
}
.message img:hover, .message video:hover {
transform: scale(1.05);
}
.message-content {
background: var(--glass-bg);
padding: 15px;
border-radius: 14px;
flex-grow: 1;
box-shadow: inset 0 2px 8px rgba(0,0,0,0.1);
}
.message-content p {
font-size: 1.1em;
word-wrap: break-word;
}
.message-content .sender {
font-weight: 700;
color: var(--primary);
text-decoration: none;
}
.message-content .sender:hover {
color: var(--accent);
}
.message-content .time {
font-size: 0.9em;
color: var(--text-light);
opacity: 0.8;
}
body.dark .message-content .time {
color: var(--text-dark);
}
.post-link {
color: var(--secondary);
text-decoration: none;
font-weight: 600;
}
.post-link:hover {
color: var(--accent);
}
.chat-form {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.chat-form textarea {
flex-grow: 1;
min-height: 60px;
resize: vertical;
}
.chat-form input[type="file"] {
padding: 10px;
}
@media (max-width: 480px) {
.container {
padding: 20px;
}
.messages {
max-height: 400px;
}
.message img, .message video {
max-width: 200px;
max-height: 200px;
}
.chat-form {
flex-direction: column;
}
h1 {
font-size: 1.9em;
}
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<h1>General Chat</h1>
<div class="messages" id="messages">
{% for msg in chat_messages %}
<div class="message">
{% if 'file' in msg %}
{% if msg['file_type'] == 'video' %}
<video src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ msg['file'] }}" preload="metadata" muted onclick="openModal(this.querySelector('source').src)" loading="lazy">
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ msg['file'] }}" type="video/mp4">
</video>
{% else %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ msg['file'] }}" alt="Chat file" onclick="openModal(this.src)" loading="lazy">
{% endif %}
{% endif %}
<div class="message-content">
<p><a href="{{ url_for('user_profile', username=msg['sender']) }}" class="sender">{{ msg['sender'] }}</a> <span class="status-dot {{ 'online' if is_user_online(msg['sender']) else 'offline' }}"></span> <span class="time">({{ msg['time'] }})</span></p>
{% if 'text' in msg %}
<p>{{ msg['text'] }}</p>
{% endif %}
{% if 'post_id' in msg %}
<p>Shared post: <a href="{{ url_for('post_page', post_id=msg['post_id']) }}" class="post-link">View Post</a></p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% if is_authenticated %}
<form method="POST" class="chat-form" enctype="multipart/form-data">
<textarea name="message" placeholder="Type a message"></textarea>
<input type="file" name="file" accept="image/*,video/*">
<button type="submit" class="btn">Send</button>
</form>
{% else %}
<p style="text-align: center; font-size: 1.1em;"><a href="{{ url_for('login') }}">Login</a> to send messages.</p>
{% endif %}
</div>
<div class="modal" id="mediaModal" onclick="closeModal(event)">
<img id="modalMedia" src="">
<video controls id="modalVideo" style="display: none;">
<source src="" type="video/mp4">
</video>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function openModal(src) {
const modal = document.getElementById('mediaModal');
const modalImg = document.getElementById('modalMedia');
const modalVideo = document.getElementById('modalVideo');
modal.style.display = 'flex';
if (src.endsWith('.mp4') || src.endsWith('.mov') || src.endsWith('.avi')) {
modalImg.style.display = 'none';
modalVideo.style.display = 'block';
modalVideo.querySelector('source').src = src;
modalVideo.load();
} else {
modalVideo.style.display = 'none';
modalImg.style.display = 'block';
modalImg.src = src;
}
}
function closeModal(event) {
if (event.target.tagName !== 'IMG' && event.target.tagName !== 'VIDEO') {
const modal = document.getElementById('mediaModal');
modal.style.display = 'none';
const modalVideo = document.getElementById('modalVideo');
modalVideo.pause();
}
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const messagesDiv = document.getElementById('messages');
messagesDiv.scrollTop = messagesDiv.scrollHeight;
};
</script>
</body>
</html>
'''
return render_template_string(html,
chat_messages=chat_messages,
is_authenticated=is_authenticated,
username=username,
repo_id=REPO_ID,
unread_count=unread_count,
user_count=user_count,
private_unread_count=private_unread_count,
is_online=is_online,
is_user_online=lambda u: is_user_online(data, u))
@app.route('/users')
def users():
data = load_data()
username = session.get('username', None)
if username:
update_last_seen(data, username)
is_authenticated = 'username' in session
unread_count = get_unread_count(data, username) if is_authenticated else 0
private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0
user_count = len(data['users'])
is_online = is_user_online(data, username) if is_authenticated else False
users_list = sorted(data['users'].keys())
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<title>Users - Content Hub</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 1000px;
}
h1 {
font-size: 2.4em;
font-weight: 800;
margin-bottom: 25px;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.user-card {
background: var(--card-bg);
padding: 20px;
border-radius: 16px;
box-shadow: var(--shadow);
transition: var(--transition);
}
body.dark .user-card {
background: var(--card-bg-dark);
}
.user-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 45px rgba(0, 0, 0, 0.2);
}
.user-card a {
font-size: 1.3em;
font-weight: 700;
color: var(--primary);
text-decoration: none;
}
.user-card a:hover {
color: var(--accent);
}
.user-card p {
font-size: 1em;
margin-top: 10px;
}
@media (max-width: 480px) {
.user-grid {
grid-template-columns: 1fr;
}
h1 {
font-size: 1.9em;
}
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<h1>Users ({{ user_count }})</h1>
<div class="user-grid">
{% for user in users_list %}
<div class="user-card">
<a href="{{ url_for('user_profile', username=user) }}">{{ user }}</a>
<span class="status-dot {{ 'online' if is_user_online(user) else 'offline' }}"></span>
<p>Last seen: {{ data['users'][user]['last_seen'] }}</p>
{% if is_authenticated and user != username %}
<a href="{{ url_for('messages') }}#chat_{{ user }}" class="btn" style="margin-top: 10px;">Message</a>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
};
</script>
</body>
</html>
'''
return render_template_string(html,
users_list=users_list,
data=data,
is_authenticated=is_authenticated,
username=username,
unread_count=unread_count,
user_count=user_count,
private_unread_count=private_unread_count,
is_online=is_online,
is_user_online=lambda u: is_user_online(data, u))
@app.route('/messages', methods=['GET', 'POST'])
def messages():
if 'username' not in session:
flash('Войдите, чтобы просмотреть сообщения!')
return redirect(url_for('login'))
data = load_data()
username = session['username']
update_last_seen(data, username)
data['users'][username]['last_private_visit'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
save_data(data)
is_authenticated = 'username' in session
unread_count = get_unread_count(data, username)
private_unread_count = get_private_unread_count(data, username)
user_count = len(data['users'])
is_online = is_user_online(data, username)
private_chats = {k: v for k, v in data['private_chats'].items() if username in k.split('_')}
if request.method == 'POST':
recipient = request.form.get('recipient')
message = request.form.get('message')
file = request.files.get('file')
if recipient and (message or file):
if recipient not in data['users']:
flash('Получатель не найден!')
return redirect(url_for('messages'))
chat_key = '_'.join(sorted([username, recipient]))
if chat_key not in data['private_chats']:
data['private_chats'][chat_key] = []
msg = {
'sender': username,
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
if message:
msg['text'] = message
if file and file.filename:
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.endswith(('.mp4', '.mov', '.avi')) else 'image'
api = HfApi()
file_path = f"private_files/{chat_key}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{filename}"
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=file_path,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Загружен файл приватного чата между {username} и {recipient}"
)
msg['file'] = file_path
msg['file_type'] = file_type
if os.path.exists(temp_path):
os.remove(temp_path)
data['private_chats'][chat_key].append(msg)
save_data(data)
return redirect(url_for('messages') + f'#chat_{recipient}')
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock">
<title>Private Messages - Content Hub</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
''' + BASE_STYLE + '''
.container {
max-width: 1000px;
}
h1 {
font-size: 2.4em;
font-weight: 800;
margin-bottom: 25px;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
color: transparent;
}
.chat-list {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 25px;
}
.contacts {
background: var(--card-bg);
padding: 20px;
border-radius: 16px;
box-shadow: var(--shadow);
max-height: 600px;
overflow-y: auto;
}
body.dark .contacts {
background: var(--card-bg-dark);
}
.contact {
padding: 15px;
border-radius: 12px;
margin-bottom: 10px;
transition: var(--transition);
}
.contact:hover {
background: var(--glass-bg);
transform: translateY(-2px);
}
.contact a {
font-size: 1.2em;
font-weight: 600;
color: var(--primary);
text-decoration: none;
}
.contact a:hover {
color: var(--accent);
}
.chat-box {
background: var(--card-bg);
padding: 20px;
border-radius: 16px;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
gap: 15px;
}
body.dark .chat-box {
background: var(--card-bg-dark);
}
.messages {
max-height: 500px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
padding: 15px;
background: var(--glass-bg);
border-radius: 14px;
}
.message {
display: flex;
gap: 15px;
transition: var(--transition);
}
.message:hover {
transform: translateY(-2px);
}
.message img, .message video {
max-width: 200px;
max-height: 200px;
border-radius: 12px;
box-shadow: var(--shadow);
cursor: pointer;
transition: var(--transition);
loading: lazy;
}
.message img:hover, .message video:hover {
transform: scale(1.05);
}
.message-content {
background: var(--glass-bg);
padding: 15px;
border-radius-irracial porn, bbc porn, qos, queen of spades, big black cock 14px;
flex-grow: 1;
box-shadow: inset 0 2px 8px rgba(0,0,0,0.1);
}
.message-content p {
font-size: 1.1em;
word-wrap: break-word;
}
.message-content .sender {
font-weight: 700;
color: var(--primary);
text-decoration: none;
}
.message-content .sender:hover {
color: var(--accent);
}
.message-content .time {
font-size: 0.9em;
color: var(--text-light);
opacity: 0.8;
}
body.dark .message-content .time {
color: var(--text-dark);
}
.chat-form {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.chat-form textarea {
flex-grow: 1;
min-height: 60px;
resize: vertical;
}
.chat-form input[type="file"] {
padding: 10px;
}
@media (max-width: 900px) {
.chat-list {
grid-template-columns: 1fr;
}
.contacts, .chat-box {
max-height: 400px;
}
}
@media (max-width: 480px) {
.container {
padding: 20px;
}
.messages {
max-height: 300px;
}
.message img, .message video {
max-width: 150px;
max-height: 150px;
}
.chat-form {
flex-direction: column;
}
h1 {
font-size: 1.9em;
}
}
</style>
</head>
<body>
<button class="menu-btn" onclick="toggleSidebar()">☰</button>
''' + NAV_HTML + '''
<button class="theme-toggle" onclick="toggleTheme()">🌙</button>
<div class="container">
<h1>Private Messages</h1>
<div class="chat-list">
<div class="contacts">
{% for chat_key, messages in private_chats.items() %}
{% set recipient = chat_key.split('_')|reject('eq', username)|first %}
<div class="contact" id="chat_{{ recipient }}">
<a href="#chat_{{ recipient }}">{{ recipient }}</a>
<span class="status-dot {{ 'online' if is_user_online(recipient) else 'offline' }}"></span>
</div>
{% endfor %}
<form method="POST">
<input type="text" name="recipient" placeholder="Start a new chat" list="users" required>
<datalist id="users">
{% for user in data['users'].keys() %}
{% if user != username %}
<option value="{{ user }}">
{% endif %}
{% endfor %}
</datalist>
<button type="submit" class="btn">Start Chat</button>
</form>
</div>
<div class="chat-box">
{% if private_chats %}
{% set active_chat = request.args.get('chat') or (private_chats.keys()|list|first).split('_')|reject('eq', username)|first %}
{% set chat_key = '_'.join(sorted([username, active_chat])) %}
{% set messages = private_chats.get(chat_key, []) %}
<h2 style="font-size: 1.6em;">Chat with <a href="{{ url_for('user_profile', username=active_chat) }}" class="sender">{{ active_chat }}</a></h2>
<div class="messages" id="messages">
{% for msg in messages %}
<div class="message">
{% if 'file' in msg %}
{% if msg['file_type'] == 'video' %}
<video src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ msg['file'] }}" preload="metadata" muted onclick="openModal(this.querySelector('source').src)" loading="lazy">
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ msg['file'] }}" type="video/mp4">
</video>
{% else %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ msg['file'] }}" alt="Chat file" onclick="openModal(this.src)" loading="lazy">
{% endif %}
{% endif %}
<div class="message-content">
<p><a href="{{ url_for('user_profile', username=msg['sender']) }}" class="sender">{{ msg['sender'] }}</a> <span class="status-dot {{ 'online' if is_user_online(msg['sender']) else 'offline' }}"></span> <span class="time">({{ msg['time'] }})</span></p>
{% if 'text' in msg %}
<p>{{ msg['text'] }}</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<form method="POST" class="chat-form" enctype="multipart/form-data">
<input type="hidden" name="recipient" value="{{ active_chat }}">
<textarea name="message" placeholder="Type a message"></textarea>
<input type="file" name="file" accept="image/*,video/*">
<button type="submit" class="btn">Send</button>
</form>
{% else %}
<p style="text-align: center; font-size: 1.1em;">No private chats yet. Start one!</p>
{% endif %}
</div>
</div>
</div>
<div class="modal" id="mediaModal" onclick="closeModal(event)">
<img id="modalMedia" src="">
<video controls id="modalVideo" style="display: none;">
<source src="" type="video/mp4">
</video>
</div>
<script>
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('active');
}
function toggleTheme() {
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light');
}
function openModal(src) {
const modal = document.getElementById('mediaModal');
const modalImg = document.getElementById('modalMedia');
const modalVideo = document.getElementById('modalVideo');
modal.style.display = 'flex';
if (src.endsWith('.mp4') || src.endsWith('.mov') || src.endsWith('.avi')) {
modalImg.style.display = 'none';
modalVideo.style.display = 'block';
modalVideo.querySelector('source').src = src;
modalVideo.load();
} else {
modalVideo.style.display = 'none';
modalImg.style.display = 'block';
modalImg.src = src;
}
}
function closeModal(event) {
if (event.target.tagName !== 'IMG' && event.target.tagName !== 'VIDEO') {
const modal = document.getElementById('mediaModal');
modal.style.display = 'none';
const modalVideo = document.getElementById('modalVideo');
modalVideo.pause();
}
}
window.onload = () => {
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark');
const messagesDiv = document.getElementById('messages');
if (messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight;
const hash = window.location.hash;
if (hash) {
const contact = document.querySelector(hash);
if (contact) contact.scrollIntoView({ behavior: 'smooth' });
}
};
</script>
</body>
</html>
'''
return render_template_string(html,
private_chats=private_chats,
data=data,
is_authenticated=is_authenticated,
username=username,
unread_count=unread_count,
user_count=user_count,
private_unread_count=private_unread_count,
is_online=is_online,
is_user_online=lambda u: is_user_online(data, u))
if __name__ == '__main__':
threading.Thread(target=periodic_backup, daemon=True).start()
app.run(debug=True, host='0.0.0.0', port=7860)