| from flask import Flask, render_template_string, request, redirect, url_for, session, flash |
| 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/w1f9" |
| HF_TOKEN_WRITE = os.getenv("HF_TOKEN") |
| HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE |
|
|
| cache = Cache(app, config={'CACHE_TYPE': 'simple'}) |
| logging.basicConfig(level=logging.INFO) |
|
|
| @cache.memoize(timeout=300) |
| def load_data(): |
| try: |
| download_db_from_hf() |
| with open(DATA_FILE, 'r', encoding='utf-8') as file: |
| data = json.load(file) |
| if not isinstance(data, dict): |
| logging.warning("Данные не в формате dict, инициализация пустой базы") |
| return {'posts': [], 'users': {}, 'general_chat': [], 'private_chats': {}} |
| data.setdefault('posts', []) |
| data.setdefault('users', {}) |
| data.setdefault('general_chat', []) |
| data.setdefault('private_chats', {}) |
| for user in data['users']: |
| data['users'][user].setdefault('last_chat_visit', '1970-01-01 00:00:00') |
| data['users'][user].setdefault('last_private_visit', '1970-01-01 00:00:00') |
| data['users'][user].setdefault('last_seen', '1970-01-01 00:00:00') |
| logging.info("Данные успешно загружены") |
| return data |
| except Exception as e: |
| logging.error(f"Ошибка при загрузке данных: {e}") |
| return {'posts': [], 'users': {}, 'general_chat': [], 'private_chats': {}} |
|
|
| def save_data(data): |
| try: |
| with open(DATA_FILE, 'w', encoding='utf-8') as file: |
| json.dump(data, file, ensure_ascii=False, indent=4) |
| upload_db_to_hf() |
| cache.clear() |
| logging.info("Данные сохранены и загружены на HF") |
| except Exception as e: |
| logging.error(f"Ошибка при сохранении данных: {e}") |
| raise |
|
|
| def upload_db_to_hf(): |
| try: |
| api = HfApi() |
| api.upload_file( |
| path_or_fileobj=DATA_FILE, |
| path_in_repo=DATA_FILE, |
| repo_id=REPO_ID, |
| repo_type="dataset", |
| token=HF_TOKEN_WRITE, |
| commit_message=f"Бэкап {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" |
| ) |
| logging.info("База данных загружена на Hugging Face") |
| except Exception as e: |
| logging.error(f"Ошибка при загрузке базы данных: {e}") |
|
|
| def download_db_from_hf(): |
| try: |
| hf_hub_download( |
| repo_id=REPO_ID, |
| filename=DATA_FILE, |
| repo_type="dataset", |
| token=HF_TOKEN_READ, |
| local_dir=".", |
| local_dir_use_symlinks=False |
| ) |
| logging.info("База данных скачана с Hugging Face") |
| except Exception as e: |
| logging.error(f"Ошибка при скачивании базы данных: {e}") |
| if not os.path.exists(DATA_FILE): |
| with open(DATA_FILE, 'w', encoding='utf-8') as f: |
| json.dump({'posts': [], 'users': {}, 'general_chat': [], 'private_chats': {}}, f) |
|
|
| def periodic_backup(): |
| while True: |
| upload_db_to_hf() |
| time.sleep(1800) |
|
|
| def get_unread_count(data, username): |
| if username not in data['users']: |
| return 0 |
| last_visit = datetime.strptime(data['users'][username]['last_chat_visit'], '%Y-%m-%d %H:%M:%S') |
| return sum(1 for msg in data['general_chat'] if datetime.strptime(msg['time'], '%Y-%m-%d %H:%M:%S') > last_visit) |
|
|
| def get_private_unread_count(data, username): |
| if username not in data['users']: |
| return 0 |
| last_visit = datetime.strptime(data['users'][username]['last_private_visit'], '%Y-%m-%d %H:%M:%S') |
| unread = 0 |
| for chat_key, messages in data['private_chats'].items(): |
| users = chat_key.split('_') |
| if username in users: |
| unread += sum(1 for msg in messages if datetime.strptime(msg['time'], '%Y-%m-%d %H:%M:%S') > last_visit and msg['sender'] != username) |
| return unread |
|
|
| def is_user_online(data, username): |
| if username not in data['users']: |
| return False |
| last_seen = datetime.strptime(data['users'][username]['last_seen'], '%Y-%m-%d %H:%M:%S') |
| return (datetime.now() - last_seen).total_seconds() < 300 |
|
|
| def update_last_seen(data, username): |
| if username in data['users']: |
| data['users'][username]['last_seen'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
| save_data(data) |
|
|
| BASE_STYLE = ''' |
| :root { |
| --primary: #ff4d6d; |
| --secondary: #00ddeb; |
| --accent: #8b5cf6; |
| --background-light: #fef9f1; |
| --background-dark: #1f1a44; |
| --card-bg: rgba(255, 255, 255, 0.9); |
| --card-bg-dark: rgba(50, 45, 80, 0.9); |
| --text-light: #2a1e5a; |
| --text-dark: #e8e1ff; |
| --shadow: 0 8px 24px rgba(0, 0, 0, 0.15); |
| --glass-bg: rgba(255, 255, 255, 0.1); |
| --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; |
| transition: var(--transition); |
| overflow-x: hidden; |
| } |
| body.dark { |
| background: var(--background-dark); |
| color: var(--text-dark); |
| } |
| .sidebar { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 280px; |
| height: 100%; |
| background: var(--glass-bg); |
| backdrop-filter: blur(15px); |
| padding: 20px; |
| box-shadow: var(--shadow); |
| z-index: 1000; |
| transition: transform var(--transition); |
| } |
| .sidebar.hidden { |
| transform: translateX(-100%); |
| } |
| .sidebar-header { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| margin-bottom: 30px; |
| } |
| .nav-brand { |
| font-size: 1.6em; |
| font-weight: 800; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| .logo { |
| width: 36px; |
| height: 36px; |
| border-radius: 12px; |
| box-shadow: var(--shadow); |
| } |
| .nav-links { |
| display: flex; |
| flex-direction: column; |
| gap: 12px; |
| } |
| .nav-link { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| padding: 12px 20px; |
| background: var(--card-bg); |
| color: var(--text-light); |
| text-decoration: none; |
| border-radius: 12px; |
| font-size: 1em; |
| font-weight: 500; |
| transition: var(--transition); |
| position: relative; |
| } |
| body.dark .nav-link { |
| background: var(--card-bg-dark); |
| color: var(--text-dark); |
| } |
| .nav-link:hover { |
| transform: scale(1.02); |
| background: var(--primary); |
| color: white; |
| box-shadow: 0 4px 16px rgba(255, 77, 109, 0.3); |
| } |
| .nav-link .badge { |
| position: absolute; |
| right: 12px; |
| background: var(--secondary); |
| color: white; |
| padding: 3px 8px; |
| border-radius: 10px; |
| font-size: 0.75em; |
| font-weight: 700; |
| } |
| .logout-btn { |
| background: var(--secondary); |
| color: white; |
| } |
| .logout-btn:hover { |
| background: #00b8c5; |
| } |
| .menu-btn { |
| display: none; |
| font-size: 24px; |
| background: var(--glass-bg); |
| border: none; |
| color: var(--primary); |
| cursor: pointer; |
| position: fixed; |
| top: 15px; |
| left: 15px; |
| z-index: 1001; |
| padding: 10px; |
| border-radius: 50%; |
| box-shadow: var(--shadow); |
| transition: var(--transition); |
| } |
| .menu-btn:hover { |
| background: var(--primary); |
| color: white; |
| } |
| .container { |
| margin: 20px auto 20px 300px; |
| max-width: 1200px; |
| padding: 20px; |
| transition: var(--transition); |
| } |
| .btn { |
| padding: 12px 24px; |
| background: var(--primary); |
| color: white; |
| border: none; |
| border-radius: 12px; |
| cursor: pointer; |
| font-size: 1em; |
| font-weight: 600; |
| transition: var(--transition); |
| display: inline-flex; |
| align-items: center; |
| gap: 8px; |
| box-shadow: var(--shadow); |
| } |
| .btn:hover { |
| transform: scale(1.03); |
| background: #e6415f; |
| box-shadow: 0 6px 20px rgba(255, 77, 109, 0.4); |
| } |
| input, textarea, select { |
| width: 100%; |
| padding: 12px; |
| margin: 10px 0; |
| border: none; |
| border-radius: 12px; |
| background: var(--glass-bg); |
| color: var(--text-light); |
| font-size: 1em; |
| transition: var(--transition); |
| box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05); |
| } |
| 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.15); |
| box-shadow: 0 0 0 3px var(--primary); |
| } |
| .modal { |
| display: none; |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0, 0, 0, 0.8); |
| z-index: 2000; |
| justify-content: center; |
| align-items: center; |
| transition: opacity var(--transition); |
| } |
| .modal img, .modal video { |
| max-width: 90%; |
| max-height: 90%; |
| object-fit: contain; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| animation: zoomIn 0.3s ease; |
| } |
| .theme-toggle { |
| position: fixed; |
| top: 15px; |
| right: 15px; |
| background: var(--glass-bg); |
| border: none; |
| padding: 10px; |
| border-radius: 50%; |
| cursor: pointer; |
| font-size: 20px; |
| box-shadow: var(--shadow); |
| transition: var(--transition); |
| } |
| .theme-toggle:hover { |
| transform: rotate(90deg); |
| background: var(--accent); |
| color: white; |
| } |
| .status-dot { |
| width: 8px; |
| height: 8px; |
| border-radius: 50%; |
| display: inline-block; |
| margin-left: 5px; |
| } |
| .online { background: var(--online); } |
| .offline { background: var(--offline); } |
| @keyframes zoomIn { |
| from { opacity: 0; transform: scale(0.95); } |
| to { opacity: 1; transform: scale(1); } |
| } |
| @media (max-width: 900px) { |
| .sidebar { |
| width: 100%; |
| max-width: 280px; |
| transform: translateX(-100%); |
| } |
| .sidebar.active { |
| transform: translateX(0); |
| } |
| .menu-btn { |
| display: block; |
| } |
| .container { |
| margin: 70px 15px 20px 15px; |
| max-width: calc(100% - 30px); |
| } |
| .theme-toggle { |
| top: 70px; |
| } |
| } |
| @media (max-width: 480px) { |
| .nav-brand { font-size: 1.4em; } |
| .nav-link { font-size: 0.9em; padding: 10px 16px; } |
| .btn { padding: 10px 18px; font-size: 0.9em; } |
| } |
| ''' |
|
|
| NAV_HTML = ''' |
| <aside class="sidebar" id="sidebar"> |
| <div class="sidebar-header"> |
| <img src="https://cdn-avatars.huggingface.co/v1/production/uploads/673b00f35373479538ac373c/W_dumUND8K6IlMxVmpUgS.jpeg" alt="Logo" class="logo"> |
| <span class="nav-brand">ADUSIS QoSHUB</span> |
| </div> |
| <nav class="nav-links"> |
| <a href="{{ url_for('feed') }}" class="nav-link"><span>📜</span> Feed</a> |
| {% if is_authenticated %} |
| <a href="{{ url_for('profile') }}" class="nav-link"><span>👤</span> Profile ({{ username }}) <span class="status-dot {{ 'online' if is_online else 'offline' }}"></span></a> |
| <a href="{{ url_for('upload') }}" class="nav-link"><span>⬆️</span> Upload</a> |
| <a href="{{ url_for('chat') }}" class="nav-link"><span>💬</span> Chat {% if unread_count > 0 %}<span class="badge">{{ unread_count }}</span>{% endif %}</a> |
| <a href="{{ url_for('users') }}" class="nav-link"><span>👥</span> Users <span class="badge">{{ user_count }}</span></a> |
| <a href="{{ url_for('messages') }}" class="nav-link"><span>✉️</span> Messages {% if private_unread_count > 0 %}<span class="badge">{{ private_unread_count }}</span>{% endif %}</a> |
| <a href="{{ url_for('logout') }}" class="nav-link logout-btn"><span>🚪</span> Logout</a> |
| {% else %} |
| <a href="{{ url_for('login') }}" class="nav-link"><span>🔑</span> Login</a> |
| <a href="{{ url_for('register') }}" class="nav-link"><span>✨</span> Register</a> |
| <a href="{{ url_for('chat') }}" class="nav-link"><span>💬</span> Chat (View Only)</a> |
| <a href="{{ url_for('users') }}" class="nav-link"><span>👥</span> Users <span class="badge">{{ user_count }}</span></a> |
| {% endif %} |
| </nav> |
| </aside> |
| ''' |
|
|
| @app.route('/register', methods=['GET', 'POST']) |
| def register(): |
| if request.method == 'POST': |
| username = request.form.get('username') |
| password = request.form.get('password') |
| data = load_data() |
| |
| if username in data['users']: |
| flash('Пользователь уже существует!') |
| return redirect(url_for('register')) |
| |
| data['users'][username] = { |
| 'password': password, |
| 'bio': '', |
| 'link': '', |
| 'avatar': None, |
| 'last_chat_visit': '1970-01-01 00:00:00', |
| 'last_private_visit': '1970-01-01 00:00:00', |
| 'last_seen': datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
| } |
| save_data(data) |
| flash('Регистрация успешна! Пожалуйста, войдите.') |
| return redirect(url_for('login')) |
| |
| is_authenticated = 'username' in session |
| username = session.get('username', None) |
| data = load_data() |
| unread_count = get_unread_count(data, username) if is_authenticated else 0 |
| private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0 |
| user_count = len(data['users']) |
| is_online = is_user_online(data, username) if is_authenticated else False |
| |
| html = ''' |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock"> |
| <meta name="google-site-verification" content="V6EqDTA9Oj9V1OfNbnBHj5RKrdcXlABD8tqfEFVUHJY" /> |
| <title>Register - Content Hub</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> |
| <style> |
| ''' + BASE_STYLE + ''' |
| .container { |
| max-width: 450px; |
| background: var(--card-bg); |
| padding: 30px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| animation: slideUp 0.4s ease; |
| } |
| body.dark .container { |
| background: var(--card-bg-dark); |
| } |
| h1 { |
| font-size: 1.8em; |
| font-weight: 800; |
| text-align: center; |
| margin-bottom: 20px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| .flash { |
| color: var(--secondary); |
| text-align: center; |
| margin-bottom: 12px; |
| font-size: 0.9em; |
| font-weight: 600; |
| } |
| .link { |
| text-align: center; |
| margin-top: 15px; |
| color: var(--primary); |
| font-size: 0.9em; |
| text-decoration: none; |
| font-weight: 600; |
| transition: var(--transition); |
| } |
| .link:hover { |
| color: var(--accent); |
| } |
| @keyframes slideUp { |
| from { opacity: 0; transform: translateY(20px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| </style> |
| </head> |
| <body> |
| <button class="menu-btn" onclick="toggleSidebar()">☰</button> |
| ''' + NAV_HTML + ''' |
| <button class="theme-toggle" onclick="toggleTheme()">🌙</button> |
| <div class="container"> |
| <h1>Register</h1> |
| {% with messages = get_flashed_messages() %} |
| {% if messages %} |
| {% for message in messages %} |
| <div class="flash">{{ message }}</div> |
| {% endfor %} |
| {% endif %} |
| {% endwith %} |
| <form method="POST"> |
| <input type="text" name="username" placeholder="Username" required> |
| <input type="password" name="password" placeholder="Password" required> |
| <button type="submit" class="btn">Register</button> |
| </form> |
| <p class="link"><a href="{{ url_for('login') }}">Already have an account? Login</a></p> |
| </div> |
| <script> |
| function toggleSidebar() { |
| document.getElementById('sidebar').classList.toggle('active'); |
| } |
| function toggleTheme() { |
| document.body.classList.toggle('dark'); |
| localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light'); |
| } |
| window.onload = () => { |
| if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark'); |
| }; |
| </script> |
| </body> |
| </html> |
| ''' |
| return render_template_string(html, is_authenticated=is_authenticated, username=username, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online) |
|
|
| @app.route('/login', methods=['GET', 'POST']) |
| def login(): |
| data = load_data() |
| if request.method == 'POST': |
| username = request.form.get('username') |
| password = request.form.get('password') |
| |
| if username in data['users'] and data['users'][username]['password'] == password: |
| session['username'] = username |
| session.permanent = True |
| update_last_seen(data, username) |
| return redirect(url_for('feed')) |
| flash('Неверное имя пользователя или пароль!') |
| return redirect(url_for('login')) |
| |
| is_authenticated = 'username' in session |
| username = session.get('username', None) |
| if is_authenticated: |
| update_last_seen(data, username) |
| unread_count = get_unread_count(data, username) if is_authenticated else 0 |
| private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0 |
| user_count = len(data['users']) |
| is_online = is_user_online(data, username) if is_authenticated else False |
| |
| html = ''' |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock"> |
| <title>Login - Content Hub</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> |
| <style> |
| ''' + BASE_STYLE + ''' |
| .container { |
| max-width: 450px; |
| background: var(--card-bg); |
| padding: 30px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| animation: slideUp 0.4s ease; |
| } |
| body.dark .container { |
| background: var(--card-bg-dark); |
| } |
| h1 { |
| font-size: 1.8em; |
| font-weight: 800; |
| text-align: center; |
| margin-bottom: 20px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| .flash { |
| color: var(--secondary); |
| text-align: center; |
| margin-bottom: 12px; |
| font-size: 0.9em; |
| font-weight: 600; |
| } |
| .link { |
| text-align: center; |
| margin-top: 15px; |
| color: var(--primary); |
| font-size: 0.9em; |
| text-decoration: none; |
| font-weight: 600; |
| transition: var(--transition); |
| } |
| .link:hover { |
| color: var(--accent); |
| } |
| @keyframes slideUp { |
| from { opacity: 0; transform: translateY(20px); } |
| 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('/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 '', 204 |
| return 'Пост не найден', 404 |
|
|
| @app.route('/', methods=['GET', 'POST']) |
| def feed(): |
| data = load_data() |
| username = session.get('username', None) |
| if username: |
| update_last_seen(data, username) |
| posts = sorted(data.get('posts', []), key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True) |
| is_authenticated = 'username' in session |
| unread_count = get_unread_count(data, username) if is_authenticated else 0 |
| private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0 |
| user_count = len(data['users']) |
| is_online = is_user_online(data, username) if is_authenticated else False |
|
|
| if request.method == 'POST' and is_authenticated: |
| post_id = request.form.get('post_id') |
| post = next((p for p in data['posts'] if p['id'] == post_id), None) |
| if not post: |
| return "Пост не найден", 404 |
| |
| if 'like' in request.form: |
| if username not in post.get('likes', []): |
| post['likes'] = post.get('likes', []) + [username] |
| else: |
| post['likes'] = [u for u in post.get('likes', []) if u != username] |
| elif 'comment' in request.form: |
| comment_text = request.form.get('comment') |
| if comment_text: |
| post['comments'] = post.get('comments', []) + [{ |
| 'user': username, |
| 'text': comment_text, |
| 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
| }] |
| elif 'share' in request.form: |
| share_to = request.form.get('share_to') |
| if share_to == 'chat': |
| data['general_chat'].append({ |
| 'sender': username, |
| 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), |
| 'post_id': post_id |
| }) |
| elif share_to in data['users']: |
| chat_key = f"{min(username, share_to)}_{max(username, share_to)}" |
| if chat_key not in data['private_chats']: |
| data['private_chats'][chat_key] = [] |
| data['private_chats'][chat_key].append({ |
| 'sender': username, |
| 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), |
| 'post_id': post_id |
| }) |
| save_data(data) |
| return redirect(url_for('feed')) |
|
|
| html = ''' |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock"> |
| <meta name="google-site-verification" content="V6EqDTA9Oj9V1OfNbnBHj5RKrdcXlABD8tqfEFVUHJY" /> |
| <title>Adusis - QoS, BBC, BNWO HUB</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> |
| <style> |
| ''' + BASE_STYLE + ''' |
| body, html { |
| height: 100%; |
| margin: 0; |
| padding: 0; |
| overflow: hidden; |
| } |
| .feed-container { |
| height: 100vh; |
| width: 100%; |
| overflow-y: scroll; |
| scroll-snap-type: y mandatory; |
| margin-left: 0; |
| padding: 0; |
| } |
| .post-container { |
| height: 100vh; |
| width: 100%; |
| position: relative; |
| scroll-snap-align: start; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: var(--background-light); |
| } |
| 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; |
| } |
| .post-overlay { |
| position: absolute; |
| bottom: 20px; |
| left: 20px; |
| right: 20px; |
| z-index: 2; |
| color: white; |
| text-shadow: 0 2px 4px rgba(0,0,0,0.8); |
| } |
| .post-actions { |
| position: absolute; |
| right: 20px; |
| bottom: 100px; |
| display: flex; |
| flex-direction: column; |
| gap: 20px; |
| z-index: 3; |
| } |
| .action-btn { |
| background: rgba(255, 255, 255, 0.2); |
| border: none; |
| border-radius: 50%; |
| width: 50px; |
| height: 50px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| transition: var(--transition); |
| color: white; |
| font-size: 24px; |
| backdrop-filter: blur(5px); |
| } |
| .action-btn:hover { |
| background: var(--primary); |
| } |
| .action-btn.liked { |
| color: var(--primary); |
| } |
| .action-count { |
| color: white; |
| text-align: center; |
| font-size: 14px; |
| margin-top: 5px; |
| text-shadow: 0 2px 4px rgba(0,0,0,0.8); |
| } |
| .comment-section { |
| display: none; |
| position: absolute; |
| bottom: 150px; |
| left: 20px; |
| right: 20px; |
| background: rgba(0, 0, 0, 0.7); |
| padding: 15px; |
| border-radius: 12px; |
| max-height: 50vh; |
| overflow-y: auto; |
| z-index: 4; |
| } |
| .comment-section.active { |
| display: block; |
| } |
| .comment { |
| margin-bottom: 10px; |
| font-size: 14px; |
| color: white; |
| } |
| .comment-form { |
| display: flex; |
| gap: 10px; |
| margin-top: 10px; |
| } |
| .comment-form textarea { |
| flex-grow: 1; |
| height: 40px; |
| background: rgba(255, 255, 255, 0.2); |
| color: white; |
| } |
| .share-options { |
| display: none; |
| position: absolute; |
| bottom: 250px; |
| right: 20px; |
| background: rgba(0, 0, 0, 0.7); |
| padding: 10px; |
| border-radius: 12px; |
| z-index: 4; |
| } |
| .share-options.active { |
| display: block; |
| } |
| .share-option { |
| background: none; |
| border: none; |
| color: white; |
| padding: 8px; |
| cursor: pointer; |
| width: 100%; |
| text-align: left; |
| } |
| .share-option:hover { |
| background: var(--primary); |
| } |
| .username-link { |
| color: var(--primary); |
| font-weight: 600; |
| text-decoration: none; |
| } |
| .username-link:hover { |
| color: var(--accent); |
| } |
| @media (max-width: 480px) { |
| .post-overlay { |
| bottom: 10px; |
| left: 10px; |
| right: 10px; |
| } |
| .post-actions { |
| bottom: 80px; |
| right: 10px; |
| } |
| .action-btn { |
| width: 40px; |
| height: 40px; |
| font-size: 20px; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <button class="menu-btn" onclick="toggleSidebar()">☰</button> |
| ''' + NAV_HTML + ''' |
| <button class="theme-toggle" onclick="toggleTheme()">🌙</button> |
| <div class="feed-container"> |
| {% for post in posts %} |
| <div class="post-container" id="post-{{ post['id'] }}"> |
| {% if post['type'] == 'video' %} |
| <video class="post-media" autoplay loop muted playsinline> |
| <source src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ post['type'] }}s/{{ post['filename'] }}" type="video/mp4"> |
| </video> |
| {% 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(100) }}</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"> |
| <form method="POST" style="display: inline;"> |
| <input type="hidden" name="post_id" value="{{ post['id'] }}"> |
| <button type="submit" name="like" class="action-btn {% if username in post['likes'] %}liked{% endif %}" onclick="event.preventDefault(); this.form.submit();"> |
| ♥ |
| </button> |
| </form> |
| <span class="action-count">{{ 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="toggleShare('{{ post['id'] }}')">➤</button> |
| <span class="action-count">{{ 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"> |
| <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'] }}"> |
| {% if is_authenticated %} |
| <form method="POST"> |
| <input type="hidden" name="post_id" value="{{ post['id'] }}"> |
| <button type="submit" name="share" value="chat" class="share-option">Share to Chat</button> |
| </form> |
| {% for user in data['users']|sort %} |
| {% if user != username %} |
| <form method="POST"> |
| <input type="hidden" name="post_id" value="{{ post['id'] }}"> |
| <button type="submit" name="share" value="{{ user }}" class="share-option">Send to {{ user }}</button> |
| </form> |
| {% endif %} |
| {% endfor %} |
| {% endif %} |
| </div> |
| </div> |
| {% endfor %} |
| </div> |
| <script> |
| function toggleSidebar() { |
| document.getElementById('sidebar').classList.toggle('active'); |
| } |
| function toggleTheme() { |
| document.body.classList.toggle('dark'); |
| localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light'); |
| } |
| function toggleComments(postId) { |
| const comments = document.getElementById('comments-' + postId); |
| comments.classList.toggle('active'); |
| fetch('/increment_view/' + postId, { method: 'POST' }); |
| } |
| function toggleShare(postId) { |
| const shareOptions = document.getElementById('share-' + postId); |
| shareOptions.classList.toggle('active'); |
| } |
| window.onload = () => { |
| if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark'); |
| const videos = document.querySelectorAll('.post-media'); |
| videos.forEach(video => { |
| if (video.tagName === 'VIDEO') { |
| const observer = new IntersectionObserver((entries) => { |
| entries.forEach(entry => { |
| if (entry.isIntersecting) { |
| video.play(); |
| } else { |
| video.pause(); |
| } |
| }); |
| }, { threshold: 0.5 }); |
| observer.observe(video); |
| } |
| }); |
| }; |
| </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 "Пост не найден", 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 'like' in request.form: |
| if username not in post.get('likes', []): |
| post['likes'] = post.get('likes', []) + [username] |
| else: |
| post['likes'] = [user for user in post.get('likes', []) if user != username] |
| save_data(data) |
| elif 'comment' in request.form: |
| comment_text = request.form.get('comment') |
| if comment_text: |
| post['comments'] = post.get('comments', []) + [{'user': username, 'text': comment_text, 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] |
| save_data(data) |
|
|
| html = ''' |
| <!DOCTYPE html> |
| <html lang="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: 800px; |
| background: var(--card-bg); |
| padding: 25px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| } |
| body.dark .container { |
| background: var(--card-bg-dark); |
| } |
| h1 { |
| font-size: 2em; |
| font-weight: 800; |
| margin-bottom: 20px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| video, img { |
| width: 100%; |
| max-height: 450px; |
| object-fit: cover; |
| border-radius: 12px; |
| box-shadow: var(--shadow); |
| margin-bottom: 20px; |
| loading: lazy; |
| } |
| p { |
| font-size: 1em; |
| margin-bottom: 12px; |
| } |
| .like-btn.liked { |
| background: var(--secondary); |
| } |
| .like-btn.liked:hover { |
| background: #00b8c5; |
| } |
| .comment-section { |
| margin-top: 25px; |
| } |
| .comment { |
| background: var(--glass-bg); |
| padding: 12px; |
| border-radius: 12px; |
| margin-bottom: 12px; |
| font-size: 0.9em; |
| } |
| .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: 300px; |
| } |
| h1 { |
| font-size: 1.6em; |
| } |
| } |
| </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 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: {{ post['views'] }} | Likes: {{ post['likes']|length }}</p> |
| {% if is_authenticated %} |
| <form method="POST" style="display: inline;"> |
| <button type="submit" name="like" class="btn like-btn {% if username in post['likes'] %}liked{% endif %}"> |
| {% if username in post['likes'] %}Unlike{% else %}Like{% endif %} |
| </button> |
| </form> |
| <form method="POST" class="comment-section"> |
| <textarea name="comment" placeholder="Leave a comment" rows="2"></textarea> |
| <button type="submit" class="btn">Submit</button> |
| </form> |
| {% endif %} |
| <h3 style="margin-top: 25px; font-size: 1.4em;">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: 12px;"><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'; |
| } |
| } |
| window.onload = () => { |
| if (localStorage.getItem('theme') === 'dark') document.body.classList.add('dark'); |
| }; |
| </script> |
| </body> |
| </html> |
| ''' |
| return render_template_string(html, post=post, repo_id=REPO_ID, is_authenticated=is_authenticated, username=username, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online, is_user_online=lambda u: is_user_online(data, u)) |
|
|
| @app.route('/profile', methods=['GET', 'POST']) |
| def profile(): |
| if 'username' not in session: |
| flash('Войдите, чтобы просмотреть свой профиль!') |
| return redirect(url_for('login')) |
| |
| data = load_data() |
| username = session['username'] |
| update_last_seen(data, username) |
| user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True) |
| is_authenticated = 'username' in session |
| unread_count = get_unread_count(data, username) |
| private_unread_count = get_private_unread_count(data, username) |
| user_count = len(data['users']) |
| is_online = is_user_online(data, username) |
| |
| total_views = sum(post.get('views', 0) for post in user_posts) |
| total_likes = sum(len(post.get('likes', [])) for post in user_posts) |
| user_data = data['users'].get(username, {}) |
| bio = user_data.get('bio', '') |
| link = user_data.get('link', '') |
| avatar = user_data.get('avatar', None) |
| last_seen = user_data.get('last_seen', 'Never') |
|
|
| if request.method == 'POST': |
| if 'delete_post' in request.form: |
| post_id = request.form.get('post_id') |
| if post_id: |
| data['posts'] = [p for p in data['posts'] if p['id'] != post_id or p['uploader'] != username] |
| save_data(data) |
| return redirect(url_for('profile')) |
| elif 'update_profile' in request.form: |
| bio = request.form.get('bio', '') |
| link = request.form.get('link', '') |
| avatar_file = request.files.get('avatar') |
| if avatar_file and avatar_file.filename: |
| filename = secure_filename(avatar_file.filename) |
| temp_path = os.path.join('uploads', filename) |
| os.makedirs('uploads', exist_ok=True) |
| avatar_file.save(temp_path) |
| api = HfApi() |
| avatar_path = f"avatars/{username}/{filename}" |
| api.upload_file( |
| path_or_fileobj=temp_path, |
| path_in_repo=avatar_path, |
| repo_id=REPO_ID, |
| repo_type="dataset", |
| token=HF_TOKEN_WRITE, |
| commit_message=f"Загружен аватар для {username}" |
| ) |
| data['users'][username]['avatar'] = avatar_path |
| if os.path.exists(temp_path): |
| os.remove(temp_path) |
| data['users'][username]['bio'] = bio |
| data['users'][username]['link'] = link |
| save_data(data) |
| return redirect(url_for('profile')) |
|
|
| html = ''' |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <meta name="keywords" content="interracial porn, bbc porn, qos, queen of spades, big black cock"> |
| <title>Profile - {{ username }} - Content Hub</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> |
| <style> |
| ''' + BASE_STYLE + ''' |
| .container { |
| max-width: 900px; |
| } |
| .profile-header { |
| display: flex; |
| gap: 20px; |
| background: var(--card-bg); |
| padding: 25px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| margin-bottom: 25px; |
| } |
| body.dark .profile-header { |
| background: var(--card-bg-dark); |
| } |
| .avatar { |
| width: 100px; |
| height: 100px; |
| border-radius: 50%; |
| object-fit: cover; |
| box-shadow: var(--shadow); |
| transition: var(--transition); |
| } |
| .avatar:hover { |
| transform: scale(1.03); |
| } |
| .profile-info h1 { |
| font-size: 2em; |
| font-weight: 800; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| margin-bottom: 12px; |
| } |
| .profile-info p { |
| font-size: 1em; |
| margin-bottom: 8px; |
| } |
| .post-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
| gap: 20px; |
| } |
| .post-item { |
| background: var(--card-bg); |
| padding: 15px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| transition: var(--transition); |
| } |
| body.dark .post-item { |
| background: var(--card-bg-dark); |
| } |
| .post-item:hover { |
| transform: translateY(-5px); |
| } |
| .post-preview { |
| width: 100%; |
| height: 200px; |
| object-fit: cover; |
| border-radius: 12px; |
| margin-bottom: 12px; |
| loading: lazy; |
| } |
| .post-item h3 { |
| font-size: 1.3em; |
| font-weight: 600; |
| margin-bottom: 8px; |
| } |
| .delete-btn { |
| background: var(--secondary); |
| } |
| .delete-btn:hover { |
| background: #00b8c5; |
| } |
| .share-btn { |
| background: var(--accent); |
| } |
| .share-btn:hover { |
| background: #7a4de0; |
| } |
| h2 { |
| font-size: 1.6em; |
| font-weight: 800; |
| margin: 25px 0 15px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| @media (max-width: 480px) { |
| .profile-header { |
| flex-direction: column; |
| text-align: center; |
| } |
| .avatar { |
| width: 80px; |
| height: 80px; |
| } |
| .post-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| </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"> |
| {% endif %} |
| <div class="profile-info"> |
| <h1>{{ username }} <span class="status-dot {{ 'online' if is_online else 'offline' }}"></span></h1> |
| {% if not is_online %} |
| <p style="color: rgba(0, 0, 0, 0.5);">Last seen: {{ last_seen }}</p> |
| {% endif %} |
| <p>{{ bio }}</p> |
| {% if link %} |
| <p><a href="{{ link }}" target="_blank" style="color: var(--primary);">{{ link }}</a></p> |
| {% endif %} |
| <p>Views: {{ total_views }} | Likes: {{ total_likes }}</p> |
| <button class="btn share-btn" onclick="copyProfileLink()">Share</button> |
| </div> |
| </div> |
| <h2>Edit Profile</h2> |
| <form method="POST" enctype="multipart/form-data"> |
| <textarea name="bio" placeholder="Profile Bio" rows="2">{{ bio }}</textarea> |
| <input type="text" name="link" placeholder="Link" value="{{ link }}"> |
| <input type="file" name="avatar" accept="image/*"> |
| <button type="submit" name="update_profile" class="btn">Save</button> |
| </form> |
| <a href="{{ url_for('upload') }}" class="btn">Add Post</a> |
| <h2>Your Posts</h2> |
| <div class="post-grid"> |
| {% if user_posts %} |
| {% for post in user_posts %} |
| <div class="post-item"> |
| <a href="{{ url_for('post_page', post_id=post['id']) }}"> |
| {% if post['type'] == 'video' %} |
| <video class="post-preview" preload="metadata" muted 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" onclick="openModal(this.src, event, '{{ post['id'] }}')"> |
| {% endif %} |
| <h3>{{ post['title'] }}</h3> |
| </a> |
| <p>{{ post['description']|truncate(100) }}</p> |
| <p>{{ post['upload_date'] }}</p> |
| <form method="POST"> |
| <input type="hidden" name="post_id" value="{{ post['id'] }}"> |
| <button type="submit" name="delete_post" class="btn delete-btn">Delete</button> |
| </form> |
| </div> |
| {% endfor %} |
| {% else %} |
| <p style="font-size: 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, event, postId) { |
| event.preventDefault(); |
| const modal = document.getElementById('imageModal'); |
| const modalImg = document.getElementById('modalImage'); |
| modal.style.display = 'flex'; |
| modalImg.src = src; |
| fetch('/increment_view/' + postId, { method: 'POST' }); |
| } |
| function closeModal(event) { |
| if (event.target.tagName !== 'IMG') { |
| document.getElementById('imageModal').style.display = 'none'; |
| } |
| } |
| function copyProfileLink() { |
| navigator.clipboard.writeText(window.location.href).then(() => { |
| alert('Link copied!'); |
| }); |
| } |
| 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, total_views=total_views, total_likes=total_likes, bio=bio, link=link, avatar=avatar, repo_id=REPO_ID, is_authenticated=is_authenticated, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online, last_seen=last_seen) |
|
|
| @app.route('/profile/<username>') |
| def user_profile(username): |
| data = load_data() |
| session_username = session.get('username', None) |
| if session_username: |
| update_last_seen(data, session_username) |
| if username not in data['users']: |
| return "Пользователь не найден", 404 |
| |
| user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.strptime(x['upload_date'], '%Y-%m-%d %H:%M:%S'), reverse=True) |
| is_authenticated = 'username' in session |
| unread_count = get_unread_count(data, session_username) if is_authenticated else 0 |
| private_unread_count = get_private_unread_count(data, session_username) if is_authenticated else 0 |
| user_count = len(data['users']) |
| is_online = is_user_online(data, session_username) 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) |
| profile_is_online = is_user_online(data, username) |
| last_seen = user_data.get('last_seen', 'Never') |
|
|
| 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: 900px; |
| } |
| .profile-header { |
| display: flex; |
| gap: 20px; |
| background: var(--card-bg); |
| padding: 25px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| margin-bottom: 25px; |
| } |
| body.dark .profile-header { |
| background: var(--card-bg-dark); |
| } |
| .avatar { |
| width: 100px; |
| height: 100px; |
| border-radius: 50%; |
| object-fit: cover; |
| box-shadow: var(--shadow); |
| transition: var(--transition); |
| } |
| .avatar:hover { |
| transform: scale(1.03); |
| } |
| .profile-info h1 { |
| font-size: 2em; |
| font-weight: 800; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| margin-bottom: 12px; |
| } |
| .profile-info p { |
| font-size: 1em; |
| margin-bottom: 8px; |
| } |
| .post-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
| gap: 20px; |
| } |
| .post-item { |
| background: var(--card-bg); |
| padding: 15px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| transition: var(--transition); |
| } |
| body.dark .post-item { |
| background: var(--card-bg-dark); |
| } |
| .post-item:hover { |
| transform: translateY(-5px); |
| } |
| .post-preview { |
| width: 100%; |
| height: 200px; |
| object-fit: cover; |
| border-radius: 12px; |
| margin-bottom: 12px; |
| loading: lazy; |
| } |
| .post-item h3 { |
| font-size: 1.3em; |
| font-weight: 600; |
| margin-bottom: 8px; |
| } |
| h2 { |
| font-size: 1.6em; |
| font-weight: 800; |
| margin: 25px 0 15px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| @media (max-width: 480px) { |
| .profile-header { |
| flex-direction: column; |
| text-align: center; |
| } |
| .avatar { |
| width: 80px; |
| height: 80px; |
| } |
| .post-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| </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"> |
| {% endif %} |
| <div class="profile-info"> |
| <h1>{{ username }} <span class="status-dot {{ 'online' if profile_is_online else 'offline' }}"></span></h1> |
| {% if not profile_is_online %} |
| <p style="color: rgba(0, 0, 0, 0.5);">Last seen: {{ last_seen }}</p> |
| {% endif %} |
| <p>{{ bio }}</p> |
| {% if link %} |
| <p><a href="{{ link }}" target="_blank" style="color: var(--primary);">{{ link }}</a></p> |
| {% endif %} |
| <p>Views: {{ total_views }} | Likes: {{ total_likes }}</p> |
| </div> |
| </div> |
| <h2>Posts</h2> |
| <div class="post-grid"> |
| {% if user_posts %} |
| {% for post in user_posts %} |
| <div class="post-item"> |
| <a href="{{ url_for('post_page', post_id=post['id']) }}"> |
| {% if post['type'] == 'video' %} |
| <video class="post-preview" preload="metadata" muted 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" onclick="openModal(this.src, event, '{{ post['id'] }}')"> |
| {% endif %} |
| <h3>{{ post['title'] }}</h3> |
| </a> |
| <p>{{ post['description']|truncate(100) }}</p> |
| <p>{{ post['upload_date'] }}</p> |
| </div> |
| {% endfor %} |
| {% else %} |
| <p style="font-size: 1em;">No 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, event, postId) { |
| event.preventDefault(); |
| const modal = document.getElementById('imageModal'); |
| const modalImg = document.getElementById('modalImage'); |
| modal.style.display = 'flex'; |
| modalImg.src = src; |
| fetch('/increment_view/' + postId, { method: 'POST' }); |
| } |
| 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, total_views=total_views, total_likes=total_likes, bio=bio, link=link, avatar=avatar, repo_id=REPO_ID, is_authenticated=is_authenticated, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online, profile_is_online=profile_is_online, last_seen=last_seen) |
|
|
| @app.route('/upload', methods=['GET', 'POST']) |
| def upload(): |
| if 'username' not in session: |
| flash('Войдите, чтобы загрузить контент!') |
| return redirect(url_for('login')) |
| |
| data = load_data() |
| username = session['username'] |
| update_last_seen(data, username) |
| is_authenticated = 'username' in session |
| unread_count = get_unread_count(data, username) |
| private_unread_count = get_private_unread_count(data, username) |
| user_count = len(data['users']) |
| is_online = is_user_online(data, username) |
|
|
| if request.method == 'POST': |
| title = request.form.get('title') |
| description = request.form.get('description') |
| file = request.files.get('file') |
| if not file or not title: |
| flash('Необходимо указать заголовок и выбрать файл!') |
| 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}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{filename}" |
| api.upload_file( |
| path_or_fileobj=temp_path, |
| path_in_repo=file_path, |
| repo_id=REPO_ID, |
| repo_type="dataset", |
| token=HF_TOKEN_WRITE, |
| commit_message=f"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}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{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 Hub</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> |
| <style> |
| ''' + BASE_STYLE + ''' |
| h1 { |
| font-size: 2em; |
| font-weight: 800; |
| margin-bottom: 20px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| #progress-container { |
| width: 100%; |
| height: 5px; |
| background: var(--glass-bg); |
| border-radius: 5px; |
| margin-top: 15px; |
| overflow: hidden; |
| } |
| #progress-bar { |
| width: 0%; |
| height: 100%; |
| background: var(--primary); |
| transition: width 0.3s ease; |
| } |
| @media (max-width: 480px) { |
| .container { |
| padding: 15px; |
| } |
| } |
| </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: 12px;">{{ 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="3"></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, 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: 800px; |
| background: var(--card-bg); |
| padding: 25px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| } |
| body.dark .container { |
| background: var(--card-bg-dark); |
| } |
| h1 { |
| font-size: 2em; |
| font-weight: 800; |
| margin-bottom: 20px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| .messages { |
| max-height: 450px; |
| overflow-y: auto; |
| padding: 12px; |
| background: var(--glass-bg); |
| border-radius: 12px; |
| margin-bottom: 15px; |
| scroll-behavior: smooth; |
| } |
| .message { |
| margin-bottom: 12px; |
| display: flex; |
| align-items: flex-start; |
| gap: 10px; |
| max-width: 70%; |
| } |
| .message.sent { |
| margin-left: auto; |
| flex-direction: row-reverse; |
| } |
| .message.received { |
| margin-right: auto; |
| } |
| .message-avatar { |
| width: 35px; |
| height: 35px; |
| border-radius: 50%; |
| object-fit: cover; |
| box-shadow: var(--shadow); |
| loading: lazy; |
| } |
| .message-content { |
| padding: 10px 15px; |
| background: var(--card-bg); |
| border-radius: 10px; |
| font-size: 0.9em; |
| 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(-2px); |
| } |
| .message .sender { |
| color: var(--primary); |
| font-weight: 600; |
| text-decoration: none; |
| } |
| .message .sender:hover { |
| color: var(--accent); |
| } |
| .message .time { |
| font-size: 0.8em; |
| color: rgba(0, 0, 0, 0.5); |
| margin-top: 5px; |
| display: block; |
| } |
| body.dark .message .time { |
| color: rgba(255, 255, 255, 0.5); |
| } |
| .message img, .message video { |
| max-width: 250px; |
| max-height: 250px; |
| width: 100%; |
| border-radius: 8px; |
| margin-top: 8px; |
| display: block; |
| loading: lazy; |
| } |
| .message a { |
| color: var(--primary); |
| text-decoration: none; |
| } |
| .message a:hover { |
| color: var(--accent); |
| } |
| .message-form { |
| margin-top: 15px; |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| .post-select { |
| padding: 10px; |
| border-radius: 10px; |
| background: var(--glass-bg); |
| color: var(--text-light); |
| } |
| body.dark .post-select { |
| color: var(--text-dark); |
| } |
| @media (max-width: 900px) { |
| .message-avatar { |
| width: 30px; |
| height: 30px; |
| } |
| .message { |
| max-width: 80%; |
| } |
| } |
| @media (max-width: 480px) { |
| .container { |
| padding: 15px; |
| } |
| .messages { |
| max-height: 350px; |
| } |
| .message-avatar { |
| width: 25px; |
| height: 25px; |
| } |
| .message { |
| max-width: 85%; |
| } |
| .message-content { |
| padding: 8px 12px; |
| font-size: 0.85em; |
| } |
| .message img, .message video { |
| max-width: 180px; |
| max-height: 180px; |
| } |
| } |
| </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 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="2"></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: 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: 800px; |
| background: var(--card-bg); |
| padding: 25px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| } |
| body.dark .container { |
| background: var(--card-bg-dark); |
| } |
| h1 { |
| font-size: 2em; |
| font-weight: 800; |
| margin-bottom: 20px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| .search-container { |
| max-width: 600px; |
| margin: 0 auto 20px; |
| } |
| .search-form { |
| display: flex; |
| gap: 10px; |
| } |
| .search-input { |
| flex-grow: 1; |
| background: var(--card-bg); |
| border-radius: 12px; |
| box-shadow: var(--shadow); |
| } |
| body.dark .search-input { |
| background: var(--card-bg-dark); |
| } |
| .search-btn { |
| padding: 12px 20px; |
| box-shadow: none; |
| } |
| .user-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); |
| gap: 15px; |
| } |
| .user-item { |
| background: var(--card-bg); |
| padding: 12px; |
| border-radius: 12px; |
| box-shadow: var(--shadow); |
| transition: var(--transition); |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| } |
| body.dark .user-item { |
| background: var(--card-bg-dark); |
| } |
| .user-item:hover { |
| transform: translateY(-3px); |
| } |
| .user-avatar { |
| width: 50px; |
| height: 50px; |
| 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: 8px 14px; |
| font-size: 0.85em; |
| } |
| .message-btn:hover { |
| background: #00b8c5; |
| } |
| @media (max-width: 480px) { |
| .user-grid { |
| grid-template-columns: 1fr; |
| } |
| .user-avatar { |
| width: 40px; |
| height: 40px; |
| } |
| } |
| </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('Войдите, чтобы просмотреть сообщения!') |
| 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: 800px; |
| background: var(--card-bg); |
| padding: 25px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| } |
| body.dark .container { |
| background: var(--card-bg-dark); |
| } |
| h1 { |
| font-size: 2em; |
| font-weight: 800; |
| margin-bottom: 20px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| } |
| .dialog-list { |
| display: flex; |
| flex-direction: column; |
| gap: 12px; |
| } |
| .dialog-item { |
| background: var(--card-bg); |
| padding: 12px; |
| border-radius: 12px; |
| box-shadow: var(--shadow); |
| transition: var(--transition); |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| } |
| body.dark .dialog-item { |
| background: var(--card-bg-dark); |
| } |
| .dialog-item:hover { |
| transform: translateY(-3px); |
| } |
| .dialog-avatar { |
| width: 40px; |
| height: 40px; |
| 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: 3px 8px; |
| border-radius: 10px; |
| font-size: 0.75em; |
| font-weight: 700; |
| } |
| @media (max-width: 480px) { |
| .dialog-avatar { |
| width: 35px; |
| height: 35px; |
| } |
| .dialog-info a { |
| font-size: 0.9em; |
| } |
| } |
| </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.85em; color: rgba(0, 0, 0, 0.5);">{{ info.last_message['time'] }}: |
| {% if 'text' in info.last_message %}{{ info.last_message['text']|truncate(25) }}{% 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: 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('Войдите, чтобы начать чат!') |
| return redirect(url_for('login')) |
|
|
| data = load_data() |
| username = session['username'] |
| update_last_seen(data, username) |
| if recipient not in data['users']: |
| return "Пользователь не найден", 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: 800px; |
| background: var(--card-bg); |
| padding: 25px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| } |
| body.dark .container { |
| background: var(--card-bg-dark); |
| } |
| h1 { |
| font-size: 2em; |
| font-weight: 800; |
| margin-bottom: 20px; |
| background: linear-gradient(135deg, var(--primary), var(--accent)); |
| -webkit-background-clip: text; |
| color: transparent; |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| } |
| .recipient-avatar, .message-avatar { |
| width: 35px; |
| height: 35px; |
| border-radius: 50%; |
| object-fit: cover; |
| box-shadow: var(--shadow); |
| loading: lazy; |
| } |
| .messages { |
| max-height: 450px; |
| overflow-y: auto; |
| padding: 12px; |
| background: var(--glass-bg); |
| border-radius: 12px; |
| margin-bottom: 15px; |
| scroll-behavior: smooth; |
| } |
| .message { |
| display: flex; |
| align-items: flex-start; |
| gap: 10px; |
| margin-bottom: 12px; |
| max-width: 70%; |
| } |
| .message.sent { |
| margin-left: auto; |
| flex-direction: row-reverse; |
| } |
| .message.received { |
| margin-right: auto; |
| } |
| .message-content { |
| padding: 10px 15px; |
| background: var(--card-bg); |
| border-radius: 10px; |
| font-size: 0.9em; |
| 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(-2px); |
| } |
| .message .sender { |
| color: var(--primary); |
| font-weight: 600; |
| text-decoration: none; |
| } |
| .message .sender:hover { |
| color: var(--accent); |
| } |
| .message .time { |
| font-size: 0.8em; |
| color: rgba(0, 0, 0, 0.5); |
| margin-top: 5px; |
| display: block; |
| } |
| body.dark .message .time { |
| color: rgba(255, 255, 255, 0.5); |
| } |
| .message img, .message video { |
| max-width: 250px; |
| max-height: 250px; |
| width: 100%; |
| border-radius: 8px; |
| margin-top: 8px; |
| display: block; |
| loading: lazy; |
| } |
| .message a { |
| color: var(--primary); |
| text-decoration: none; |
| } |
| .message a:hover { |
| color: var(--accent); |
| } |
| .message-form { |
| margin-top: 15px; |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| .post-select { |
| padding: 10px; |
| border-radius: 10px; |
| 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: 30px; |
| height: 30px; |
| } |
| .message { |
| max-width: 80%; |
| } |
| } |
| @media (max-width: 480px) { |
| .container { |
| padding: 15px; |
| } |
| .messages { |
| max-height: 350px; |
| } |
| .message-avatar, .recipient-avatar { |
| width: 25px; |
| height: 25px; |
| } |
| .message { |
| max-width: 85%; |
| } |
| .message-content { |
| padding: 8px 12px; |
| font-size: 0.85em; |
| } |
| .message img, .message video { |
| max-width: 180px; |
| max-height: 180px; |
| } |
| } |
| </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"> |
| {% 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 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="2"></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> |
| <a href="{{ url_for('messages') }}" class="btn" style="margin-top: 12px;">Back to Messages</a> |
| </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), |
| avatar=avatar, |
| recipient_avatar=recipient_avatar, |
| recipient_online=recipient_online) |
|
|
| @app.route('/admhosto', methods=['GET', 'POST']) |
| def admin_panel(): |
| 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.form.get('search', '').strip().lower() if request.method == 'POST' and 'search' in request.form else request.args.get('search', '').strip().lower() |
| if search_query: |
| posts = [post for post in posts if search_query in post['title'].lower() or search_query in post['description'].lower()] |
|
|
| if request.method == 'POST' and 'delete' in request.form: |
| post_id = request.form.get('post_id') |
| if post_id: |
| data['posts'] = [p for p in data['posts'] if p['id'] != post_id] |
| save_data(data) |
| return redirect(url_for('admin_panel')) |
|
|
| 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>Admin Panel - Content Hub</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> |
| <style> |
| ''' + BASE_STYLE + ''' |
| 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: 600px; |
| margin: 0 auto 25px; |
| } |
| .search-form { |
| display: flex; |
| gap: 10px; |
| } |
| .search-input { |
| flex-grow: 1; |
| background: var(--card-bg); |
| border-radius: 12px; |
| box-shadow: var(--shadow); |
| } |
| body.dark .search-input { |
| background: var(--card-bg-dark); |
| } |
| .search-btn { |
| padding: 12px 20px; |
| box-shadow: none; |
| } |
| .post-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
| gap: 20px; |
| } |
| .post-item { |
| background: var(--card-bg); |
| padding: 15px; |
| border-radius: 16px; |
| box-shadow: var(--shadow); |
| transition: var(--transition); |
| } |
| body.dark .post-item { |
| background: var(--card-bg-dark); |
| } |
| .post-item:hover { |
| transform: translateY(-5px); |
| } |
| .post-preview { |
| width: 100%; |
| height: 200px; |
| object-fit: cover; |
| border-radius: 12px; |
| margin-bottom: 12px; |
| loading: lazy; |
| } |
| .post-item h3 { |
| font-size: 1.3em; |
| font-weight: 600; |
| margin-bottom: 8px; |
| } |
| .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) { |
| .post-grid { |
| grid-template-columns: 1fr; |
| } |
| .post-preview { |
| height: 150px; |
| } |
| 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>Admin Panel</h1> |
| <div class="search-container"> |
| <form method="POST" class="search-form"> |
| <input type="text" name="search" class="search-input" placeholder="Search posts" value="{{ search_query }}"> |
| <button type="submit" class="btn search-btn">🔍</button> |
| </form> |
| </div> |
| <div class="post-grid"> |
| {% for post in posts %} |
| <div class="post-item"> |
| <a href="{{ url_for('post_page', post_id=post['id']) }}"> |
| {% if post['type'] == 'video' %} |
| <video class="post-preview" preload="metadata" muted 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" onclick="openModal(this.src, event, '{{ post['id'] }}')"> |
| {% endif %} |
| <h3>{{ post['title'] }}</h3> |
| </a> |
| <p>{{ post['description']|truncate(100) }}</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> |
| <p>{{ post['upload_date'] }} | Views: {{ post['views'] }} | Likes: {{ post['likes']|length }}</p> |
| <form method="POST" style="margin-top: 10px;"> |
| <input type="hidden" name="post_id" value="{{ post['id'] }}"> |
| <button type="submit" name="delete" class="btn delete-btn">Delete</button> |
| </form> |
| </div> |
| {% endfor %} |
| {% if not posts %} |
| <p style="font-size: 1em;">No posts found.</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, event, postId) { |
| event.preventDefault(); |
| const modal = document.getElementById('imageModal'); |
| const modalImg = document.getElementById('modalImage'); |
| modal.style.display = 'flex'; |
| modalImg.src = src; |
| fetch('/increment_view/' + postId, { method: 'POST' }); |
| } |
| 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, |
| 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), |
| search_query=search_query) |
|
|
| if __name__ == '__main__': |
| threading.Thread(target=periodic_backup, daemon=True).start() |
| app.run(debug=True, host='0.0.0.0', port=5000) |