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
|
||||
|
||||
- **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).
|
||||
- **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**:
|
||||
- AJAX-based commenting (no video reloads).
|
||||
- 5 reaction types (👍, ❤️, 🙏, 💡, 👏).
|
||||
- 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).
|
||||
- Dedicated Admin/Moderator dashboard for reports and users.
|
||||
- **Custom Branding**: Real-time control over site title, colors, logo, and footer.
|
||||
- **Search & Discovery**: Tag-based categorization, search, and intelligent recommendations.
|
||||
- **Analytics**: Engagement-based view counting (only counts when video is played).
|
||||
- **Search & Discovery**: Keyword search, intelligent recommendations, and pagination.
|
||||
|
||||
## 🛠️ 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]);
|
||||
}
|
||||
}
|
||||
cleanupTags();
|
||||
$success = "Video updated successfully!";
|
||||
// Refresh video data
|
||||
$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]);
|
||||
cleanupTags();
|
||||
header('Location: index.php?msg=deleted');
|
||||
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>
|
||||
<?php endif; ?>
|
||||
</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 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');
|
||||
|
||||
if (!isModerator()) {
|
||||
echo json_encode(['success' => false, 'error' => 'Moderator privileges required']);
|
||||
if (!isLoggedIn()) {
|
||||
echo json_encode(['success' => false, 'error' => 'Login required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$comment_id = (int)($_POST['comment_id'] ?? 0);
|
||||
|
||||
if (!$comment_id) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid data']);
|
||||
exit;
|
||||
}
|
||||
|
||||
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]);
|
||||
logEvent('comment', "Comment deleted: ID $comment_id by user " . $_SESSION['username']);
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'error' => 'DB error']);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ if (!$video_id) {
|
|||
|
||||
try {
|
||||
$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 = 'heart') as hearts,
|
||||
(SELECT COUNT(*) FROM reactions WHERE comment_id = c.id AND reaction_type = 'pray') as prays,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ if (!$video_id) {
|
|||
try {
|
||||
$stmt = $pdo->prepare("UPDATE videos SET views = views + 1 WHERE 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]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'error' => 'Database error']);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ foreach ($bad_words as $word) {
|
|||
try {
|
||||
$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])) {
|
||||
// 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]);
|
||||
} else {
|
||||
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->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$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) {
|
||||
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>
|
||||
:root {
|
||||
--primary-color: <?= $primary_color ?>;
|
||||
--primary-rgb: <?= hexToRgb($primary_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) {
|
||||
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 {
|
||||
font-size: 1.2rem !important;
|
||||
max-width: 150px;
|
||||
font-size: 1.1rem !important;
|
||||
margin: 0 !important;
|
||||
flex: 1;
|
||||
max-width: 60%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.search-bar {
|
||||
display: none !important;
|
||||
.user-actions {
|
||||
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>
|
||||
</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>
|
||||
<a href="index.php" class="logo">
|
||||
<?php if ($logo_url): ?>
|
||||
|
|
@ -53,18 +104,32 @@ $logo_url = get_setting('logo_url', '');
|
|||
</form>
|
||||
|
||||
<div class="user-actions" style="display: flex; gap: 16px; align-items: center;">
|
||||
<?php if (isEditor()): ?>
|
||||
<a href="admin/index.php" class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9rem;">
|
||||
<i class="fas fa-plus"></i> Admin
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isLoggedIn()): ?>
|
||||
<span style="color: var(--text-muted);">Hi, <?= htmlspecialchars($_SESSION['username']) ?></span>
|
||||
<a href="logout.php" title="Logout"><i class="fas fa-sign-out-alt"></i></a>
|
||||
<?php if (isLoggedIn()):
|
||||
$hdr_avatar = '';
|
||||
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: ?>
|
||||
<a href="login.php" class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9rem;">Login</a>
|
||||
<span style="color: white; font-size: 0.8rem;"><?= strtoupper(substr($_SESSION['username'], 0, 1)) ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
|
||||
<?php if (isEditor()): ?>
|
||||
<a href="admin/index.php" class="btn" style="background: var(--glass);"><i class="fas fa-cog"></i> <span class="hide-mobile">Admin</span></a>
|
||||
<?php endif; ?>
|
||||
<a href="logout.php" class="btn btn-primary">Logout</a>
|
||||
<?php else: ?>
|
||||
<a href="login.php" class="btn" style="background: var(--glass);">Login</a>
|
||||
<a href="register.php" class="btn btn-primary">Register</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
<main style="min-height: calc(100vh - 64px);">
|
||||
<main>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
<?php
|
||||
function get_setting($key, $default = '') {
|
||||
global $pdo;
|
||||
static $settings_cache = null;
|
||||
|
||||
if ($settings_cache === null) {
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = ?");
|
||||
$stmt->execute([$key]);
|
||||
$res = $stmt->fetch();
|
||||
return $res ? $res['setting_value'] : $default;
|
||||
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings");
|
||||
$settings_cache = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
} catch (Exception $e) {
|
||||
return $default;
|
||||
$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/header.php';
|
||||
|
||||
$search = $_GET['q'] ?? '';
|
||||
$tag_filter = $_GET['tag'] ?? '';
|
||||
$page = isset($_GET['p']) ? (int)$_GET['p'] : 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$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 tags t ON vt.tag_id = t.id";
|
||||
$search = $_GET['q'] ?? '';
|
||||
$tag_filter = $_GET['tag'] ?? '';
|
||||
$params = [];
|
||||
|
||||
$where_clauses = [];
|
||||
if ($search) {
|
||||
$query .= " WHERE (v.title LIKE ? OR v.description LIKE ? OR t.name LIKE ?)";
|
||||
$params = ["%$search%", "%$search%", "%$search%"];
|
||||
} elseif ($tag_filter) {
|
||||
$query .= " WHERE t.name = ?";
|
||||
$params = [$tag_filter];
|
||||
$where_clauses[] = "(v.title LIKE ? OR v.description LIKE ? OR t.name LIKE ?)";
|
||||
$params = array_merge($params, ["%$search%", "%$search%", "%$search%"]);
|
||||
}
|
||||
if ($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->execute($params);
|
||||
$videos = $stmt->fetchAll();
|
||||
|
|
@ -34,13 +53,6 @@ $videos = $stmt->fetchAll();
|
|||
$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">
|
||||
<?php if (empty($videos)): ?>
|
||||
<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): ?>
|
||||
<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' ?>');">
|
||||
<!-- Placeholder thumbnail if none exists -->
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<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">
|
||||
<span><?= htmlspecialchars($video['uploader']) ?></span> •
|
||||
<span><?= number_format($video['views']) ?> views</span> •
|
||||
<span><?= date('M d, Y', strtotime($video['release_date'])) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -68,4 +83,20 @@ $popular_tags = $pdo->query("SELECT name FROM tags LIMIT 10")->fetchAll(PDO::FET
|
|||
<?php endif; ?>
|
||||
</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'; ?>
|
||||
|
|
|
|||
20
install.php
20
install.php
|
|
@ -33,6 +33,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
role ENUM('admin', 'moderator', 'editor', 'user') DEFAULT 'user',
|
||||
avatar_url TEXT,
|
||||
theme_preference ENUM('dark', 'light') DEFAULT 'dark',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
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 (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 (
|
||||
setting_key VARCHAR(50) PRIMARY KEY,
|
||||
setting_value TEXT
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['user_role'] = $user['role'];
|
||||
logEvent('auth', "User logged in: $username");
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} else {
|
||||
logEvent('auth', "FAILED login attempt for username: $username");
|
||||
$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'; ?>
|
||||
141
watch.php
141
watch.php
|
|
@ -63,36 +63,24 @@ require_once 'includes/header.php';
|
|||
<!-- Main Content -->
|
||||
<div>
|
||||
<!-- 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'): ?>
|
||||
<div style="aspect-ratio: 16/9;">
|
||||
<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' ?>">
|
||||
<source src="<?= htmlspecialchars($video['video_url']) ?>" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<!-- 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 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 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);">
|
||||
<i class="fas fa-play" style="margin-left: 5px;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="iframe-container" style="width: 100%; height: 100%; display: none;">
|
||||
<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>
|
||||
<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" 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>
|
||||
<?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>
|
||||
|
||||
<!-- Video Info -->
|
||||
|
|
@ -108,36 +96,40 @@ require_once 'includes/header.php';
|
|||
<?= number_format($video['views']) ?> views • <?= date('M d, Y', strtotime($video['release_date'])) ?>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Share Menu Dropdown -->
|
||||
<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);">
|
||||
<div style="margin-bottom: 12px; font-weight: 600; font-size: 0.9rem;">Share this sermon</div>
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<button onclick="copyCurrentLink()" class="btn" style="background: var(--glass); width: 100%; justify-content: flex-start; font-size: 0.85rem;">
|
||||
<i class="fas fa-link" style="margin-right: 8px;"></i> Copy Link
|
||||
<button onclick="toggleShareMenu()" class="btn" style="background: var(--primary-color); padding: 8px 16px;">
|
||||
<i class="fas fa-share"></i> Share
|
||||
</button>
|
||||
<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="fab fa-facebook" style="margin-right: 8px;"></i> Facebook
|
||||
</a>
|
||||
<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>
|
||||
<?php if (isLoggedIn()):
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM bookmarks WHERE user_id = ? AND video_id = ?");
|
||||
$stmt->execute([$_SESSION['user_id'], $video_id]);
|
||||
$is_bookmarked = $stmt->fetch();
|
||||
?>
|
||||
<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);">
|
||||
<i class="<?= $is_bookmarked ? 'fas' : 'far' ?> fa-bookmark"></i> <?= $is_bookmarked ? 'Bookmarked' : 'Bookmark' ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 20px 0;">
|
||||
<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;">
|
||||
<?= strtoupper(substr($video['uploader'], 0, 1)) ?>
|
||||
<?php
|
||||
$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 style="font-weight: 600;"><?= htmlspecialchars($video['uploader']) ?></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>
|
||||
|
||||
|
|
@ -155,8 +147,12 @@ require_once 'includes/header.php';
|
|||
<div id="comment-list" style="display: flex; flex-direction: column; gap: 24px;">
|
||||
<?php foreach ($comments as $c): ?>
|
||||
<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;">
|
||||
<?= strtoupper(substr($c['username'], 0, 1)) ?>
|
||||
<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);">
|
||||
<?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 style="flex-grow: 1;">
|
||||
<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>
|
||||
|
||||
<?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>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
@ -208,6 +204,8 @@ require_once 'includes/header.php';
|
|||
|
||||
<script>
|
||||
const videoId = <?= $video_id ?>;
|
||||
const currentUserId = <?= $_SESSION['user_id'] ?? 0 ?>;
|
||||
const isLoggedIn = <?= isLoggedIn() ? 'true' : 'false' ?>;
|
||||
const isModerator = <?= isModerator() ? 'true' : 'false' ?>;
|
||||
const videoElem = document.getElementById('main-video');
|
||||
const commentForm = document.getElementById('comment-form');
|
||||
|
|
@ -225,7 +223,12 @@ function loadExternalVideo() {
|
|||
const overlay = document.getElementById('external-overlay');
|
||||
const container = document.getElementById('iframe-container');
|
||||
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);
|
||||
|
||||
|
|
@ -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) {
|
||||
const res = await fetch('api/react.php', {
|
||||
method: 'POST',
|
||||
|
|
@ -303,19 +335,32 @@ function renderComments(comments) {
|
|||
commentList.innerHTML = '';
|
||||
comments.forEach(c => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'comment-item';
|
||||
div.id = `comment-${c.id}`;
|
||||
div.style.display = 'flex';
|
||||
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 style="width: 40px; height: 40px; background: #333; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-weight: bold;">
|
||||
${c.username.charAt(0).toUpperCase()}
|
||||
<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);">
|
||||
${avatarHtml}
|
||||
</div>
|
||||
<div style="flex-grow: 1;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div style="font-weight: 600; font-size: 0.9rem;">
|
||||
${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="margin-top: 4px;">${escapeHtml(c.comment_text)}</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 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);">
|
||||
<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}, '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="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>
|
||||
`;
|
||||
|
|
|
|||
Loading…
Reference in New Issue