|
|
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("Data is not in dict format, initializing an empty database") |
|
|
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("Data loaded successfully") |
|
|
return data |
|
|
except Exception as e: |
|
|
logging.error(f"Error loading data: {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("Data saved and uploaded to HF") |
|
|
except Exception as e: |
|
|
logging.error(f"Error saving data: {e}") |
|
|
raise |
|
|
|
|
|
def upload_db_to_hf(): |
|
|
try: |
|
|
api = HfApi() |
|
|
api.upload_file( |
|
|
path_or_fileobj=DATA_FILE, |
|
|
path_in_repo=DATA_FILE, |
|
|
repo_id=REPO_ID, |
|
|
repo_type="dataset", |
|
|
token=HF_TOKEN_WRITE, |
|
|
commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" |
|
|
) |
|
|
logging.info("Database uploaded to Hugging Face") |
|
|
except Exception as e: |
|
|
logging.error(f"Error uploading database: {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("Database downloaded from Hugging Face") |
|
|
except Exception as e: |
|
|
logging.error(f"Error downloading database: {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: #f5f6fa; |
|
|
--background-dark: #1a1625; |
|
|
--card-bg: rgba(255, 255, 255, 0.95); |
|
|
--card-bg-dark: rgba(40, 35, 60, 0.95); |
|
|
--text-light: #2a1e5a; |
|
|
--text-dark: #e8e1ff; |
|
|
--shadow: 0 10px 30px rgba(0, 0, 0, 0.2); |
|
|
--glass-bg: rgba(255, 255, 255, 0.15); |
|
|
--transition: all 0.3s ease; |
|
|
--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.6; |
|
|
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(20px); |
|
|
padding: 25px; |
|
|
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: 35px; |
|
|
} |
|
|
.nav-brand { |
|
|
font-size: 1.8em; |
|
|
font-weight: 900; |
|
|
background: linear-gradient(135deg, var(--primary), var(--accent)); |
|
|
-webkit-background-clip: text; |
|
|
color: transparent; |
|
|
} |
|
|
.logo { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border-radius: 14px; |
|
|
box-shadow: var(--shadow); |
|
|
} |
|
|
.nav-links { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 15px; |
|
|
} |
|
|
.nav-link { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 15px; |
|
|
padding: 14px 25px; |
|
|
background: var(--card-bg); |
|
|
color: var(--text-light); |
|
|
text-decoration: none; |
|
|
border-radius: 14px; |
|
|
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(5px); |
|
|
background: var(--primary); |
|
|
color: white; |
|
|
box-shadow: 0 6px 20px rgba(255, 77, 109, 0.4); |
|
|
} |
|
|
.nav-link .badge { |
|
|
position: absolute; |
|
|
right: 15px; |
|
|
background: var(--secondary); |
|
|
color: white; |
|
|
padding: 4px 10px; |
|
|
border-radius: 12px; |
|
|
font-size: 0.8em; |
|
|
font-weight: 700; |
|
|
} |
|
|
.logout-btn { |
|
|
background: var(--secondary); |
|
|
color: white; |
|
|
} |
|
|
.logout-btn:hover { |
|
|
background: #00b8c5; |
|
|
} |
|
|
.menu-btn { |
|
|
display: none; |
|
|
font-size: 28px; |
|
|
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: 25px; |
|
|
transition: var(--transition); |
|
|
} |
|
|
.btn { |
|
|
padding: 14px 28px; |
|
|
background: var(--primary); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 14px; |
|
|
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 8px 25px rgba(255, 77, 109, 0.5); |
|
|
} |
|
|
input, textarea, select { |
|
|
width: 100%; |
|
|
padding: 14px; |
|
|
margin: 12px 0; |
|
|
border: none; |
|
|
border-radius: 14px; |
|
|
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.2); |
|
|
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.85); |
|
|
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.3s ease; |
|
|
} |
|
|
.theme-toggle { |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
background: var(--glass-bg); |
|
|
border: none; |
|
|
padding: 12px; |
|
|
border-radius: 50%; |
|
|
cursor: pointer; |
|
|
font-size: 24px; |
|
|
box-shadow: var(--shadow); |
|
|
transition: var(--transition); |
|
|
} |
|
|
.theme-toggle:hover { |
|
|
transform: rotate(90deg); |
|
|
background: var(--accent); |
|
|
color: white; |
|
|
} |
|
|
.status-dot { |
|
|
width: 10px; |
|
|
height: 10px; |
|
|
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.9); } |
|
|
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.5em; } |
|
|
.nav-link { font-size: 1em; padding: 12px 20px; } |
|
|
.btn { padding: 12px 20px; font-size: 1em; } |
|
|
} |
|
|
''' |
|
|
|
|
|
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('User already exists!') |
|
|
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('Registration successful! Please login.') |
|
|
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: 35px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
animation: slideUp 0.4s ease; |
|
|
} |
|
|
body.dark .container { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2em; |
|
|
font-weight: 800; |
|
|
text-align: center; |
|
|
margin-bottom: 25px; |
|
|
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: 1em; |
|
|
font-weight: 600; |
|
|
} |
|
|
.link { |
|
|
text-align: center; |
|
|
margin-top: 20px; |
|
|
color: var(--primary); |
|
|
font-size: 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('Invalid username or password!') |
|
|
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: 35px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
animation: slideUp 0.4s ease; |
|
|
} |
|
|
body.dark .container { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2em; |
|
|
font-weight: 800; |
|
|
text-align: center; |
|
|
margin-bottom: 25px; |
|
|
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: 1em; |
|
|
font-weight: 600; |
|
|
} |
|
|
.link { |
|
|
text-align: center; |
|
|
margin-top: 20px; |
|
|
color: var(--primary); |
|
|
font-size: 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 |
|
|
|
|
|
search_query = request.args.get('search', '').strip().lower() |
|
|
if search_query: |
|
|
posts = [post for post in posts if search_query in post['title'].lower() or search_query in post['description'].lower()] |
|
|
|
|
|
|
|
|
if request.method == 'POST' and 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 "Post not found", 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', search=search_query)) |
|
|
|
|
|
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; |
|
|
|
|
|
} |
|
|
.feed-container { |
|
|
height: calc(100vh - 80px); /* Adjust for bottom navbar */ |
|
|
width: 100%; |
|
|
overflow-y: scroll; |
|
|
scroll-snap-type: y mandatory; |
|
|
scroll-behavior: smooth; |
|
|
margin-left: 0; |
|
|
padding: 0; |
|
|
-webkit-overflow-scrolling: touch; |
|
|
padding-bottom: 80px; /* Space for the bottom navbar */ |
|
|
} |
|
|
.post-container { |
|
|
height: calc(100vh - 80px); /* Adjust for bottom navbar */ |
|
|
width: 100%; |
|
|
position: relative; |
|
|
scroll-snap-align: center; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
background: var(--background-light); |
|
|
overflow: hidden; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
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; |
|
|
loading: lazy; |
|
|
} |
|
|
.play-btn { |
|
|
position: absolute; |
|
|
top: 50%; |
|
|
left: 50%; |
|
|
transform: translate(-50%, -50%); |
|
|
background: rgba(255, 255, 255, 0.8); |
|
|
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; |
|
|
} |
|
|
.post-overlay { |
|
|
position: absolute; |
|
|
bottom: 80px; |
|
|
left: 25px; |
|
|
right: 25px; |
|
|
z-index: 2; |
|
|
color: white; |
|
|
background: linear-gradient(to top, rgba(0,0,0,0.8), transparent); |
|
|
padding: 20px; |
|
|
border-radius: 14px; |
|
|
pointer-events: none; |
|
|
} |
|
|
.post-overlay h2 { |
|
|
font-size: 1.5em; |
|
|
font-weight: 800; |
|
|
margin-bottom: 10px; |
|
|
text-shadow: 0 3px 6px rgba(0,0,0,0.9); |
|
|
} |
|
|
.post-overlay p { |
|
|
font-size: 1em; |
|
|
text-shadow: 0 3px 6px rgba(0,0,0,0.9); |
|
|
} |
|
|
.username-link { |
|
|
color: var(--primary); |
|
|
font-weight: 700; |
|
|
text-decoration: none; |
|
|
} |
|
|
.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.25); |
|
|
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(10px); |
|
|
box-shadow: var(--shadow); |
|
|
} |
|
|
.action-btn:hover { |
|
|
background: var(--primary); |
|
|
transform: scale(1.1); |
|
|
} |
|
|
.action-btn.liked { |
|
|
color: var(--primary); |
|
|
background: rgba(255, 77, 109, 0.5); |
|
|
} |
|
|
.action-count { |
|
|
color: white; |
|
|
text-align: center; |
|
|
font-size: 16px; |
|
|
margin-top: 8px; |
|
|
text-shadow: 0 3px 6px rgba(0,0,0,0.9); |
|
|
} |
|
|
.comment-section { |
|
|
display: none; |
|
|
position: absolute; |
|
|
bottom: 180px; |
|
|
left: 25px; |
|
|
right: 25px; |
|
|
background: rgba(0, 0, 0, 0.85); |
|
|
padding: 20px; |
|
|
border-radius: 14px; |
|
|
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.25); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 10px; |
|
|
padding: 12px; |
|
|
font-size: 14px; |
|
|
resize: none; |
|
|
} |
|
|
.comment-form .btn { |
|
|
padding: 12px 20px; |
|
|
font-size: 14px; |
|
|
} |
|
|
.share-options { |
|
|
display: none; /* Initially hide share options */ |
|
|
position: absolute; |
|
|
bottom: 280px; |
|
|
right: 25px; |
|
|
background: rgba(0, 0, 0, 0.85); |
|
|
padding: 15px; |
|
|
border-radius: 14px; |
|
|
z-index: 4; |
|
|
box-shadow: var(--shadow); |
|
|
|
|
|
} |
|
|
/* .share-options.active { |
|
|
display: block; |
|
|
} |
|
|
*/ |
|
|
.share-option { |
|
|
background: none; |
|
|
border: none; |
|
|
color: white; |
|
|
padding: 10px 15px; |
|
|
cursor: pointer; |
|
|
width: 100%; |
|
|
text-align: left; |
|
|
font-size: 14px; |
|
|
transition: var(--transition); |
|
|
} |
|
|
.share-option:hover { |
|
|
background: var(--primary); |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
/* Bottom Navigation Bar Styles */ |
|
|
.bottom-navbar { |
|
|
position: fixed; |
|
|
bottom: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
background: var(--card-bg); |
|
|
display: flex; |
|
|
justify-content: space-around; |
|
|
align-items: center; |
|
|
padding: 10px 0; |
|
|
z-index: 1000; |
|
|
box-shadow: 0 -5px 15px rgba(0,0,0,0.1); |
|
|
} |
|
|
body.dark .bottom-navbar{ |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
|
|
|
.bottom-navbar a, .bottom-navbar button { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
text-decoration: none; |
|
|
color: var(--text-light); |
|
|
font-size: 14px; |
|
|
padding: 8px 12px; |
|
|
border-radius: 10px; |
|
|
transition: var(--transition) |
|
|
} |
|
|
|
|
|
body.dark .bottom-navbar a, body.dark .bottom-navbar button{ |
|
|
color: var(--text-dark); |
|
|
} |
|
|
|
|
|
.bottom-navbar a:hover, .bottom-navbar button:hover { |
|
|
background-color: var(--primary); |
|
|
color: white; |
|
|
} |
|
|
.bottom-navbar i { |
|
|
font-size: 24px; |
|
|
margin-bottom: 4px; |
|
|
} |
|
|
.search-bar-container { |
|
|
position: fixed; /* Fixed position */ |
|
|
bottom: 20px; /* Distance from the bottom */ |
|
|
left: 50%; /* Center horizontally */ |
|
|
transform: translateX(-50%); /* Adjust for centering */ |
|
|
width: 80%; /* Or any desired width */ |
|
|
max-width: 500px; /* Max width */ |
|
|
z-index: 1001; /* Ensure it's above other content, but below sidebar */ |
|
|
} |
|
|
.search-bar { |
|
|
width: 100%; |
|
|
padding: 10px 15px; |
|
|
border-radius: 20px; |
|
|
border: none; |
|
|
background-color: rgba(255,255,255,0.9); |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
@media (max-width: 480px) { |
|
|
.post-overlay { |
|
|
bottom: 60px; |
|
|
left: 15px; |
|
|
right: 15px; |
|
|
padding: 15px; |
|
|
} |
|
|
.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; |
|
|
} |
|
|
.post-overlay h2 { |
|
|
font-size: 1.2em; |
|
|
} |
|
|
.post-overlay p { |
|
|
font-size: 0.9em; |
|
|
} |
|
|
.play-btn { |
|
|
width: 60px; |
|
|
height: 60px; |
|
|
font-size: 30px; |
|
|
} |
|
|
.bottom-navbar { |
|
|
padding: 8px 0; |
|
|
} |
|
|
.bottom-navbar a, .bottom-navbar button { |
|
|
font-size: 12px; |
|
|
padding: 6px 10px; |
|
|
} |
|
|
.bottom-navbar i { |
|
|
font-size: 20px; |
|
|
} |
|
|
.search-bar-container{ |
|
|
bottom:10px; |
|
|
} |
|
|
} |
|
|
|
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<button class="theme-toggle" onclick="toggleTheme()">🌙</button> |
|
|
|
|
|
<!-- Search Bar --> |
|
|
<div class="search-bar-container"> |
|
|
<form action="{{ url_for('feed') }}" method="GET"> |
|
|
<input type="text" name="search" class="search-bar" placeholder="Search posts..." value="{{ request.args.get('search', '') }}"> |
|
|
</form> |
|
|
</div> |
|
|
|
|
|
<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'] }}"> |
|
|
{% 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 class="share-options" id="share-{{ post['id'] }}"> |
|
|
</div> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
|
|
|
|
|
|
<!-- Bottom Navigation Bar --> |
|
|
<div class="bottom-navbar"> |
|
|
<a href="{{ url_for('feed') }}"><i class="fas fa-home"></i> Home</a> |
|
|
{% if is_authenticated %} |
|
|
<a href="{{ url_for('upload') }}"><i class="fas fa-upload"></i> Upload</a> |
|
|
<a href="{{ url_for('profile') }}"><i class="fas fa-user"></i> Profile</a> |
|
|
{% endif %} |
|
|
<a href="#"><i class="fas fa-search"></i> Search</a> |
|
|
</div> |
|
|
|
|
|
|
|
|
<script src="https://kit.fontawesome.com/your-font-awesome-kit-id.js" crossorigin="anonymous"></script> |
|
|
<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 toggleShare(postId) { |
|
|
const shareOptions = document.getElementById('share-' + postId); |
|
|
shareOptions.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'); |
|
|
|
|
|
// Intersection Observer for View Tracking and Video Playback |
|
|
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'); |
|
|
|
|
|
// Auto-scrolling to the center of the current post |
|
|
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), |
|
|
data=data) |
|
|
|
|
|
@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 "Post not found", 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: 30px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
} |
|
|
body.dark .container { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2.2em; |
|
|
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; |
|
|
loading: lazy; |
|
|
} |
|
|
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; |
|
|
} |
|
|
.username-link { |
|
|
color: var(--primary); |
|
|
font-weight: 600; |
|
|
text-decoration: none; |
|
|
} |
|
|
.username-link:hover { |
|
|
color: var(--accent); |
|
|
} |
|
|
@media (max-width: 480px) { |
|
|
video, img { |
|
|
max-height: 350px; |
|
|
} |
|
|
h1 { |
|
|
font-size: 1.8em; |
|
|
} |
|
|
} |
|
|
</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> |
|
|
<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.5em;">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)); |
|
|
} |
|
|
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('Login to view your profile!') |
|
|
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"Uploaded avatar for {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: 25px; |
|
|
background: var(--card-bg); |
|
|
padding: 30px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
body.dark .profile-header { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
.avatar { |
|
|
width: 120px; |
|
|
height: 120px; |
|
|
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.2em; |
|
|
font-weight: 800; |
|
|
background: linear-gradient(135deg, var(--primary), var(--accent)); |
|
|
-webkit-background-clip: text; |
|
|
color: transparent; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
.profile-info p { |
|
|
font-size: 1.1em; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
.post-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); |
|
|
gap: 25px; |
|
|
} |
|
|
.post-item { |
|
|
background: var(--card-bg); |
|
|
padding: 20px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
transition: var(-- transition); |
|
|
} |
|
|
body.dark .post-item { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
.post-item:hover { |
|
|
transform: translateY(-8px); |
|
|
} |
|
|
.post-preview { |
|
|
width: 100%; |
|
|
height: 220px; |
|
|
object-fit: cover; |
|
|
border-radius: 16px; |
|
|
margin-bottom: 15px; |
|
|
loading: lazy; |
|
|
} |
|
|
.post-item h3 { |
|
|
font-size: 1.4em; |
|
|
font-weight: 600; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
.delete-btn { |
|
|
background: var(--secondary); |
|
|
} |
|
|
.delete-btn:hover { |
|
|
background: #00b8c5; |
|
|
} |
|
|
.username-link { |
|
|
color: var(--primary); |
|
|
font-weight: 600; |
|
|
text-decoration: none; |
|
|
} |
|
|
.username-link:hover { |
|
|
color: var(--accent); |
|
|
} |
|
|
@media (max-width: 480px) { |
|
|
.profile-header { |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
padding: 20px; |
|
|
} |
|
|
.avatar { |
|
|
width: 100px; |
|
|
height: 100px; |
|
|
} |
|
|
.profile-info h1 { |
|
|
font-size: 1.8em; |
|
|
text-align: center; |
|
|
} |
|
|
.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> |
|
|
</div> |
|
|
</div> |
|
|
<h2 style="font-size: 1.8em; margin-bottom: 20px;">Edit Profile</h2> |
|
|
<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> |
|
|
<h2 style="font-size: 1.8em; margin: 30px 0 20px;">My 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" style="margin-top: 15px;"> |
|
|
<input type="hidden" name="post_id" value="{{ post['id'] }}"> |
|
|
<button type="submit" name="delete_post" class="btn delete-btn">Delete</button> |
|
|
</form> |
|
|
</div> |
|
|
{% endfor %} |
|
|
{% if not user_posts %} |
|
|
<p style="font-size: 1.1em;">You haven'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>', methods=['GET']) |
|
|
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 "User not found", 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') |
|
|
user_online = 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: 25px; |
|
|
background: var(--card-bg); |
|
|
padding: 30px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
body.dark .profile-header { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
.avatar { |
|
|
width: 120px; |
|
|
height: 120px; |
|
|
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.2em; |
|
|
font-weight: 800; |
|
|
background: linear-gradient(135deg, var(--primary), var(--accent)); |
|
|
-webkit-background-clip: text; |
|
|
color: transparent; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
.profile-info p { |
|
|
font-size: 1.1em; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
.post-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); |
|
|
gap: 25px; |
|
|
} |
|
|
.post-item { |
|
|
background: var(--card-bg); |
|
|
padding: 20px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
transition: var(--transition); |
|
|
} |
|
|
body.dark .post-item { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
.post-item:hover { |
|
|
transform: translateY(-8px); |
|
|
} |
|
|
.post-preview { |
|
|
width: 100%; |
|
|
height: 220px; |
|
|
object-fit: cover; |
|
|
border-radius: 16px; |
|
|
margin-bottom: 15px; |
|
|
loading: lazy; |
|
|
} |
|
|
.post-item h3 { |
|
|
font-size: 1.4em; |
|
|
font-weight: 600; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
.username-link { |
|
|
color: var(--primary); |
|
|
font-weight: 600; |
|
|
text-decoration: none; |
|
|
} |
|
|
.username-link:hover { |
|
|
color: var(--accent); |
|
|
} |
|
|
@media (max-width: 480px) { |
|
|
.profile-header { |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
padding: 20px; |
|
|
} |
|
|
.avatar { |
|
|
width: 100px; |
|
|
height: 100px; |
|
|
} |
|
|
.profile-info h1 { |
|
|
font-size: 1.8em; |
|
|
text-align: center; |
|
|
} |
|
|
.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 user_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> |
|
|
{% if is_authenticated and current_user != username %} |
|
|
<a href="{{ url_for('private_chat', recipient=username) }}" class="btn">Send Message</a> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
<h2 style="font-size: 1.8em; margin-bottom: 20px;">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, |
|
|
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, |
|
|
current_user=current_user, |
|
|
repo_id=REPO_ID, |
|
|
unread_count=unread_count, |
|
|
user_count=user_count, |
|
|
private_unread_count=private_unread_count, |
|
|
is_online=is_online, |
|
|
user_online=user_online) |
|
|
|
|
|
@app.route('/upload', methods=['GET', 'POST']) |
|
|
def upload(): |
|
|
if 'username' not in session: |
|
|
flash('Login to upload content!') |
|
|
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 not file or not title: |
|
|
flash('Title and file are required!') |
|
|
return redirect(url_for('upload')) |
|
|
|
|
|
filename = secure_filename(file.filename) |
|
|
temp_path = os.path.join('uploads', filename) |
|
|
os.makedirs('uploads', exist_ok=True) |
|
|
file.save(temp_path) |
|
|
|
|
|
file_type = 'video' if filename.lower().endswith(('.mp4', '.mov', '.avi')) else 'photo' |
|
|
api = HfApi() |
|
|
file_path = f"{file_type}s/{username}/{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"Uploaded {file_type} by {username}" |
|
|
) |
|
|
|
|
|
post_id = str(random.randint(100000, 999999)) |
|
|
while any(p['id'] == post_id for p in data['posts']): |
|
|
post_id = str(random.randint(100000, 999999)) |
|
|
|
|
|
data['posts'].append({ |
|
|
'id': post_id, |
|
|
'title': title, |
|
|
'description': description, |
|
|
'filename': f"{username}/{filename}", |
|
|
'type': file_type, |
|
|
'uploader': username, |
|
|
'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) |
|
|
|
|
|
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: 35px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
animation: slideUp 0.4s ease; |
|
|
} |
|
|
body.dark .container { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2em; |
|
|
font-weight: 800; |
|
|
margin-bottom: 25px; |
|
|
background: linear-gradient(135deg, var(--primary), var(--accent)); |
|
|
-webkit-background-clip: text; |
|
|
color: transparent; |
|
|
} |
|
|
#progress-container { |
|
|
width: 100%; |
|
|
height: 6px; |
|
|
background: var(--glass-bg); |
|
|
border-radius: 6px; |
|
|
margin-top: 20px; |
|
|
overflow: hidden; |
|
|
} |
|
|
#progress-bar { |
|
|
width: 0%; |
|
|
height: 100%; |
|
|
background: var(--primary); |
|
|
transition: width 0.3s ease; |
|
|
} |
|
|
@media (max-width: 480px) { |
|
|
.container { |
|
|
padding: 20px; |
|
|
} |
|
|
} |
|
|
</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" style="color: var(--secondary); text-align: center; margin-bottom: 15px;">{{ message }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
<form id="upload-form" enctype="multipart/form-data"> |
|
|
<input type="text" name="title" placeholder="Title" required> |
|
|
<textarea name="description" placeholder="Description" rows="4"></textarea> |
|
|
<input type="file" name="file" accept="video/*,image/*" required> |
|
|
<button type="submit" class="btn">Upload</button> |
|
|
</form> |
|
|
<div id="progress-container"> |
|
|
<div id="progress-bar"></div> |
|
|
</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'); |
|
|
} |
|
|
document.getElementById('upload-form').onsubmit = async function(e) { |
|
|
e.preventDefault(); |
|
|
const formData = new FormData(this); |
|
|
const progressBar = document.getElementById('progress-bar'); |
|
|
const xhr = new XMLHttpRequest(); |
|
|
xhr.open('POST', '/upload', true); |
|
|
xhr.upload.onprogress = function(event) { |
|
|
if (event.lengthComputable) { |
|
|
const percent = Math.round((event.loaded / event.total) * 100); |
|
|
progressBar.style.width = percent + '%'; |
|
|
} |
|
|
}; |
|
|
xhr.onload = function() { |
|
|
if (xhr.status === 200) { |
|
|
window.location = '/profile'; |
|
|
} else { |
|
|
alert('Upload error'); |
|
|
} |
|
|
}; |
|
|
xhr.send(formData); |
|
|
}; |
|
|
window.onload = () => { |
|
|
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark'); |
|
|
}; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return render_template_string(html, |
|
|
username=username, |
|
|
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('/chat', methods=['GET', 'POST']) |
|
|
def chat(): |
|
|
data = load_data() |
|
|
username = session.get('username', None) |
|
|
if username: |
|
|
update_last_seen(data, username) |
|
|
is_authenticated = 'username' in session |
|
|
chat_messages = data['general_chat'] |
|
|
|
|
|
if is_authenticated: |
|
|
data['users'][username]['last_chat_visit'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
|
save_data(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 |
|
|
|
|
|
if request.method == 'POST' and is_authenticated: |
|
|
message = request.form.get('message') |
|
|
file = request.files.get('file') |
|
|
post_id = request.form.get('post_id') |
|
|
|
|
|
message_data = { |
|
|
'sender': username, |
|
|
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
|
} |
|
|
|
|
|
if message: |
|
|
message_data['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.lower().endswith(('.mp4', '.mov', '.avi')) else 'photo' |
|
|
api = HfApi() |
|
|
file_path = f"chat_files/general/{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"Uploaded chat file by {username}" |
|
|
) |
|
|
message_data['file'] = file_path |
|
|
message_data['file_type'] = file_type |
|
|
if os.path.exists(temp_path): |
|
|
os.remove(temp_path) |
|
|
|
|
|
if post_id: |
|
|
post = next((p for p in data['posts'] if p['id'] == post_id and p['uploader'] == username), None) |
|
|
if post: |
|
|
message_data['post_id'] = post_id |
|
|
|
|
|
if 'text' in message_data or 'file' in message_data or 'post_id' in message_data: |
|
|
data['general_chat'].append(message_data) |
|
|
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: 900px; |
|
|
background: var(--card-bg); |
|
|
padding: 30px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
} |
|
|
body.dark .container { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2.2em; |
|
|
font-weight: 800; |
|
|
margin-bottom: 25px; |
|
|
background: linear-gradient(135deg, var(--primary), var(--accent)); |
|
|
-webkit-background-clip: text; |
|
|
color: transparent; |
|
|
} |
|
|
.messages { |
|
|
max-height: 500px; |
|
|
overflow-y: auto; |
|
|
padding: 15px; |
|
|
background: var(--glass-bg); |
|
|
border-radius: 14px; |
|
|
margin-bottom: 20px; |
|
|
scroll-behavior: smooth; |
|
|
} |
|
|
.message { |
|
|
margin-bottom: 15px; |
|
|
display: flex; |
|
|
align-items: flex-start; |
|
|
gap: 12px; |
|
|
max-width: 75%; |
|
|
} |
|
|
.message.sent { |
|
|
margin-left: auto; |
|
|
flex-direction: row-reverse; |
|
|
} |
|
|
.message.received { |
|
|
margin-right: auto; |
|
|
} |
|
|
.message-avatar { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border-radius: 50%; |
|
|
object-fit: cover; |
|
|
box-shadow: var(--shadow); |
|
|
loading: lazy; |
|
|
} |
|
|
.message-content { |
|
|
padding: 12px 18px; |
|
|
background: var(--card-bg); |
|
|
border-radius: 12px; |
|
|
font-size: 1em; |
|
|
transition: var(--transition); |
|
|
box-shadow: var(--shadow); |
|
|
border: 1px solid rgba(0, 0, 0, 0.05); |
|
|
word-wrap: break-word; |
|
|
} |
|
|
body.dark .message-content { |
|
|
background: var(--card-bg-dark); |
|
|
border: 1px solid rgba(255, 255, 255, 0.05); |
|
|
} |
|
|
.message.sent .message-content { |
|
|
border-color: var(--primary); |
|
|
} |
|
|
.message.received .message-content { |
|
|
border-color: var(--secondary); |
|
|
} |
|
|
.message:hover .message-content { |
|
|
transform: translateY(-3px); |
|
|
} |
|
|
.message .sender { |
|
|
color: var(--primary); |
|
|
font-weight: 600; |
|
|
text-decoration: none; |
|
|
} |
|
|
.message .sender:hover { |
|
|
color: var(--accent); |
|
|
} |
|
|
.message .time { |
|
|
font-size: 0.85em; |
|
|
color: rgba(0, 0, 0, 0.5); |
|
|
margin-top: 8px; |
|
|
display: block; |
|
|
} |
|
|
body.dark .message .time { |
|
|
color: rgba(255, 255, 255, 0.5); |
|
|
} |
|
|
.message img, .message video { |
|
|
max-width: 300px; |
|
|
max-height: 300px; |
|
|
width: 100%; |
|
|
border-radius: 10px; |
|
|
margin-top: 10px; |
|
|
display: block; |
|
|
loading: lazy; |
|
|
} |
|
|
.message a { |
|
|
color: var(--primary); |
|
|
text-decoration: none; |
|
|
} |
|
|
.message a:hover { |
|
|
color: var(--accent); |
|
|
} |
|
|
.message-form { |
|
|
margin-top: 20px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 12px; |
|
|
} |
|
|
.post-select { |
|
|
padding: 12px; |
|
|
border-radius: 12px; |
|
|
background: var(--glass-bg); |
|
|
color: var(--text-light); |
|
|
} |
|
|
body.dark .post-select { |
|
|
color: var(--text-dark); |
|
|
} |
|
|
@media (max-width: 900px) { |
|
|
.message-avatar { |
|
|
width: 35px; |
|
|
height: 35px; |
|
|
} |
|
|
.message { |
|
|
max-width: 80%; |
|
|
} |
|
|
} |
|
|
@media (max-width: 480px) { |
|
|
.container { |
|
|
padding: 20px; |
|
|
} |
|
|
.messages { |
|
|
max-height: 400px; |
|
|
} |
|
|
.message-avatar { |
|
|
width: 30px; |
|
|
height: 30px; |
|
|
} |
|
|
.message { |
|
|
max-width: 85%; |
|
|
} |
|
|
.message-content { |
|
|
padding: 10px 15px; |
|
|
font-size: 0.9em; |
|
|
} |
|
|
.message img, .message video { |
|
|
max-width: 200px; |
|
|
max-height: 200px; |
|
|
} |
|
|
} |
|
|
</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 message in chat_messages %} |
|
|
<div class="message {{ 'sent' if message['sender'] == username else 'received' }}"> |
|
|
{% if message['sender'] == username %} |
|
|
{% if avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Your Avatar" class="message-avatar" loading="lazy"> |
|
|
{% else %} |
|
|
<div class="message-avatar" style="background: var(--primary);"></div> |
|
|
{% endif %} |
|
|
{% else %} |
|
|
{% if data['users'][message['sender']].get('avatar') %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ data['users'][message['sender']]['avatar'] }}" alt="{{ message['sender'] }} Avatar" class="message-avatar" loading="lazy"> |
|
|
{% else %} |
|
|
<div class="message-avatar" style="background: var(--secondary);"></div> |
|
|
{% endif %} |
|
|
{% endif %} |
|
|
<div class="message-content"> |
|
|
<a href="{{ url_for('user_profile', username=message['sender']) }}" class="sender">{{ message['sender'] }}</a> |
|
|
<span class="status-dot {{ 'online' if is_user_online(message['sender']) else 'offline' }}"></span> |
|
|
{% if 'text' in message %} |
|
|
<p>{{ message['text'] }}</p> |
|
|
{% endif %} |
|
|
{% if 'file' in message %} |
|
|
{% if message['file_type'] == 'video' %} |
|
|
<video controls preload="metadata" loading="lazy"> |
|
|
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ message['file'] }}" type="video/mp4"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ message['file'] }}" alt="Chat file" loading="lazy" onclick="openModal(this.src)"> |
|
|
{% endif %} |
|
|
{% endif %} |
|
|
{% if 'post_id' in message %} |
|
|
{% with post = posts|selectattr('id', 'equalto', message['post_id'])|first %} |
|
|
{% if post %} |
|
|
<p>Shared: <a href="{{ url_for('post_page', post_id=message['post_id']) }}">{{ post['title'] }}</a></p> |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
{% endif %} |
|
|
<span class="time">{{ message['time'] }}</span> |
|
|
</div> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
{% if is_authenticated %} |
|
|
<form method="POST" enctype="multipart/form-data" class="message-form"> |
|
|
<textarea name="message" placeholder="Type a message" rows="3"></textarea> |
|
|
<input type="file" name="file" accept="video/*,image/*"> |
|
|
<select name="post_id" class="post-select"> |
|
|
<option value="">Share a post (optional)</option> |
|
|
{% for post in user_posts %} |
|
|
<option value="{{ post['id'] }}">{{ post['title'] }}</option> |
|
|
{% endfor %} |
|
|
</select> |
|
|
<button type="submit" class="btn">Send</button> |
|
|
</form> |
|
|
{% else %} |
|
|
<p style="font-size: 1.1em;"><a href="{{ url_for('login') }}">Login</a> to send messages.</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'; |
|
|
} |
|
|
} |
|
|
window.onload = () => { |
|
|
if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark'); |
|
|
const messagesDiv = document.getElementById('messages'); |
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight; |
|
|
}; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
user_posts = [p for p in data['posts'] if p['uploader'] == username] if is_authenticated else [] |
|
|
avatar = data['users'][username].get('avatar') if is_authenticated else None |
|
|
return render_template_string(html, |
|
|
chat_messages=chat_messages, |
|
|
username=username, |
|
|
is_authenticated=is_authenticated, |
|
|
repo_id=REPO_ID, |
|
|
posts=data['posts'], |
|
|
user_posts=user_posts, |
|
|
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), |
|
|
data=data, |
|
|
avatar=avatar) |
|
|
|
|
|
@app.route('/users', methods=['GET', 'POST']) |
|
|
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 |
|
|
|
|
|
search_query = request.form.get('search', '').strip().lower() if request.method == 'POST' else '' |
|
|
user_list = [(user, data['users'][user].get('avatar'), is_user_online(data, user)) |
|
|
for user in data['users'] if not search_query or search_query in user.lower()] |
|
|
|
|
|
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: 900px; |
|
|
background: var(--card-bg); |
|
|
padding: 30px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
} |
|
|
body.dark .container { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2.2em; |
|
|
font-weight: 800; |
|
|
margin-bottom: 25px; |
|
|
background: linear-gradient(135deg, var(--primary), var(--accent)); |
|
|
-webkit-background-clip: text; |
|
|
color: transparent; |
|
|
} |
|
|
.search-container { |
|
|
max-width: 700px; |
|
|
margin: 0 auto 25px; |
|
|
} |
|
|
.search-form { |
|
|
display: flex; |
|
|
gap: 12px; |
|
|
} |
|
|
.search-input { |
|
|
flex-grow: 1; |
|
|
background: var(--glass-bg); |
|
|
border-radius: 12px; |
|
|
box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
.search-btn { |
|
|
padding: 12px 25px; |
|
|
box-shadow: none; |
|
|
} |
|
|
.user-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); |
|
|
gap: 20px; |
|
|
} |
|
|
.user-item { |
|
|
background: var(--card-bg); |
|
|
padding: 15px; |
|
|
border-radius: 16px; |
|
|
box-shadow: var(--shadow); |
|
|
transition: var(--transition); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 15px; |
|
|
} |
|
|
body.dark .user-item { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
.user-item:hover { |
|
|
transform: translateY(-5px); |
|
|
} |
|
|
.user-avatar { |
|
|
width: 60px; |
|
|
height: 60px; |
|
|
border-radius: 50%; |
|
|
object-fit: cover; |
|
|
box-shadow: var(--shadow); |
|
|
loading: lazy; |
|
|
} |
|
|
.user-info { |
|
|
flex-grow: 1; |
|
|
} |
|
|
.user-info a { |
|
|
color: var(--primary); |
|
|
font-weight: 600; |
|
|
text-decoration: none; |
|
|
} |
|
|
.user-info a:hover { |
|
|
color: var(--accent); |
|
|
} |
|
|
.message-btn { |
|
|
background: var(--secondary); |
|
|
padding: 10px 20px; |
|
|
font-size: 0.9em; |
|
|
} |
|
|
.message-btn:hover { |
|
|
background: #00b8c5; |
|
|
} |
|
|
@media (max-width: 480px) { |
|
|
.user-grid { |
|
|
grid-template-columns: 1fr; |
|
|
} |
|
|
.user-avatar { |
|
|
width: 50px; |
|
|
height: 50px; |
|
|
} |
|
|
.user-info a { |
|
|
font-size: 1em; |
|
|
} |
|
|
} |
|
|
</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="search-container"> |
|
|
<form method="POST" class="search-form"> |
|
|
<input type="text" name="search" class="search-input" placeholder="Search users" value="{{ search_query }}"> |
|
|
<button type="submit" class="btn search-btn">🔍</button> |
|
|
</form> |
|
|
</div> |
|
|
<div class="user-grid"> |
|
|
{% for user, avatar, online in user_list %} |
|
|
{% if user != username %} |
|
|
<div class="user-item"> |
|
|
{% if avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="{{ user }} Avatar" class="user-avatar" loading="lazy"> |
|
|
{% else %} |
|
|
<div class="user-avatar" style="background: var(--primary);"></div> |
|
|
{% endif %} |
|
|
<div class="user-info"> |
|
|
<a href="{{ url_for('user_profile', username=user) }}">{{ user }}</a> |
|
|
<span class="status-dot {{ 'online' if online else 'offline' }}"></span> |
|
|
{% if is_authenticated %} |
|
|
<br><a href="{{ url_for('private_chat', recipient=user) }}" class="btn message-btn">Send Message</a> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
{% endif %} |
|
|
{% 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, |
|
|
user_list=user_list, |
|
|
username=username, |
|
|
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, |
|
|
search_query=search_query) |
|
|
|
|
|
@app.route('/messages', methods=['GET']) |
|
|
def messages(): |
|
|
if 'username' not in session: |
|
|
flash('Login to view messages!') |
|
|
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) |
|
|
|
|
|
dialogs = {} |
|
|
for chat_key, messages in data['private_chats'].items(): |
|
|
user1, user2 = chat_key.split('_') |
|
|
if username not in (user1, user2): |
|
|
continue |
|
|
other_user = user1 if user2 == username else user2 |
|
|
last_message = messages[-1] if messages else None |
|
|
unread = sum(1 for msg in messages if datetime.strptime(msg['time'], '%Y-%m-%d %H:%M:%S') > datetime.strptime(data['users'][username]['last_private_visit'], '%Y-%m-%d %H:%M:%S') and msg['sender'] != username) |
|
|
dialogs[other_user] = { |
|
|
'last_message': last_message, |
|
|
'unread': unread, |
|
|
'avatar': data['users'][other_user].get('avatar'), |
|
|
'online': is_user_online(data, other_user) |
|
|
} |
|
|
|
|
|
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>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: 900px; |
|
|
background: var(--card-bg); |
|
|
padding: 30px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
} |
|
|
body.dark .container { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2.2em; |
|
|
font-weight: 800; |
|
|
margin-bottom: 25px; |
|
|
background: linear-gradient(135deg, var(--primary), var(--accent)); |
|
|
-webkit-background-clip: text; |
|
|
color: transparent; |
|
|
} |
|
|
.dialog-list { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 15px; |
|
|
} |
|
|
.dialog-item { |
|
|
background: var(--card-bg); |
|
|
padding: 15px; |
|
|
border-radius: 16px; |
|
|
box-shadow: var(--shadow); |
|
|
transition: var(--transition); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 15px; |
|
|
} |
|
|
body.dark .dialog-item { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
.dialog-item:hover { |
|
|
transform: translateY(-5px); |
|
|
} |
|
|
.dialog-avatar { |
|
|
width: 50px; |
|
|
height: 50px; |
|
|
border-radius: 50%; |
|
|
object-fit: cover; |
|
|
box-shadow: var(--shadow); |
|
|
loading: lazy; |
|
|
} |
|
|
.dialog-info { |
|
|
flex-grow: 1; |
|
|
} |
|
|
.dialog-info a { |
|
|
color: var(--primary); |
|
|
font-weight: 600; |
|
|
text-decoration: none; |
|
|
} |
|
|
.dialog-info a:hover { |
|
|
color: var(--accent); |
|
|
} |
|
|
.dialog-unread { |
|
|
background: var(--secondary); |
|
|
color: white; |
|
|
padding: 5px 10px; |
|
|
border-radius: 12px; |
|
|
font-size: 0.8em; |
|
|
font-weight: 700; |
|
|
} |
|
|
@media (max-width: 480px) { |
|
|
.dialog-avatar { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
} |
|
|
.dialog-info a { |
|
|
font-size: 0.95em; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<button class="theme-toggle" onclick="toggleTheme()">🌙</button> |
|
|
<div class="container"> |
|
|
<h1>Messages</h1> |
|
|
<div class="dialog-list"> |
|
|
{% for other_user, info in dialogs.items() %} |
|
|
<div class="dialog-item"> |
|
|
{% if info.avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ info.avatar }}" alt="{{ other_user }} Avatar" class="dialog-avatar" loading="lazy"> |
|
|
{% else %} |
|
|
<div class="dialog-avatar" style="background: var(--primary);"></div> |
|
|
{% endif %} |
|
|
<div class="dialog-info"> |
|
|
<a href="{{ url_for('private_chat', recipient=other_user) }}">{{ other_user }}</a> |
|
|
<span class="status-dot {{ 'online' if info.online else 'offline' }}"></span> |
|
|
{% if info.last_message %} |
|
|
<p style="font-size: 0.9em; color: rgba(0, 0, 0, 0.5);">{{ info.last_message['time'] }}: |
|
|
{% if 'text' in info.last_message %}{{ info.last_message['text']|truncate(30) }}{% elif 'file' in info.last_message %}File{% elif 'post_id' in info.last_message %}Post{% endif %}</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
{% if info.unread > 0 %} |
|
|
<span class="dialog-unread">{{ info.unread }}</span> |
|
|
{% endif %} |
|
|
</div> |
|
|
{% endfor %} |
|
|
{% if not dialogs %} |
|
|
<p style="font-size: 1.1em;">No messages yet.</p> |
|
|
{% endif %} |
|
|
</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, |
|
|
dialogs=dialogs, |
|
|
username=username, |
|
|
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('/chat/<recipient>', methods=['GET', 'POST']) |
|
|
def private_chat(recipient): |
|
|
if 'username' not in session: |
|
|
flash('Login to start a chat!') |
|
|
return redirect(url_for('login')) |
|
|
|
|
|
data = load_data() |
|
|
username = session['username'] |
|
|
update_last_seen(data, username) |
|
|
if recipient not in data['users']: |
|
|
return "User not found", 404 |
|
|
|
|
|
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) |
|
|
|
|
|
data['users'][username]['last_private_visit'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
|
save_data(data) |
|
|
|
|
|
chat_key = f"{min(username, recipient)}_{max(username, recipient)}" |
|
|
if chat_key not in data['private_chats']: |
|
|
data['private_chats'][chat_key] = [] |
|
|
private_messages = data['private_chats'][chat_key] |
|
|
|
|
|
if request.method == 'POST': |
|
|
message = request.form.get('message') |
|
|
file = request.files.get('file') |
|
|
post_id = request.form.get('post_id') |
|
|
|
|
|
message_data = { |
|
|
'sender': username, |
|
|
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
|
} |
|
|
|
|
|
if message: |
|
|
message_data['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.lower().endswith(('.mp4', '.mov', '.avi')) else 'photo' |
|
|
api = HfApi() |
|
|
file_path = f"chat_files/private/{chat_key}/{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"Uploaded private chat file by {username} to {recipient}" |
|
|
) |
|
|
message_data['file'] = file_path |
|
|
message_data['file_type'] = file_type |
|
|
if os.path.exists(temp_path): |
|
|
os.remove(temp_path) |
|
|
|
|
|
if post_id: |
|
|
post = next((p for p in data['posts'] if p['id'] == post_id and p['uploader'] == username), None) |
|
|
if post: |
|
|
message_data['post_id'] = post_id |
|
|
|
|
|
if 'text' in message_data or 'file' in message_data or 'post_id' in message_data: |
|
|
data['private_chats'][chat_key].append(message_data) |
|
|
save_data(data) |
|
|
|
|
|
return redirect(url_for('private_chat', recipient=recipient)) |
|
|
|
|
|
recipient_avatar = data['users'][recipient].get('avatar') |
|
|
recipient_online = is_user_online(data, recipient) |
|
|
avatar = data['users'][username].get('avatar') if is_authenticated else None |
|
|
|
|
|
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>Chat with {{ recipient }} - 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: 30px; |
|
|
border-radius: 20px; |
|
|
box-shadow: var(--shadow); |
|
|
} |
|
|
body.dark .container { |
|
|
background: var(--card-bg-dark); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2.2em; |
|
|
font-weight: 800; |
|
|
margin-bottom: 25px; |
|
|
background: linear-gradient(135deg, var(--primary), var(--accent)); |
|
|
-webkit-background-clip: text; |
|
|
color: transparent; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 15px; |
|
|
} |
|
|
.recipient-avatar, .message-avatar { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border-radius: 50%; |
|
|
object-fit: cover; |
|
|
box-shadow: var(--shadow); |
|
|
loading: lazy; |
|
|
} |
|
|
.messages { |
|
|
max-height: 500px; |
|
|
overflow-y: auto; |
|
|
padding: 15px; |
|
|
background: var(--glass-bg); |
|
|
border-radius: 14px; |
|
|
margin-bottom: 20px; |
|
|
scroll-behavior: smooth; |
|
|
} |
|
|
.message { |
|
|
display: flex; |
|
|
align-items: flex-start; |
|
|
gap: 12px; |
|
|
margin-bottom: 15px; |
|
|
max-width: 75%; |
|
|
} |
|
|
.message.sent { |
|
|
margin-left: auto; |
|
|
flex-direction: row-reverse; |
|
|
} |
|
|
.message.received { |
|
|
margin-right: auto; |
|
|
} |
|
|
.message-content { |
|
|
padding: 12px 18px; |
|
|
background: var(--card-bg); |
|
|
border-radius: 12px; |
|
|
font-size: 1em; |
|
|
transition: var(--transition); |
|
|
box-shadow: var(--shadow); |
|
|
border: 1px solid rgba(0, 0, 0, 0.05); |
|
|
word-wrap: break-word; |
|
|
} |
|
|
body.dark .message-content { |
|
|
background: var(--card-bg-dark); |
|
|
border: 1px solid rgba(255, 255, 255, 0.05); |
|
|
} |
|
|
.message.sent .message-content { |
|
|
border-color: var(--primary); |
|
|
} |
|
|
.message.received .message-content { |
|
|
border-color: var(--secondary); |
|
|
} |
|
|
.message:hover .message-content { |
|
|
transform: translateY(-3px); |
|
|
} |
|
|
.message .sender { |
|
|
color: var(--primary); |
|
|
font-weight: 600; |
|
|
text-decoration: none; |
|
|
} |
|
|
.message .sender:hover { |
|
|
color: var(--accent); |
|
|
} |
|
|
.message .time { |
|
|
font-size: 0.85em; |
|
|
color: rgba(0, 0, 0, 0.5); |
|
|
margin-top: 8px; |
|
|
display: block; |
|
|
} |
|
|
body.dark .message .time { |
|
|
color: rgba(255, 255, 255, 0.5); |
|
|
} |
|
|
.message img, .message video { |
|
|
max-width: 300px; |
|
|
max-height: 300px; |
|
|
width: 100%; |
|
|
border-radius: 10px; |
|
|
margin-top: 10px; |
|
|
display: block; |
|
|
loading: lazy; |
|
|
} |
|
|
.message a { |
|
|
color: var(--primary); |
|
|
text-decoration: none; |
|
|
} |
|
|
.message a:hover { |
|
|
color: var(--accent); |
|
|
} |
|
|
.message-form { |
|
|
margin-top: 20px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 12px; |
|
|
} |
|
|
.post-select { |
|
|
padding: 12px; |
|
|
border-radius: 12px; |
|
|
background: var(--glass-bg); |
|
|
color: var(--text-light); |
|
|
} |
|
|
body.dark .post-select { |
|
|
color: var(--text-dark); |
|
|
} |
|
|
@media (max-width: 900px) { |
|
|
.message-avatar, .recipient-avatar { |
|
|
width: 35px; |
|
|
height: 35px; |
|
|
} |
|
|
.message { |
|
|
max-width: 80%; |
|
|
} |
|
|
} |
|
|
@media (max-width: 480px) { |
|
|
.container { |
|
|
padding: 20px; |
|
|
} |
|
|
.messages { |
|
|
max-height: 400px; |
|
|
} |
|
|
.message-avatar, .recipient-avatar { |
|
|
width: 30px; |
|
|
height: 30px; |
|
|
} |
|
|
.message { |
|
|
max-width: 85%; |
|
|
} |
|
|
.message-content { |
|
|
padding: 10px 15px; |
|
|
font-size: 0.9em; |
|
|
} |
|
|
.message img, .message video { |
|
|
max-width: 200px; |
|
|
max-height: 200px; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<button class="theme-toggle" onclick="toggleTheme()">🌙</button> |
|
|
<div class="container"> |
|
|
<h1> |
|
|
{% if recipient_avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ recipient_avatar }}" alt="{{ recipient }} Avatar" class="recipient-avatar" loading="lazy"> |
|
|
{% else %} |
|
|
<div class="recipient-avatar" style="background: var(--primary);"></div> |
|
|
{% endif %} |
|
|
Chat with {{ recipient }} <span class="status-dot {{ 'online' if recipient_online else 'offline' }}"></span> |
|
|
</h1> |
|
|
<div class="messages" id="messages"> |
|
|
{% for message in private_messages %} |
|
|
<div class="message {{ 'sent' if message['sender'] == username else 'received' }}"> |
|
|
{% if message['sender'] == username %} |
|
|
{% if avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}" alt="Your Avatar" class="message-avatar" loading="lazy"> |
|
|
{% else %} |
|
|
<div class="message-avatar" style="background: var(--primary);"></div> |
|
|
{% endif %} |
|
|
{% else %} |
|
|
{% if recipient_avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ recipient_avatar }}" alt="{{ recipient }} Avatar" class="message-avatar" loading="lazy"> |
|
|
{% endif %} |
|
|
{% endif %} |
|
|
<div class="message-content"> |
|
|
<a href="{{ url_for('user_profile', username=message['sender']) }}" class="sender">{{ message['sender'] }}</a> |
|
|
<span class="status-dot {{ 'online' if is_user_online(message['sender']) else 'offline' }}"></span> |
|
|
{% if 'text' in message %} |
|
|
<p>{{ message['text'] }}</p> |
|
|
{% endif %} |
|
|
{% if 'file' in message %} |
|
|
{% if message['file_type'] == 'video' %} |
|
|
<video controls preload="metadata" loading="lazy"> |
|
|
<source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ message['file'] }}" type="video/mp4"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ message['file'] }}" alt="Chat file" loading="lazy" onclick="openModal(this.src)"> |
|
|
{% endif %} |
|
|
{% endif %} |
|
|
{% if 'post_id' in message %} |
|
|
{% with post = posts|selectattr('id', 'equalto', message['post_id'])|first %} |
|
|
{% if post %} |
|
|
<p>Shared: <a href="{{ url_for('post_page', post_id=message['post_id']) }}">{{ post['title'] }}</a></p> |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
{% endif %} |
|
|
<span class="time">{{ message['time'] }}</span> |
|
|
</div> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
<form method="POST" enctype="multipart/form-data" class="message-form"> |
|
|
<textarea name="message" placeholder="Type a message" rows="3"></textarea> |
|
|
<input type="file" name="file" accept="video/*,image/*"> |
|
|
<select name="post_id" class="post-select"> |
|
|
<option value="">Share a post (optional)</option> |
|
|
{% for post in user_posts %} |
|
|
<option value="{{ post['id'] }}">{{ post['title'] }}</option> |
|
|
{% endfor %} |
|
|
</select> |
|
|
<button type="submit" class="btn">Send</button> |
|
|
</form> |
|
|
</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 messagesDiv = document.getElementById('messages'); |
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight; |
|
|
}; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
user_posts = [p for p in data['posts'] if p['uploader'] == username] |
|
|
return render_template_string(html, |
|
|
private_messages=private_messages, |
|
|
recipient=recipient, |
|
|
username=username, |
|
|
is_authenticated=is_authenticated, |
|
|
repo_id=REPO_ID, |
|
|
posts=data['posts'], |
|
|
user_posts=user_posts, |
|
|
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), |
|
|
recipient_avatar=recipient_avatar, |
|
|
recipient_online=recipient_online, |
|
|
avatar=avatar) |
|
|
|
|
|
if __name__ == '__main__': |
|
|
threading.Thread(target=periodic_backup, daemon=True).start() |
|
|
app.run(debug=False, host='0.0.0.0', port=7860) |