ChurchTube v3.1: Admin Logs, User Avatars, Timestamped Bookmarks, and Mobile Fixes
This commit is contained in:
parent
e7c9ea5386
commit
05175ac03b
16
README.md
16
README.md
|
|
@ -6,19 +6,23 @@ ChurchTube is a premium, self-hosted video platform designed specifically for ch
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **Premium UX**: Modern, responsive design with glassmorphism and smooth animations.
|
- **Premium UX**: Modern, responsive design with forced **Dark Mode** and glassmorphism.
|
||||||
- **Dual Video Sources**: Upload videos directly or link them from external sources (NAS, Google Drive, Cloud).
|
- **Dual Video Sources**: Upload videos directly or link them from external sources (NAS, Google Drive, Cloud).
|
||||||
- **Smart Google Drive Support**: Automatically converts Google Drive links for seamless embedding.
|
- **Smart Google Drive Support**: Automatically converts Google Drive links for seamless embedding and mobile-friendly controls.
|
||||||
|
- **User Identity**:
|
||||||
|
- Custom **User Avatars** for a more personal community experience.
|
||||||
|
- Profile management for passwords and identity.
|
||||||
|
- **Timestamped Bookmarks**: Save the exact second of a sermon and jump back to it later from your profile.
|
||||||
- **Interactive Community**:
|
- **Interactive Community**:
|
||||||
- AJAX-based commenting (no video reloads).
|
- AJAX-based commenting (no video reloads).
|
||||||
- 5 reaction types (👍, ❤️, 🙏, 💡, 👏).
|
- 5 reaction types (👍, ❤️, 🙏, 💡, 👏).
|
||||||
- Automated Profanity Filter with auto-reporting.
|
- Automated Profanity Filter with auto-reporting.
|
||||||
- **Robust Moderation**:
|
- Users can delete their own comments.
|
||||||
|
- **Administrative Accountability**:
|
||||||
|
- **System Logs**: Track logins, failed attempts, video plays, and comment history with IP address auditing.
|
||||||
- Role-Based Access Control (Admin, Moderator, Editor, User).
|
- Role-Based Access Control (Admin, Moderator, Editor, User).
|
||||||
- Dedicated Admin/Moderator dashboard for reports and users.
|
|
||||||
- **Custom Branding**: Real-time control over site title, colors, logo, and footer.
|
- **Custom Branding**: Real-time control over site title, colors, logo, and footer.
|
||||||
- **Search & Discovery**: Tag-based categorization, search, and intelligent recommendations.
|
- **Search & Discovery**: Keyword search, intelligent recommendations, and pagination.
|
||||||
- **Analytics**: Engagement-based view counting (only counts when video is played).
|
|
||||||
|
|
||||||
## 🛠️ Technology Stack
|
## 🛠️ Technology Stack
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$pdo->prepare("INSERT IGNORE INTO video_tags (video_id, tag_id) VALUES (?, ?)")->execute([$id, $tag_id]);
|
$pdo->prepare("INSERT IGNORE INTO video_tags (video_id, tag_id) VALUES (?, ?)")->execute([$id, $tag_id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cleanupTags();
|
||||||
$success = "Video updated successfully!";
|
$success = "Video updated successfully!";
|
||||||
// Refresh video data
|
// Refresh video data
|
||||||
$stmt = $pdo->prepare("SELECT * FROM videos WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT * FROM videos WHERE id = ?");
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ if (isset($_GET['delete'])) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo->prepare("DELETE FROM videos WHERE id = ?")->execute([$id]);
|
$pdo->prepare("DELETE FROM videos WHERE id = ?")->execute([$id]);
|
||||||
|
cleanupTags();
|
||||||
header('Location: index.php?msg=deleted');
|
header('Location: index.php?msg=deleted');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +52,10 @@ echo str_replace(['assets/', 'index.php', 'login.php', 'logout.php', 'admin/'],
|
||||||
<span style="position: absolute; top: -5px; right: -5px; background: #ff4081; width: 20px; height: 20px; border-radius: 50%; font-size: 0.7rem; display: flex; align-items: center; justify-content: center; color: white;">!</span>
|
<span style="position: absolute; top: -5px; right: -5px; background: #ff4081; width: 20px; height: 20px; border-radius: 50%; font-size: 0.7rem; display: flex; align-items: center; justify-content: center; color: white;">!</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="logs.php" class="btn" style="background: var(--bg-card); border: 1px solid var(--glass-border); flex-direction: column; padding: 20px;">
|
||||||
|
<i class="fas fa-list-ul" style="font-size: 1.5rem; margin-bottom: 10px;"></i>
|
||||||
|
System Logs
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px;">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
require_once '../includes/db.php';
|
||||||
|
require_once '../includes/auth.php';
|
||||||
|
requireAdmin();
|
||||||
|
|
||||||
|
$type_filter = $_GET['type'] ?? '';
|
||||||
|
$query = "SELECT l.*, u.username FROM logs l LEFT JOIN users u ON l.user_id = u.id";
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($type_filter) {
|
||||||
|
$query .= " WHERE l.type = ?";
|
||||||
|
$params = [$type_filter];
|
||||||
|
}
|
||||||
|
|
||||||
|
$query .= " ORDER BY l.created_at DESC LIMIT 100";
|
||||||
|
$stmt = $pdo->prepare($query);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$logs = $stmt->fetchAll();
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
require_once '../includes/header.php';
|
||||||
|
$header = ob_get_clean();
|
||||||
|
echo str_replace(['assets/', 'index.php', 'login.php', 'logout.php', 'admin/'], ['../assets/', '../index.php', '../login.php', '../logout.php', './'], $header);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div style="max-width: 1200px; margin: 40px auto; padding: 0 24px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px;">
|
||||||
|
<h1>System Logs</h1>
|
||||||
|
<div style="display: flex; gap: 12px;">
|
||||||
|
<select onchange="window.location.href='?type='+this.value" class="btn" style="background: var(--glass); color: var(--text-main); border: 1px solid var(--glass-border);">
|
||||||
|
<option value="">All Logs</option>
|
||||||
|
<option value="auth" <?= $type_filter === 'auth' ? 'selected' : '' ?>>Auth</option>
|
||||||
|
<option value="play" <?= $type_filter === 'play' ? 'selected' : '' ?>>Plays</option>
|
||||||
|
<option value="comment" <?= $type_filter === 'comment' ? 'selected' : '' ?>>Comments</option>
|
||||||
|
<option value="error" <?= $type_filter === 'error' ? 'selected' : '' ?>>Errors</option>
|
||||||
|
<option value="system" <?= $type_filter === 'system' ? 'selected' : '' ?>>System</option>
|
||||||
|
</select>
|
||||||
|
<a href="index.php" class="btn" style="background: var(--glass);">Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-card); border-radius: 16px; border: 1px solid var(--glass-border); overflow: hidden;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: var(--glass); color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase;">
|
||||||
|
<th style="padding: 16px;">Time</th>
|
||||||
|
<th style="padding: 16px;">Type</th>
|
||||||
|
<th style="padding: 16px;">User</th>
|
||||||
|
<th style="padding: 16px;">Message</th>
|
||||||
|
<th style="padding: 16px;">IP Address</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($logs as $l): ?>
|
||||||
|
<tr style="border-bottom: 1px solid var(--glass-border);">
|
||||||
|
<td style="padding: 16px; font-size: 0.85rem; color: var(--text-muted);"><?= date('M d, H:i:s', strtotime($l['created_at'])) ?></td>
|
||||||
|
<td style="padding: 16px;">
|
||||||
|
<span style="padding: 4px 8px; border-radius: 4px; font-size: 0.75rem; background: <?=
|
||||||
|
$l['type'] === 'error' ? '#ff4081' : (
|
||||||
|
$l['type'] === 'auth' ? '#7c4dff' : (
|
||||||
|
$l['type'] === 'play' ? '#4caf50' : 'var(--glass)')) ?>;">
|
||||||
|
<?= strtoupper($l['type']) ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 16px;"><?= htmlspecialchars($l['username'] ?? 'Guest') ?></td>
|
||||||
|
<td style="padding: 16px; font-size: 0.9rem;"><?= htmlspecialchars($l['message']) ?></td>
|
||||||
|
<td style="padding: 16px; font-size: 0.85rem; color: var(--text-muted);"><?= htmlspecialchars($l['ip_address']) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($logs)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" style="padding: 40px; text-align: center; color: var(--text-muted);">No logs found.</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require_once '../includes/footer.php'; ?>
|
||||||
|
|
@ -4,20 +4,35 @@ require_once '../includes/auth.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
if (!isModerator()) {
|
if (!isLoggedIn()) {
|
||||||
echo json_encode(['success' => false, 'error' => 'Moderator privileges required']);
|
echo json_encode(['success' => false, 'error' => 'Login required']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$comment_id = (int)($_POST['comment_id'] ?? 0);
|
$comment_id = (int)($_POST['comment_id'] ?? 0);
|
||||||
|
|
||||||
if (!$comment_id) {
|
if (!$comment_id) {
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid data']);
|
echo json_encode(['success' => false, 'error' => 'Invalid data']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check ownership or moderator status
|
||||||
|
$stmt = $pdo->prepare("SELECT user_id FROM comments WHERE id = ?");
|
||||||
|
$stmt->execute([$comment_id]);
|
||||||
|
$comment = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$comment) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Comment not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($comment['user_id'] != $_SESSION['user_id'] && !isModerator()) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$pdo->prepare("DELETE FROM comments WHERE id = ?")->execute([$comment_id]);
|
$pdo->prepare("DELETE FROM comments WHERE id = ?")->execute([$comment_id]);
|
||||||
|
logEvent('comment', "Comment deleted: ID $comment_id by user " . $_SESSION['username']);
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
echo json_encode(['success' => false, 'error' => 'DB error']);
|
echo json_encode(['success' => false, 'error' => 'DB error']);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ if (!$video_id) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT c.*, u.username,
|
SELECT c.*, u.username, u.avatar_url,
|
||||||
(SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'thumb') as thumbs,
|
(SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'thumb') as thumbs,
|
||||||
(SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'heart') as hearts,
|
(SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'heart') as hearts,
|
||||||
(SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'pray') as prays,
|
(SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'pray') as prays,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,13 @@ if (!$video_id) {
|
||||||
try {
|
try {
|
||||||
$stmt = $pdo->prepare("UPDATE videos SET views = views + 1 WHERE id = ?");
|
$stmt = $pdo->prepare("UPDATE videos SET views = views + 1 WHERE id = ?");
|
||||||
$stmt->execute([$video_id]);
|
$stmt->execute([$video_id]);
|
||||||
|
|
||||||
|
// Log the play event
|
||||||
|
$v_stmt = $pdo->prepare("SELECT title FROM videos WHERE id = ?");
|
||||||
|
$v_stmt->execute([$video_id]);
|
||||||
|
$title = $v_stmt->fetchColumn();
|
||||||
|
logEvent('play', "Started watching: $title (ID: $video_id)");
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
echo json_encode(['success' => false, 'error' => 'Database error']);
|
echo json_encode(['success' => false, 'error' => 'Database error']);
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ foreach ($bad_words as $word) {
|
||||||
try {
|
try {
|
||||||
$stmt = $pdo->prepare("INSERT INTO comments (video_id, user_id, comment_text, is_reported) VALUES (?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO comments (video_id, user_id, comment_text, is_reported) VALUES (?, ?, ?, ?)");
|
||||||
if ($stmt->execute([$video_id, $_SESSION['user_id'], $filtered_text, $is_flagged ? 1 : 0])) {
|
if ($stmt->execute([$video_id, $_SESSION['user_id'], $filtered_text, $is_flagged ? 1 : 0])) {
|
||||||
|
// Log the comment
|
||||||
|
$v_stmt = $pdo->prepare("SELECT title FROM videos WHERE id = ?");
|
||||||
|
$v_stmt->execute([$video_id]);
|
||||||
|
$title = $v_stmt->fetchColumn();
|
||||||
|
logEvent('comment', "Commented on $title: $filtered_text" . ($is_flagged ? " [FLAGGED]" : ""));
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
} else {
|
} else {
|
||||||
echo json_encode(['success' => false, 'error' => 'Database error']);
|
echo json_encode(['success' => false, 'error' => 'Database error']);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
require_once '../includes/db.php';
|
||||||
|
require_once '../includes/auth.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isLoggedIn()) {
|
||||||
|
$video_id = (int)$_POST['video_id'];
|
||||||
|
$timestamp = (float)($_POST['timestamp'] ?? 0);
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM bookmarks WHERE user_id = ? AND video_id = ?");
|
||||||
|
$stmt->execute([$user_id, $video_id]);
|
||||||
|
$bookmark = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($bookmark) {
|
||||||
|
$pdo->prepare("DELETE FROM bookmarks WHERE id = ?")->execute([$bookmark['id']]);
|
||||||
|
echo json_encode(['success' => true, 'action' => 'removed']);
|
||||||
|
} else {
|
||||||
|
$pdo->prepare("INSERT INTO bookmarks (user_id, video_id, video_timestamp) VALUES (?, ?, ?)")->execute([$user_id, $video_id, $timestamp]);
|
||||||
|
echo json_encode(['success' => true, 'action' => 'added']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
require_once '../includes/db.php';
|
||||||
|
require_once '../includes/auth.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isLoggedIn()) {
|
||||||
|
$theme = $_POST['theme'] === 'light' ? 'light' : 'dark';
|
||||||
|
$stmt = $pdo->prepare("UPDATE users SET theme_preference = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$theme, $_SESSION['user_id']]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
<?php
|
|
||||||
// ChurchTube Diagnostics Script
|
|
||||||
|
|
||||||
function check_status($condition, $success, $failure) {
|
|
||||||
if ($condition) {
|
|
||||||
echo "<li style='color: #4CAF50;'>[PASS] $success</li>";
|
|
||||||
} else {
|
|
||||||
echo "<li style='color: #f44336;'>[FAIL] $failure</li>";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>ChurchTube Diagnostics</title>
|
|
||||||
<link rel="stylesheet" href="assets/css/style.css">
|
|
||||||
<style>
|
|
||||||
.diag-box { background: #1e1e1e; padding: 30px; border-radius: 12px; margin-top: 50px; border: 1px solid #333; }
|
|
||||||
ul { list-style: none; padding: 0; }
|
|
||||||
li { padding: 12px; border-bottom: 1px solid #333; font-family: monospace; }
|
|
||||||
.advice { background: rgba(124, 77, 255, 0.1); padding: 15px; border-radius: 8px; margin-top: 20px; border-left: 4px solid var(--primary-color); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="centered-container" style="max-width: 800px;">
|
|
||||||
<h1 class="logo">ChurchTube Diagnostics</h1>
|
|
||||||
<div class="diag-box">
|
|
||||||
<h3>System Health Check</h3>
|
|
||||||
<ul>
|
|
||||||
<?php
|
|
||||||
// PHP Version
|
|
||||||
check_status(version_compare(PHP_VERSION, '7.4.0', '>='),
|
|
||||||
"PHP Version: " . PHP_VERSION,
|
|
||||||
"PHP Version: " . PHP_VERSION . " (Requires 7.4+)");
|
|
||||||
|
|
||||||
// Extensions
|
|
||||||
check_status(extension_loaded('pdo_mysql'), "PDO MySQL extension is loaded.", "PDO MySQL extension is MISSING.");
|
|
||||||
check_status(extension_loaded('curl'), "CURL extension is loaded.", "CURL extension is MISSING (required for external link validation).");
|
|
||||||
|
|
||||||
// Config File
|
|
||||||
$has_config = file_exists('includes/config.php');
|
|
||||||
if (check_status($has_config, "Configuration file exists.", "Configuration file (includes/config.php) is MISSING.")) {
|
|
||||||
require_once 'includes/config.php';
|
|
||||||
// DB Connection
|
|
||||||
try {
|
|
||||||
$pdo = new PDO("mysql:host=".DB_HOST.";dbname=".DB_NAME, DB_USER, DB_PASS);
|
|
||||||
check_status(true, "Database connection established successfully.", "");
|
|
||||||
} catch (Exception $e) {
|
|
||||||
check_status(false, "", "Database connection failed: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write Permissions
|
|
||||||
check_status(is_writable('uploads'), "Uploads directory is writable.", "Uploads directory is NOT writable. Run: chmod 777 uploads");
|
|
||||||
|
|
||||||
// PHP Settings
|
|
||||||
$upload_max = ini_get('upload_max_filesize');
|
|
||||||
$post_max = ini_get('post_max_size');
|
|
||||||
$is_low = (int)$upload_max < 100 || (int)$post_max < 100;
|
|
||||||
|
|
||||||
if ($is_low) {
|
|
||||||
echo "<li style='color: #ffab40;'>[WARN] Max Upload Size: $upload_max (Post Max: $post_max) - This is low for videos!</li>";
|
|
||||||
} else {
|
|
||||||
echo "<li style='color: #4CAF50;'>[PASS] Max Upload Size: $upload_max (Post Max: $post_max)</li>";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="advice">
|
|
||||||
<h4>Pro-Tips & Fixes:</h4>
|
|
||||||
<ul style="border:none; margin-top: 10px;">
|
|
||||||
<li><strong>MySQL Issues?</strong> Run: <code>sudo systemctl status mysql</code>. If stopped, run <code>sudo systemctl start mysql</code>.</li>
|
|
||||||
<li><strong>Upload Limits?</strong> Edit <code>/etc/php/7.4/apache2/php.ini</code>. Look for <code>upload_max_filesize</code> and <code>post_max_size</code>. Set them to <code>500M</code> or more, then run <code>sudo systemctl restart apache2</code>.</li>
|
|
||||||
<li><strong>Permission Issues?</strong> Run: <code>sudo chmod -R 777 uploads/</code> to ensure the web server can save video files.</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 20px; text-align: center;">
|
|
||||||
<a href="index.php" class="btn btn-primary">Back to Site</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -5,6 +5,16 @@ try {
|
||||||
$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS);
|
$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS);
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
|
require_once 'functions.php';
|
||||||
|
|
||||||
|
function cleanupTags() {
|
||||||
|
global $pdo;
|
||||||
|
try {
|
||||||
|
$pdo->exec("DELETE FROM tags WHERE id NOT IN (SELECT tag_id FROM video_tags)");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Fail silently
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Database Connection Error: " . $e->getMessage());
|
die("Database Connection Error: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
function logEvent($type, $message) {
|
||||||
|
global $pdo;
|
||||||
|
try {
|
||||||
|
$user_id = $_SESSION['user_id'] ?? null;
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
|
||||||
|
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||||
|
$ip = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
|
||||||
|
} elseif (isset($_SERVER['HTTP_X_REAL_IP'])) {
|
||||||
|
$ip = $_SERVER['HTTP_X_REAL_IP'];
|
||||||
|
} elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
|
||||||
|
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO logs (user_id, type, message, ip_address) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$user_id, $type, $message, $ip]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Fail silently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -19,23 +19,74 @@ $logo_url = get_setting('logo_url', '');
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary-color: <?= $primary_color ?>;
|
--primary-color: <?= $primary_color ?>;
|
||||||
|
--primary-rgb: <?= hexToRgb($primary_color) ?>;
|
||||||
--secondary-color: <?= $secondary_color ?>;
|
--secondary-color: <?= $secondary_color ?>;
|
||||||
|
|
||||||
|
/* Dark Theme (Only) */
|
||||||
|
--bg-main: #0f0f0f;
|
||||||
|
--bg-card: #1a1a1a;
|
||||||
|
--glass: rgba(255, 255, 255, 0.05);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--text-main: #ffffff;
|
||||||
|
--text-muted: #aaaaaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
header {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
height: auto !important;
|
||||||
|
padding: 10px 16px !important;
|
||||||
|
gap: 10px !important;
|
||||||
|
position: relative !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
}
|
||||||
.logo {
|
.logo {
|
||||||
font-size: 1.2rem !important;
|
font-size: 1.1rem !important;
|
||||||
max-width: 150px;
|
margin: 0 !important;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 60%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.search-bar {
|
.user-actions {
|
||||||
display: none !important;
|
width: auto !important;
|
||||||
|
order: 2 !important;
|
||||||
|
gap: 8px !important;
|
||||||
}
|
}
|
||||||
|
.search-bar {
|
||||||
|
display: flex !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 36px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
order: 3 !important;
|
||||||
|
padding: 0 12px !important;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 4px 10px !important;
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
}
|
||||||
|
.hide-mobile { display: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="theme-dark">
|
||||||
|
<?php
|
||||||
|
function hexToRgb($hex) {
|
||||||
|
$hex = str_replace("#", "", $hex);
|
||||||
|
if(strlen($hex) == 3) {
|
||||||
|
$r = hexdec(substr($hex,0,1).substr($hex,0,1));
|
||||||
|
$g = hexdec(substr($hex,1,1).substr($hex,1,1));
|
||||||
|
$b = hexdec(substr($hex,2,1).substr($hex,2,1));
|
||||||
|
} else {
|
||||||
|
$r = hexdec(substr($hex,0,2));
|
||||||
|
$g = hexdec(substr($hex,2,2));
|
||||||
|
$b = hexdec(substr($hex,4,2));
|
||||||
|
}
|
||||||
|
return "$r, $g, $b";
|
||||||
|
}
|
||||||
|
?>
|
||||||
<header>
|
<header>
|
||||||
<a href="index.php" class="logo">
|
<a href="index.php" class="logo">
|
||||||
<?php if ($logo_url): ?>
|
<?php if ($logo_url): ?>
|
||||||
|
|
@ -53,18 +104,32 @@ $logo_url = get_setting('logo_url', '');
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="user-actions" style="display: flex; gap: 16px; align-items: center;">
|
<div class="user-actions" style="display: flex; gap: 16px; align-items: center;">
|
||||||
<?php if (isEditor()): ?>
|
<?php if (isLoggedIn()):
|
||||||
<a href="admin/index.php" class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9rem;">
|
$hdr_avatar = '';
|
||||||
<i class="fas fa-plus"></i> Admin
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT avatar_url FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
$hdr_avatar = $stmt->fetchColumn();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Avatar column might not exist yet if migration wasn't run
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<a href="profile.php" style="width: 32px; height: 32px; border-radius: 50%; overflow: hidden; background: var(--primary-color); display: flex; align-items: center; justify-content: center; border: 2px solid var(--glass-border);">
|
||||||
|
<?php if ($hdr_avatar): ?>
|
||||||
|
<img src="<?= htmlspecialchars($hdr_avatar) ?>" style="width: 100%; height: 100%; object-fit: cover;">
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color: white; font-size: 0.8rem;"><?= strtoupper(substr($_SESSION['username'], 0, 1)) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (isLoggedIn()): ?>
|
<?php if (isEditor()): ?>
|
||||||
<span style="color: var(--text-muted);">Hi, <?= htmlspecialchars($_SESSION['username']) ?></span>
|
<a href="admin/index.php" class="btn" style="background: var(--glass);"><i class="fas fa-cog"></i> <span class="hide-mobile">Admin</span></a>
|
||||||
<a href="logout.php" title="Logout"><i class="fas fa-sign-out-alt"></i></a>
|
<?php endif; ?>
|
||||||
|
<a href="logout.php" class="btn btn-primary">Logout</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<a href="login.php" class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9rem;">Login</a>
|
<a href="login.php" class="btn" style="background: var(--glass);">Login</a>
|
||||||
|
<a href="register.php" class="btn btn-primary">Register</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main style="min-height: calc(100vh - 64px);">
|
<main>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
function get_setting($key, $default = '') {
|
function get_setting($key, $default = '') {
|
||||||
global $pdo;
|
global $pdo;
|
||||||
try {
|
static $settings_cache = null;
|
||||||
$stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = ?");
|
|
||||||
$stmt->execute([$key]);
|
if ($settings_cache === null) {
|
||||||
$res = $stmt->fetch();
|
try {
|
||||||
return $res ? $res['setting_value'] : $default;
|
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings");
|
||||||
} catch (Exception $e) {
|
$settings_cache = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
return $default;
|
} catch (Exception $e) {
|
||||||
|
$settings_cache = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return isset($settings_cache[$key]) ? $settings_cache[$key] : $default;
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
|
||||||
65
index.php
65
index.php
|
|
@ -8,24 +8,43 @@ require_once 'includes/db.php';
|
||||||
require_once 'includes/settings_helper.php';
|
require_once 'includes/settings_helper.php';
|
||||||
require_once 'includes/header.php';
|
require_once 'includes/header.php';
|
||||||
|
|
||||||
$search = $_GET['q'] ?? '';
|
$page = isset($_GET['p']) ? (int)$_GET['p'] : 1;
|
||||||
$tag_filter = $_GET['tag'] ?? '';
|
$limit = 10;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
$query = "SELECT DISTINCT v.*, u.username as uploader FROM videos v
|
$query = "SELECT DISTINCT v.*, u.username as uploader FROM videos v
|
||||||
JOIN users u ON v.uploader_id = u.id
|
LEFT JOIN users u ON v.uploader_id = u.id
|
||||||
LEFT JOIN video_tags vt ON v.id = vt.video_id
|
LEFT JOIN video_tags vt ON v.id = vt.video_id
|
||||||
LEFT JOIN tags t ON vt.tag_id = t.id";
|
LEFT JOIN tags t ON vt.tag_id = t.id";
|
||||||
|
$search = $_GET['q'] ?? '';
|
||||||
|
$tag_filter = $_GET['tag'] ?? '';
|
||||||
$params = [];
|
$params = [];
|
||||||
|
|
||||||
|
$where_clauses = [];
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$query .= " WHERE (v.title LIKE ? OR v.description LIKE ? OR t.name LIKE ?)";
|
$where_clauses[] = "(v.title LIKE ? OR v.description LIKE ? OR t.name LIKE ?)";
|
||||||
$params = ["%$search%", "%$search%", "%$search%"];
|
$params = array_merge($params, ["%$search%", "%$search%", "%$search%"]);
|
||||||
} elseif ($tag_filter) {
|
}
|
||||||
$query .= " WHERE t.name = ?";
|
if ($tag_filter) {
|
||||||
$params = [$tag_filter];
|
$where_clauses[] = "t.name = ?";
|
||||||
|
$params[] = $tag_filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
$query .= " ORDER BY v.release_date DESC, v.created_at DESC";
|
if (!empty($where_clauses)) {
|
||||||
|
$query .= " WHERE " . implode(" AND ", $where_clauses);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count total for pagination
|
||||||
|
$count_query = "SELECT COUNT(DISTINCT v.id) FROM videos v " .
|
||||||
|
"LEFT JOIN video_tags vt ON v.id = vt.video_id " .
|
||||||
|
"LEFT JOIN tags t ON vt.tag_id = t.id " .
|
||||||
|
(!empty($where_clauses) ? " WHERE " . implode(" AND ", $where_clauses) : "");
|
||||||
|
$total_stmt = $pdo->prepare($count_query);
|
||||||
|
$total_stmt->execute($params);
|
||||||
|
$total_count = $total_stmt->fetchColumn();
|
||||||
|
$total_pages = ceil($total_count / $limit);
|
||||||
|
|
||||||
|
$query .= " ORDER BY v.release_date DESC, v.created_at DESC LIMIT $limit OFFSET $offset";
|
||||||
$stmt = $pdo->prepare($query);
|
$stmt = $pdo->prepare($query);
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
$videos = $stmt->fetchAll();
|
$videos = $stmt->fetchAll();
|
||||||
|
|
@ -34,13 +53,6 @@ $videos = $stmt->fetchAll();
|
||||||
$popular_tags = $pdo->query("SELECT name FROM tags LIMIT 10")->fetchAll(PDO::FETCH_COLUMN);
|
$popular_tags = $pdo->query("SELECT name FROM tags LIMIT 10")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div style="padding: 12px 24px; display: flex; gap: 12px; overflow-x: auto; border-bottom: 1px solid var(--glass-border); margin-bottom: 20px;">
|
|
||||||
<a href="index.php" class="btn" style="background: <?= !$tag_filter ? 'var(--primary-color)' : 'var(--glass)' ?>; border-radius: 20px; font-size: 0.85rem; padding: 6px 16px;">All</a>
|
|
||||||
<?php foreach ($popular_tags as $ptag): ?>
|
|
||||||
<a href="index.php?tag=<?= urlencode($ptag) ?>" class="btn" style="background: <?= $tag_filter === $ptag ? 'var(--primary-color)' : 'var(--glass)' ?>; border-radius: 20px; font-size: 0.85rem; padding: 6px 16px;"><?= htmlspecialchars($ptag) ?></a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="video-grid">
|
<div class="video-grid">
|
||||||
<?php if (empty($videos)): ?>
|
<?php if (empty($videos)): ?>
|
||||||
<div style="grid-column: 1/-1; text-align: center; padding: 100px; color: var(--text-muted);">
|
<div style="grid-column: 1/-1; text-align: center; padding: 100px; color: var(--text-muted);">
|
||||||
|
|
@ -54,12 +66,15 @@ $popular_tags = $pdo->query("SELECT name FROM tags LIMIT 10")->fetchAll(PDO::FET
|
||||||
<?php foreach ($videos as $video): ?>
|
<?php foreach ($videos as $video): ?>
|
||||||
<a href="watch.php?id=<?= $video['id'] ?>" class="video-card">
|
<a href="watch.php?id=<?= $video['id'] ?>" class="video-card">
|
||||||
<div class="video-thumbnail" style="background-image: url('<?= $video['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>');">
|
<div class="video-thumbnail" style="background-image: url('<?= $video['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>');">
|
||||||
<!-- Placeholder thumbnail if none exists -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="video-info">
|
<div class="video-info">
|
||||||
<h3 class="video-title"><?= htmlspecialchars($video['title']) ?></h3>
|
<h3 class="video-title"><?= htmlspecialchars($video['title']) ?></h3>
|
||||||
|
<div style="font-size: 0.8rem; color: var(--text-muted); margin: 8px 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">
|
||||||
|
<?= htmlspecialchars($video['description']) ?>
|
||||||
|
</div>
|
||||||
<div class="video-meta">
|
<div class="video-meta">
|
||||||
<span><?= htmlspecialchars($video['uploader']) ?></span> •
|
<span><?= htmlspecialchars($video['uploader']) ?></span> •
|
||||||
|
<span><?= number_format($video['views']) ?> views</span> •
|
||||||
<span><?= date('M d, Y', strtotime($video['release_date'])) ?></span>
|
<span><?= date('M d, Y', strtotime($video['release_date'])) ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -68,4 +83,20 @@ $popular_tags = $pdo->query("SELECT name FROM tags LIMIT 10")->fetchAll(PDO::FET
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($total_pages > 1): ?>
|
||||||
|
<div style="display: flex; justify-content: center; gap: 12px; margin: 40px 0;">
|
||||||
|
<?php if ($page > 1): ?>
|
||||||
|
<a href="?p=<?= $page - 1 ?><?= $search ? "&q=".urlencode($search) : '' ?><?= $tag_filter ? "&tag=".urlencode($tag_filter) : '' ?>" class="btn" style="background: var(--glass);"><i class="fas fa-chevron-left"></i> Previous</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||||
|
<a href="?p=<?= $i ?><?= $search ? "&q=".urlencode($search) : '' ?><?= $tag_filter ? "&tag=".urlencode($tag_filter) : '' ?>" class="btn" style="background: <?= $i === $page ? 'var(--primary-color)' : 'var(--glass)' ?>; min-width: 40px; justify-content: center;"><?= $i ?></a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
<?php if ($page < $total_pages): ?>
|
||||||
|
<a href="?p=<?= $page + 1 ?><?= $search ? "&q=".urlencode($search) : '' ?><?= $tag_filter ? "&tag=".urlencode($tag_filter) : '' ?>" class="btn" style="background: var(--glass);">Next <i class="fas fa-chevron-right"></i></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
|
|
|
||||||
20
install.php
20
install.php
|
|
@ -33,6 +33,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
password VARCHAR(255) NOT NULL,
|
password VARCHAR(255) NOT NULL,
|
||||||
email VARCHAR(100) NOT NULL UNIQUE,
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
role ENUM('admin', 'moderator', 'editor', 'user') DEFAULT 'user',
|
role ENUM('admin', 'moderator', 'editor', 'user') DEFAULT 'user',
|
||||||
|
avatar_url TEXT,
|
||||||
|
theme_preference ENUM('dark', 'light') DEFAULT 'dark',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS videos (
|
CREATE TABLE IF NOT EXISTS videos (
|
||||||
|
|
@ -78,6 +80,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE,
|
FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS bookmarks (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
video_id INT NOT NULL,
|
||||||
|
video_timestamp FLOAT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS logs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NULL,
|
||||||
|
type VARCHAR(50),
|
||||||
|
message TEXT,
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
setting_key VARCHAR(50) PRIMARY KEY,
|
setting_key VARCHAR(50) PRIMARY KEY,
|
||||||
setting_value TEXT
|
setting_value TEXT
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$_SESSION['user_id'] = $user['id'];
|
$_SESSION['user_id'] = $user['id'];
|
||||||
$_SESSION['username'] = $user['username'];
|
$_SESSION['username'] = $user['username'];
|
||||||
$_SESSION['user_role'] = $user['role'];
|
$_SESSION['user_role'] = $user['role'];
|
||||||
|
logEvent('auth', "User logged in: $username");
|
||||||
header('Location: index.php');
|
header('Location: index.php');
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
|
logEvent('auth', "FAILED login attempt for username: $username");
|
||||||
$error = "Invalid username or password.";
|
$error = "Invalid username or password.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
<?php
|
||||||
|
require_once 'includes/db.php';
|
||||||
|
require_once 'includes/auth.php';
|
||||||
|
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$success = '';
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (isset($_POST['change_password'])) {
|
||||||
|
$old_pass = $_POST['old_password'];
|
||||||
|
$new_pass = $_POST['new_password'];
|
||||||
|
$confirm_pass = $_POST['confirm_password'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT password FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$user_id]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if (password_verify($old_pass, $user['password'])) {
|
||||||
|
if ($new_pass === $confirm_pass) {
|
||||||
|
if (strlen($new_pass) >= 6) {
|
||||||
|
$hashed = password_hash($new_pass, PASSWORD_DEFAULT);
|
||||||
|
$pdo->prepare("UPDATE users SET password = ? WHERE id = ?")->execute([$hashed, $user_id]);
|
||||||
|
$success = "Password changed successfully!";
|
||||||
|
} else {
|
||||||
|
$error = "New password must be at least 6 characters.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = "New passwords do not match.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = "Incorrect current password.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['update_avatar'])) {
|
||||||
|
if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === 0) {
|
||||||
|
$ext = strtolower(pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowed = ['jpg', 'jpeg', 'png', 'webp'];
|
||||||
|
if (in_array($ext, $allowed)) {
|
||||||
|
$filename = 'avatar_' . $user_id . '_' . time() . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['avatar']['tmp_name'], 'uploads/' . $filename)) {
|
||||||
|
// Delete old avatar if exists
|
||||||
|
$stmt = $pdo->prepare("SELECT avatar_url FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$user_id]);
|
||||||
|
$old = $stmt->fetchColumn();
|
||||||
|
if ($old && strpos($old, 'uploads/') === 0) @unlink($old);
|
||||||
|
|
||||||
|
$avatar_url = 'uploads/' . $filename;
|
||||||
|
$pdo->prepare("UPDATE users SET avatar_url = ? WHERE id = ?")->execute([$avatar_url, $user_id]);
|
||||||
|
$success = "Avatar updated!";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = "Invalid image format.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user data
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$user_id]);
|
||||||
|
$user_data = $stmt->fetch();
|
||||||
|
$avatar = $user_data['avatar_url'] ?: '';
|
||||||
|
|
||||||
|
// Get bookmarks
|
||||||
|
$stmt = $pdo->prepare("SELECT v.* FROM videos v JOIN bookmarks b ON v.id = b.video_id WHERE b.user_id = ? ORDER BY b.created_at DESC");
|
||||||
|
$stmt->execute([$user_id]);
|
||||||
|
$bookmarks = $stmt->fetchAll();
|
||||||
|
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div style="max-width: 1000px; margin: 40px auto; padding: 0 24px; display: grid; grid-template-columns: 300px 1fr; gap: 40px;">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div>
|
||||||
|
<div style="background: var(--bg-card); padding: 24px; border-radius: 16px; border: 1px solid var(--glass-border); position: sticky; top: 100px;">
|
||||||
|
<div style="text-align: center; margin-bottom: 24px;">
|
||||||
|
<div style="width: 100px; height: 100px; background: var(--primary-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; overflow: hidden; border: 3px solid var(--glass-border);">
|
||||||
|
<?php if ($avatar): ?>
|
||||||
|
<img src="<?= htmlspecialchars($avatar) ?>" style="width: 100%; height: 100%; object-fit: cover;">
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="font-size: 2.5rem; color: white;"><?= strtoupper(substr($_SESSION['username'], 0, 1)) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<h3><?= htmlspecialchars($_SESSION['username']) ?></h3>
|
||||||
|
<p style="color: var(--text-muted); font-size: 0.9rem;">Member since <?= date('M Y', strtotime($user_data['created_at'])) ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data" style="margin-bottom: 24px;">
|
||||||
|
<input type="hidden" name="update_avatar" value="1">
|
||||||
|
<label class="btn" style="background: var(--glass); font-size: 0.8rem; cursor: pointer; display: block; text-align: center;">
|
||||||
|
<i class="fas fa-camera"></i> Change Avatar
|
||||||
|
<input type="file" name="avatar" style="display: none;" onchange="this.form.submit()">
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
|
<a href="#bookmarks" class="btn" style="background: var(--glass); justify-content: flex-start;"><i class="fas fa-bookmark" style="width: 20px;"></i> My Bookmarks</a>
|
||||||
|
<a href="#security" class="btn" style="background: var(--glass); justify-content: flex-start;"><i class="fas fa-shield-alt" style="width: 20px;"></i> Security</a>
|
||||||
|
<a href="logout.php" class="btn" style="background: rgba(255,64,129,0.1); color: #ff4081; justify-content: flex-start;"><i class="fas fa-sign-out-alt" style="width: 20px;"></i> Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div>
|
||||||
|
<!-- Bookmarks Section -->
|
||||||
|
<section id="bookmarks" style="margin-bottom: 60px;">
|
||||||
|
<h2 style="margin-bottom: 24px;">My Bookmarks</h2>
|
||||||
|
<?php
|
||||||
|
$stmt = $pdo->prepare("SELECT v.*, b.video_timestamp FROM videos v JOIN bookmarks b ON v.id = b.video_id WHERE b.user_id = ? ORDER BY b.created_at DESC");
|
||||||
|
$stmt->execute([$user_id]);
|
||||||
|
$bookmarks = $stmt->fetchAll();
|
||||||
|
|
||||||
|
function formatTime($seconds) {
|
||||||
|
if ($seconds <= 0) return "";
|
||||||
|
$mins = floor($seconds / 60);
|
||||||
|
$secs = floor($seconds % 60);
|
||||||
|
return sprintf("%d:%02d", $mins, $secs);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php if (empty($bookmarks)): ?>
|
||||||
|
<div style="background: var(--bg-card); padding: 40px; border-radius: 16px; border: 1px solid var(--glass-border); text-align: center; color: var(--text-muted);">
|
||||||
|
<i class="fas fa-bookmark" style="font-size: 3rem; margin-bottom: 16px; display: block;"></i>
|
||||||
|
<p>You haven't bookmarked any sermons yet.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px;">
|
||||||
|
<?php foreach ($bookmarks as $v):
|
||||||
|
$time_str = formatTime($v['video_timestamp']);
|
||||||
|
?>
|
||||||
|
<div style="background: var(--bg-card); border-radius: 12px; border: 1px solid var(--glass-border); overflow: hidden; position: relative;">
|
||||||
|
<a href="watch.php?id=<?= $v['id'] ?><?= $v['video_timestamp'] > 0 ? '&t='.$v['video_timestamp'] : '' ?>" style="text-decoration: none; color: inherit;">
|
||||||
|
<div style="height: 150px; background-image: url('<?= $v['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>'); background-size: cover; background-position: center; position: relative;">
|
||||||
|
<?php if ($time_str): ?>
|
||||||
|
<div style="position: absolute; bottom: 8px; right: 8px; background: rgba(0,0,0,0.8); color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem;">
|
||||||
|
At <?= $time_str ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 12px;">
|
||||||
|
<h4 style="margin-bottom: 4px; font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><?= htmlspecialchars($v['title']) ?></h4>
|
||||||
|
<div style="font-size: 0.8rem; color: var(--text-muted);">
|
||||||
|
Saved on <?= date('M d, Y', strtotime($v['created_at'])) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<button onclick="removeBookmark(<?= $v['id'] ?>)" style="position: absolute; top: 8px; right: 8px; background: rgba(255,64,129,0.9); color: white; border: none; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 10px rgba(0,0,0,0.3);">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function removeBookmark(id) {
|
||||||
|
if (!confirm('Remove this bookmark?')) return;
|
||||||
|
const res = await fetch('api/toggle_bookmark.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: `video_id=${id}`
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Security Section -->
|
||||||
|
<section id="security">
|
||||||
|
<h2 style="margin-bottom: 24px;">Security</h2>
|
||||||
|
<div style="background: var(--bg-card); padding: 32px; border-radius: 16px; border: 1px solid var(--glass-border);">
|
||||||
|
<h3>Change Password</h3>
|
||||||
|
<p style="color: var(--text-muted); margin-bottom: 24px; font-size: 0.9rem;">Keep your account secure by using a strong password.</p>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div style="background: rgba(76,175,80,0.1); color: #4caf50; padding: 12px; border-radius: 8px; margin-bottom: 20px; border: 1px solid rgba(76,175,80,0.2);">
|
||||||
|
<?= $success ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div style="background: rgba(255,64,129,0.1); color: #ff4081; padding: 12px; border-radius: 8px; margin-bottom: 20px; border: 1px solid rgba(255,64,129,0.2);">
|
||||||
|
<?= $error ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="change_password" value="1">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Current Password</label>
|
||||||
|
<input type="password" name="old_password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">New Password</label>
|
||||||
|
<input type="password" name="new_password" class="form-control" required minlength="6">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Confirm New Password</label>
|
||||||
|
<input type="password" name="confirm_password" class="form-control" required minlength="6">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="margin-top: 12px;">Update Password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
155
watch.php
155
watch.php
|
|
@ -63,36 +63,24 @@ require_once 'includes/header.php';
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div>
|
<div>
|
||||||
<!-- Video Player Area -->
|
<!-- Video Player Area -->
|
||||||
<div id="video-wrapper" style="background: #000; aspect-ratio: 16/9; border-radius: 12px; overflow: hidden; margin-bottom: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); position: relative; width: 100%;">
|
<div id="video-wrapper" style="background: #000; border-radius: 12px; overflow: hidden; margin-bottom: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); position: relative; width: 100%;">
|
||||||
<?php if ($video['source_type'] === 'upload'): ?>
|
<?php if ($video['source_type'] === 'upload'): ?>
|
||||||
<video id="main-video" controls onplay="incrementViews(<?= $video_id ?>)" style="width: 100%; height: 100%; object-fit: contain;" poster="<?= $video['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>">
|
<div style="aspect-ratio: 16/9;">
|
||||||
<source src="<?= htmlspecialchars($video['video_url']) ?>" type="video/mp4">
|
<video id="main-video" controls onplay="incrementViews(<?= $video_id ?>)" style="width: 100%; height: 100%; object-fit: contain;" poster="<?= $video['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>">
|
||||||
</video>
|
<source src="<?= htmlspecialchars($video['video_url']) ?>" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<!-- Play Overlay for Linked Videos -->
|
<!-- Play Overlay for Linked Videos -->
|
||||||
<div id="external-overlay" style="position: absolute; inset: 0; background-image: url('<?= $video['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>'); background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 5;" onclick="loadExternalVideo()">
|
<div id="external-overlay" style="aspect-ratio: 16/9; background-image: url('<?= $video['thumbnail_url'] ?: 'assets/images/default_thumb.png' ?>'); background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center; cursor: pointer;" onclick="loadExternalVideo()">
|
||||||
<div style="background: rgba(var(--primary-rgb, 124, 77, 255), 0.9); width: 80px; height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; box-shadow: 0 0 30px rgba(0,0,0,0.5); transition: transform 0.2s;" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">
|
<div style="background: rgba(var(--primary-rgb, 124, 77, 255), 0.9); width: 80px; height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; box-shadow: 0 0 30px rgba(0,0,0,0.5);">
|
||||||
<i class="fas fa-play" style="margin-left: 5px;"></i>
|
<i class="fas fa-play" style="margin-left: 5px;"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="iframe-container" style="width: 100%; height: 100%; display: none;">
|
<div id="iframe-container" style="display: none; position: relative; width: 100%; padding-bottom: 56.25%; height: 0; background: #000; min-height: 300px;">
|
||||||
<iframe id="external-iframe" width="100%" height="100%" src="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="width: 100%; height: 100%; object-fit: contain;"></iframe>
|
<iframe id="external-iframe" src="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; overflow: hidden;"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Recommendation Overlay (for uploaded videos) -->
|
|
||||||
<div id="video-end-overlay" style="display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.8); z-index: 10; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 20px;">
|
|
||||||
<h3 style="margin-bottom: 20px;">Up Next</h3>
|
|
||||||
<div style="display: flex; gap: 16px;">
|
|
||||||
<?php foreach (array_slice($recommendations, 0, 2) as $rec): ?>
|
|
||||||
<a href="watch.php?id=<?= $rec['id'] ?>" style="width: 200px;">
|
|
||||||
<div style="aspect-ratio: 16/9; background: #333; background-image: url('<?= $rec['thumbnail_url'] ?>'); background-size: cover; border-radius: 8px;"></div>
|
|
||||||
<div style="margin-top: 8px; font-size: 0.9rem;"><?= htmlspecialchars($rec['title']) ?></div>
|
|
||||||
</a>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<button onclick="document.getElementById('video-end-overlay').style.display='none'" class="btn" style="margin-top: 20px; background: var(--glass);">Replay</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Video Info -->
|
<!-- Video Info -->
|
||||||
|
|
@ -108,36 +96,40 @@ require_once 'includes/header.php';
|
||||||
<?= number_format($video['views']) ?> views • <?= date('M d, Y', strtotime($video['release_date'])) ?>
|
<?= number_format($video['views']) ?> views • <?= date('M d, Y', strtotime($video['release_date'])) ?>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 12px; position: relative;">
|
<div style="display: flex; gap: 12px; position: relative;">
|
||||||
<button class="btn" onclick="toggleShareMenu()" style="background: var(--primary-color); color: white;"><i class="fas fa-share"></i> Share</button>
|
<button onclick="toggleShareMenu()" class="btn" style="background: var(--primary-color); padding: 8px 16px;">
|
||||||
|
<i class="fas fa-share"></i> Share
|
||||||
<!-- Share Menu Dropdown -->
|
</button>
|
||||||
<div id="share-menu" style="display: none; position: absolute; top: 100%; right: 0; background: var(--bg-card); border: 1px solid var(--glass-border); border-radius: 8px; padding: 12px; z-index: 100; min-width: 200px; margin-top: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);">
|
<?php if (isLoggedIn()):
|
||||||
<div style="margin-bottom: 12px; font-weight: 600; font-size: 0.9rem;">Share this sermon</div>
|
$stmt = $pdo->prepare("SELECT 1 FROM bookmarks WHERE user_id = ? AND video_id = ?");
|
||||||
<div style="display: grid; gap: 8px;">
|
$stmt->execute([$_SESSION['user_id'], $video_id]);
|
||||||
<button onclick="copyCurrentLink()" class="btn" style="background: var(--glass); width: 100%; justify-content: flex-start; font-size: 0.85rem;">
|
$is_bookmarked = $stmt->fetch();
|
||||||
<i class="fas fa-link" style="margin-right: 8px;"></i> Copy Link
|
?>
|
||||||
</button>
|
<button onclick="toggleBookmark(<?= $video_id ?>)" id="bookmark-btn" class="btn" style="background: rgba(255,255,255,0.15); color: #fff; padding: 8px 16px; border: 1px solid rgba(255,255,255,0.2);">
|
||||||
<a href="https://www.facebook.com/sharer/sharer.php?u=<?= urlencode((isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']) ?>" target="_blank" class="btn" style="background: #1877f2; width: 100%; justify-content: flex-start; font-size: 0.85rem; text-decoration: none; color: white;">
|
<i class="<?= $is_bookmarked ? 'fas' : 'far' ?> fa-bookmark"></i> <?= $is_bookmarked ? 'Bookmarked' : 'Bookmark' ?>
|
||||||
<i class="fab fa-facebook" style="margin-right: 8px;"></i> Facebook
|
</button>
|
||||||
</a>
|
<?php endif; ?>
|
||||||
<a href="https://twitter.com/intent/tweet?url=<?= urlencode((isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']) ?>&text=Check out this sermon on ChurchTube!" target="_blank" class="btn" style="background: #000; width: 100%; justify-content: flex-start; font-size: 0.85rem; text-decoration: none; color: white;">
|
|
||||||
<i class="fab fa-x-twitter" style="margin-right: 8px;"></i> Twitter
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="padding: 20px 0;">
|
<div style="padding: 20px 0;">
|
||||||
<div style="display: flex; gap: 16px; align-items: center; margin-bottom: 16px;">
|
<div style="display: flex; gap: 16px; align-items: center; margin-bottom: 16px;">
|
||||||
<div style="width: 48px; height: 48px; background: var(--primary-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold;">
|
<?php
|
||||||
<?= strtoupper(substr($video['uploader'], 0, 1)) ?>
|
$u_stmt = $pdo->prepare("SELECT avatar_url FROM users WHERE id = ?");
|
||||||
|
$u_stmt->execute([$video['uploader_id']]);
|
||||||
|
$uploader_avatar = $u_stmt->fetchColumn();
|
||||||
|
?>
|
||||||
|
<div style="width: 48px; height: 48px; background: var(--primary-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; overflow: hidden; border: 2px solid var(--glass-border);">
|
||||||
|
<?php if ($uploader_avatar): ?>
|
||||||
|
<img src="<?= htmlspecialchars($uploader_avatar) ?>" style="width: 100%; height: 100%; object-fit: cover;">
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="font-weight: bold; color: white;"><?= strtoupper(substr($video['uploader'], 0, 1)) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: 600;"><?= htmlspecialchars($video['uploader']) ?></div>
|
<div style="font-weight: 600;"><?= htmlspecialchars($video['uploader']) ?></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="background: var(--glass); padding: 16px; border-radius: 12px; white-space: pre-wrap;"><?= htmlspecialchars($video['description']) ?></div>
|
<div style="background: var(--glass); padding: 16px; border-radius: 12px; white-space: pre-wrap; line-height: 1.6;"><?= htmlspecialchars($video['description']) ?></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -155,8 +147,12 @@ require_once 'includes/header.php';
|
||||||
<div id="comment-list" style="display: flex; flex-direction: column; gap: 24px;">
|
<div id="comment-list" style="display: flex; flex-direction: column; gap: 24px;">
|
||||||
<?php foreach ($comments as $c): ?>
|
<?php foreach ($comments as $c): ?>
|
||||||
<div class="comment-item" style="display: flex; gap: 16px;">
|
<div class="comment-item" style="display: flex; gap: 16px;">
|
||||||
<div style="width: 40px; height: 40px; background: #333; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-weight: bold;">
|
<div style="width: 40px; height: 40px; background: #333; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; border: 1px solid var(--glass-border);">
|
||||||
<?= strtoupper(substr($c['username'], 0, 1)) ?>
|
<?php if ($c['avatar_url']): ?>
|
||||||
|
<img src="<?= htmlspecialchars($c['avatar_url']) ?>" style="width: 100%; height: 100%; object-fit: cover;">
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="font-weight: bold;"><?= strtoupper(substr($c['username'], 0, 1)) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex-grow: 1;">
|
<div style="flex-grow: 1;">
|
||||||
<div style="font-weight: 600; font-size: 0.9rem;">
|
<div style="font-weight: 600; font-size: 0.9rem;">
|
||||||
|
|
@ -175,7 +171,7 @@ require_once 'includes/header.php';
|
||||||
|
|
||||||
<span onclick="report(<?= $c['id'] ?>)" style="cursor:pointer; margin-left: 10px; font-weight: 600; color: #ff4081;">REPORT</span>
|
<span onclick="report(<?= $c['id'] ?>)" style="cursor:pointer; margin-left: 10px; font-weight: 600; color: #ff4081;">REPORT</span>
|
||||||
|
|
||||||
<?php if (isModerator()): ?>
|
<?php if (isLoggedIn() && ($c['user_id'] == $_SESSION['user_id'] || isModerator())): ?>
|
||||||
<span onclick="deleteComment(<?= $c['id'] ?>)" style="cursor:pointer; margin-left: 10px; color: #ff4081;"><i class="fas fa-trash"></i></span>
|
<span onclick="deleteComment(<?= $c['id'] ?>)" style="cursor:pointer; margin-left: 10px; color: #ff4081;"><i class="fas fa-trash"></i></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -208,6 +204,8 @@ require_once 'includes/header.php';
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const videoId = <?= $video_id ?>;
|
const videoId = <?= $video_id ?>;
|
||||||
|
const currentUserId = <?= $_SESSION['user_id'] ?? 0 ?>;
|
||||||
|
const isLoggedIn = <?= isLoggedIn() ? 'true' : 'false' ?>;
|
||||||
const isModerator = <?= isModerator() ? 'true' : 'false' ?>;
|
const isModerator = <?= isModerator() ? 'true' : 'false' ?>;
|
||||||
const videoElem = document.getElementById('main-video');
|
const videoElem = document.getElementById('main-video');
|
||||||
const commentForm = document.getElementById('comment-form');
|
const commentForm = document.getElementById('comment-form');
|
||||||
|
|
@ -225,7 +223,12 @@ function loadExternalVideo() {
|
||||||
const overlay = document.getElementById('external-overlay');
|
const overlay = document.getElementById('external-overlay');
|
||||||
const container = document.getElementById('iframe-container');
|
const container = document.getElementById('iframe-container');
|
||||||
const iframe = document.getElementById('external-iframe');
|
const iframe = document.getElementById('external-iframe');
|
||||||
const videoUrl = "<?= htmlspecialchars($video['video_url']) ?>";
|
let videoUrl = "<?= htmlspecialchars($video['video_url']) ?>";
|
||||||
|
|
||||||
|
// Auto-convert Google Drive links to preview mode for better mobile support
|
||||||
|
if (videoUrl.includes('drive.google.com') && videoUrl.includes('/view')) {
|
||||||
|
videoUrl = videoUrl.replace('/view', '/preview');
|
||||||
|
}
|
||||||
|
|
||||||
incrementViews(videoId);
|
incrementViews(videoId);
|
||||||
|
|
||||||
|
|
@ -249,6 +252,35 @@ async function incrementViews(id) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleBookmark(id) {
|
||||||
|
let timestamp = 0;
|
||||||
|
if (videoElem) {
|
||||||
|
timestamp = videoElem.currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch('api/toggle_bookmark.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: `video_id=${id}×tamp=${timestamp}`
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
const btn = document.getElementById('bookmark-btn');
|
||||||
|
const isBookmarked = data.action === 'added';
|
||||||
|
btn.innerHTML = `<i class="${isBookmarked ? 'fas' : 'far'} fa-bookmark"></i> ${isBookmarked ? 'Bookmarked' : 'Bookmark'}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep linking to timestamp
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const t = urlParams.get('t');
|
||||||
|
if (t && videoElem) {
|
||||||
|
videoElem.currentTime = parseFloat(t);
|
||||||
|
videoElem.play().catch(() => {}); // Autoplay might be blocked
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function react(commentId, type) {
|
async function react(commentId, type) {
|
||||||
const res = await fetch('api/react.php', {
|
const res = await fetch('api/react.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -303,19 +335,32 @@ function renderComments(comments) {
|
||||||
commentList.innerHTML = '';
|
commentList.innerHTML = '';
|
||||||
comments.forEach(c => {
|
comments.forEach(c => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'comment-item';
|
div.id = `comment-${c.id}`;
|
||||||
div.style.display = 'flex';
|
div.style.display = 'flex';
|
||||||
div.style.gap = '16px';
|
div.style.gap = '16px';
|
||||||
|
div.style.marginBottom = '24px';
|
||||||
|
|
||||||
|
const avatarHtml = c.avatar_url
|
||||||
|
? `<img src="${c.avatar_url}" style="width: 100%; height: 100%; object-fit: cover;">`
|
||||||
|
: `<span style="font-weight: bold;">${c.username.charAt(0).toUpperCase()}</span>`;
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div style="width: 40px; height: 40px; background: #333; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-weight: bold;">
|
<div style="width: 40px; height: 40px; background: #333; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; border: 1px solid var(--glass-border);">
|
||||||
${c.username.charAt(0).toUpperCase()}
|
${avatarHtml}
|
||||||
</div>
|
</div>
|
||||||
<div style="flex-grow: 1;">
|
<div style="flex-grow: 1;">
|
||||||
<div style="font-weight: 600; font-size: 0.9rem;">
|
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||||||
${escapeHtml(c.username)}
|
<div style="font-weight: 600; font-size: 0.9rem;">
|
||||||
<span style="font-weight: 400; color: var(--text-muted); margin-left: 8px; font-size: 0.8rem;">${c.display_date}</span>
|
${escapeHtml(c.username)}
|
||||||
|
<span style="font-weight: 400; color: var(--text-muted); margin-left: 8px; font-size: 0.8rem;">${c.display_date}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 12px; align-items: center;">
|
||||||
|
${(isLoggedIn && (c.user_id == currentUserId || isModerator)) ?
|
||||||
|
`<button onclick="deleteComment(${c.id})" class="btn" style="background: none; padding: 4px; color: #ff4081;" title="Delete"><i class="fas fa-trash"></i></button>` : ''}
|
||||||
|
<button onclick="report(${c.id})" class="btn" style="background: none; padding: 4px; color: #ff4081; font-weight: 600; font-size: 0.75rem;">REPORT</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 4px;">${escapeHtml(c.comment_text)}</div>
|
<div style="margin-top: 4px; line-height: 1.4;">${escapeHtml(c.comment_text)}</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 16px; margin-top: 10px; align-items: center; font-size: 0.85rem; color: var(--text-muted);">
|
<div style="display: flex; gap: 16px; margin-top: 10px; align-items: center; font-size: 0.85rem; color: var(--text-muted);">
|
||||||
<span onclick="react(${c.id}, 'thumb')" style="cursor:pointer;"><i class="fas fa-thumbs-up"></i> <span id="count-thumb-${c.id}">${c.thumbs}</span></span>
|
<span onclick="react(${c.id}, 'thumb')" style="cursor:pointer;"><i class="fas fa-thumbs-up"></i> <span id="count-thumb-${c.id}">${c.thumbs}</span></span>
|
||||||
|
|
@ -323,10 +368,6 @@ function renderComments(comments) {
|
||||||
<span onclick="react(${c.id}, 'pray')" style="cursor:pointer;"><i class="fas fa-hands-praying"></i> <span id="count-pray-${c.id}">${c.prays}</span></span>
|
<span onclick="react(${c.id}, 'pray')" style="cursor:pointer;"><i class="fas fa-hands-praying"></i> <span id="count-pray-${c.id}">${c.prays}</span></span>
|
||||||
<span onclick="react(${c.id}, 'insight')" style="cursor:pointer;"><i class="fas fa-lightbulb"></i> <span id="count-insight-${c.id}">${c.insights}</span></span>
|
<span onclick="react(${c.id}, 'insight')" style="cursor:pointer;"><i class="fas fa-lightbulb"></i> <span id="count-insight-${c.id}">${c.insights}</span></span>
|
||||||
<span onclick="react(${c.id}, 'clap')" style="cursor:pointer;"><i class="fas fa-hands-clapping"></i> <span id="count-clap-${c.id}">${c.claps}</span></span>
|
<span onclick="react(${c.id}, 'clap')" style="cursor:pointer;"><i class="fas fa-hands-clapping"></i> <span id="count-clap-${c.id}">${c.claps}</span></span>
|
||||||
|
|
||||||
<span onclick="report(${c.id})" style="cursor:pointer; margin-left: 10px; font-weight: 600; color: #ff4081;">REPORT</span>
|
|
||||||
|
|
||||||
${isModerator ? `<span onclick="deleteComment(${c.id})" style="cursor:pointer; margin-left: 10px; color: #ff4081;"><i class="fas fa-trash"></i></span>` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue