|
|
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) |