<?php
// ============================================================
// Fiveo1 Social Platform - Fixed & Enhanced
// Version 1.1 | Evan Winter | MIT License
// ============================================================
session_start([
'cookie_httponly' => true,
'cookie_secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'),
'use_strict_mode' => true,
'use_only_cookies' => true,
'cookie_samesite' => 'Strict'
]);
ini_set('upload_max_filesize', '2G');
ini_set('post_max_size', '2G');
ini_set('memory_limit', '2G');
ini_set('max_execution_time', 0);
error_reporting(E_ALL);
ini_set('display_errors', 0); // set to 1 for debugging
// Helper: get base URL dynamically
function baseUrl() {
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
$host = $_SERVER['HTTP_HOST'];
$script = $_SERVER['SCRIPT_NAME'];
$dir = dirname($script);
if ($dir == '\\' || $dir == '/') $dir = '';
return $protocol . $host . $dir;
}
$dbPath = __DIR__ . '/db.sqlite';
$db = new SQLite3($dbPath);
$db->busyTimeout(30000);
$db->exec("PRAGMA journal_mode=WAL");
$db->exec("PRAGMA synchronous=NORMAL");
// Create tables if not exist
$db->exec("CREATE TABLE IF NOT EXISTS content (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT CHECK(type IN ('image','video','article')),
title TEXT,
description TEXT,
content TEXT,
filename TEXT,
file_path TEXT,
thumbnail TEXT,
latitude REAL,
longitude REAL,
location_name TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
ip_address TEXT,
fingerprint TEXT
)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_content_created ON content(created_at DESC)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_content_type ON content(type)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_content_location ON content(latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL");
$db->exec("CREATE INDEX IF NOT EXISTS idx_content_search ON content(title, description, content)");
$db->exec("PRAGMA optimize");
// Ensure upload directories exist
foreach (['uploads', 'uploads/images', 'uploads/videos', 'uploads/videos/thumbnails', 'uploads/chunks'] as $dir) {
if (!is_dir(__DIR__ . '/' . $dir)) mkdir(__DIR__ . '/' . $dir, 0755, true);
}
// ------------------------------------------------------------------
if (!function_exists('haversineDistance')) {
function haversineDistance($lat1, $lon1, $lat2, $lon2) {
$earthRadiusKm = 6371;
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat/2) * sin($dLat/2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon/2) * sin($dLon/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $earthRadiusKm * $c;
}
}
function combineChunks($chunkDir, $totalChunks, $finalTempPath) {
$out = fopen($finalTempPath, 'wb');
if (!$out) return false;
for ($i = 0; $i < $totalChunks; $i++) {
$partPath = $chunkDir . '/part_' . $i;
if (!file_exists($partPath)) {
fclose($out);
return false;
}
$part = fopen($partPath, 'rb');
while (!feof($part)) {
fwrite($out, fread($part, 8192));
}
fclose($part);
}
fclose($out);
return true;
}
function cleanupOldChunks() {
$chunkBase = __DIR__ . '/uploads/chunks/';
if (!is_dir($chunkBase)) return;
$now = time();
foreach (glob($chunkBase . '*', GLOB_ONLYDIR) as $dir) {
if ($now - filemtime($dir) > 3600) {
array_map('unlink', glob($dir . '/*'));
rmdir($dir);
}
}
}
if (mt_rand(1, 100) === 1) cleanupOldChunks();
function getCachedGeoPoints($db, $cacheFile = __DIR__ . '/uploads/.geopoints.cache') {
$ttl = 300;
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) {
$data = file_get_contents($cacheFile);
if ($data !== false) {
$cached = unserialize($data);
if (is_array($cached)) return $cached;
}
}
$geoQuery = "SELECT id, title, latitude, longitude, location_name, created_at
FROM content
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
ORDER BY created_at ASC";
$geoResult = $db->query($geoQuery);
$points = [];
while ($row = $geoResult->fetchArray(SQLITE3_ASSOC)) {
$points[] = [
'id' => $row['id'],
'title' => $row['title'],
'lat' => (float)$row['latitude'],
'lng' => (float)$row['longitude'],
'location_name' => $row['location_name'],
'created_at' => $row['created_at']
];
}
file_put_contents($cacheFile, serialize($points), LOCK_EX);
return $points;
}
function invalidateGeoCache() {
$cacheFile = __DIR__ . '/uploads/.geopoints.cache';
if (file_exists($cacheFile)) @unlink($cacheFile);
}
// Generate a filename and title from location and time
function generateMediaTitleAndFilename($location_name, $timestamp = null) {
if ($timestamp === null) $timestamp = time();
$dateTitle = date('n/j/Y, g:i:s A', $timestamp);
$title = '@' . $dateTitle;
if (!empty($location_name)) {
$title .= ' - ' . $location_name;
}
$dateFile = date('m-d-Y_H-i-s', $timestamp);
$filename = '@' . $dateFile;
if (!empty($location_name)) {
$slug = preg_replace('/[^a-z0-9]+/i', '_', trim($location_name));
$slug = trim($slug, '_');
if ($slug !== '') $filename .= '_' . $slug;
}
return ['title' => $title, 'filename' => $filename . '.jpg'];
}
// ------------------------------------------------------------------
// Handle POST requests (image, video chunked, article)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$fingerprint = $_POST['fingerprint'] ?? session_id();
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// --- ARTICLE CREATION ---
if (isset($_POST['article_title']) && isset($_POST['article_content'])) {
$title = trim($_POST['article_title']);
$content = trim($_POST['article_content']);
$description = $_POST['article_description'] ?? '';
if ($title !== '' && $content !== '') {
$stmt = $db->prepare("INSERT INTO content (type, title, description, content, ip_address, fingerprint) VALUES ('article', :title, :desc, :content, :ip, :fp)");
$stmt->bindValue(':title', $title);
$stmt->bindValue(':desc', $description);
$stmt->bindValue(':content', $content);
$stmt->bindValue(':ip', $ip);
$stmt->bindValue(':fp', $fingerprint);
$stmt->execute();
header('Content-Type: application/json');
echo json_encode(['success' => true, 'post_id' => $db->lastInsertRowID()]);
} else {
http_response_code(400);
echo json_encode(['error' => 'Title and content required']);
}
exit;
}
// --- IMAGE OR VIDEO UPLOAD ---
if (!isset($_FILES['media'])) {
http_response_code(400);
echo json_encode(['error' => 'No file uploaded']);
exit;
}
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';
$latitude = isset($_POST['latitude']) && $_POST['latitude'] !== '' ? floatval($_POST['latitude']) : null;
$longitude = isset($_POST['longitude']) && $_POST['longitude'] !== '' ? floatval($_POST['longitude']) : null;
$location_name = $_POST['location_name'] ?? '';
$type = $_POST['type'] ?? '';
$isChunked = isset($_POST['chunk_index']) && isset($_POST['upload_id']) && isset($_POST['chunk_total']);
// --- CHUNKED VIDEO UPLOAD ---
if ($isChunked) {
$chunkIndex = intval($_POST['chunk_index']);
$chunkTotal = intval($_POST['chunk_total']);
$uploadId = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['upload_id']);
if (empty($uploadId) || $chunkTotal <= 0 || $chunkIndex < 0 || $chunkIndex >= $chunkTotal) {
http_response_code(400);
echo json_encode(['error' => 'Invalid chunk parameters']);
exit;
}
$chunkBaseDir = __DIR__ . '/uploads/chunks/' . $uploadId;
if (!is_dir($chunkBaseDir)) mkdir($chunkBaseDir, 0755, true);
$chunkPath = $chunkBaseDir . '/part_' . $chunkIndex;
if (!move_uploaded_file($_FILES['media']['tmp_name'], $chunkPath)) {
http_response_code(500);
echo json_encode(['error' => 'Failed to save chunk']);
exit;
}
// Store metadata on first chunk
if ($chunkIndex === 0) {
$meta = [
'title' => $title,
'description' => $description,
'type' => $type,
'latitude' => $latitude,
'longitude' => $longitude,
'location_name' => $location_name,
'fingerprint' => $fingerprint,
'ip' => $ip
];
file_put_contents($chunkBaseDir . '/meta.json', json_encode($meta));
}
// On last chunk, reassemble and save
if ($chunkIndex === $chunkTotal - 1) {
$metaFile = $chunkBaseDir . '/meta.json';
if (!file_exists($metaFile)) {
http_response_code(400);
echo json_encode(['error' => 'Metadata missing']);
exit;
}
$meta = json_decode(file_get_contents($metaFile), true);
if (!$meta) {
http_response_code(400);
echo json_encode(['error' => 'Invalid metadata']);
exit;
}
$title = $meta['title'];
$description = $meta['description'];
$latitude = $meta['latitude'];
$longitude = $meta['longitude'];
$location_name = $meta['location_name'];
$fingerprint = $meta['fingerprint'];
$ip = $meta['ip'];
$tempFinal = $chunkBaseDir . '/final_temp';
if (!combineChunks($chunkBaseDir, $chunkTotal, $tempFinal)) {
http_response_code(500);
echo json_encode(['error' => 'Failed to combine chunks']);
exit;
}
$upload_dir = __DIR__ . '/uploads/videos/';
$web_path = '/uploads/videos/';
$filename = uniqid('video_') . '.mp4';
$finalPath = $upload_dir . $filename;
rename($tempFinal, $finalPath);
$file_path = $web_path . $filename;
// Generate thumbnail if ffmpeg available
$thumbnail = null;
if (function_exists('exec')) {
$thumb_dir = __DIR__ . '/uploads/videos/thumbnails/';
if (!is_dir($thumb_dir)) mkdir($thumb_dir, 0755, true);
$thumb_filename = uniqid() . '.jpg';
$thumbnail = '/uploads/videos/thumbnails/' . $thumb_filename;
$cmd = "ffmpeg -i " . escapeshellarg($finalPath) . " -ss 00:00:01 -vframes 1 -vf scale=320:-1 " . escapeshellarg($thumb_dir . $thumb_filename) . " > /dev/null 2>&1 &";
exec($cmd);
}
$stmt = $db->prepare("INSERT INTO content (type, title, description, filename, file_path, thumbnail, latitude, longitude, location_name, ip_address, fingerprint)
VALUES ('video', :title, :desc, :filename, :file_path, :thumb, :lat, :lng, :loc, :ip, :fp)");
$stmt->bindValue(':title', $title);
$stmt->bindValue(':desc', $description);
$stmt->bindValue(':filename', $filename);
$stmt->bindValue(':file_path', $file_path);
$stmt->bindValue(':thumb', $thumbnail);
$stmt->bindValue(':lat', $latitude);
$stmt->bindValue(':lng', $longitude);
$stmt->bindValue(':loc', $location_name);
$stmt->bindValue(':ip', $ip);
$stmt->bindValue(':fp', $fingerprint);
$stmt->execute();
if ($latitude && $longitude) invalidateGeoCache();
// Cleanup
array_map('unlink', glob($chunkBaseDir . '/*'));
rmdir($chunkBaseDir);
header('Content-Type: application/json');
echo json_encode(['success' => true, 'post_id' => $db->lastInsertRowID()]);
exit;
} else {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'chunk' => $chunkIndex, 'total' => $chunkTotal]);
exit;
}
}
// --- SINGLE IMAGE UPLOAD (non-chunked) ---
if ($type !== 'image') {
http_response_code(400);
echo json_encode(['error' => 'Only image uploads supported without chunking']);
exit;
}
if ($_FILES['media']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['error' => 'Upload failed']);
exit;
}
$upload_dir = __DIR__ . '/uploads/images/';
$web_path = '/uploads/images/';
$serverTime = time();
$titleData = generateMediaTitleAndFilename($location_name, $serverTime);
$title = $titleData['title'];
$filename = $titleData['filename'];
$finalPath = $upload_dir . $filename;
$counter = 1;
$originalFilename = $filename;
while (file_exists($finalPath)) {
$info = pathinfo($originalFilename);
$filename = $info['filename'] . '_' . $counter . '.jpg';
$finalPath = $upload_dir . $filename;
$counter++;
}
if (!move_uploaded_file($_FILES['media']['tmp_name'], $finalPath)) {
http_response_code(500);
echo json_encode(['error' => 'Failed to save image']);
exit;
}
$file_path = $web_path . $filename;
$stmt = $db->prepare("INSERT INTO content (type, title, description, filename, file_path, latitude, longitude, location_name, ip_address, fingerprint)
VALUES ('image', :title, :desc, :filename, :file_path, :lat, :lng, :loc, :ip, :fp)");
$stmt->bindValue(':title', $title);
$stmt->bindValue(':desc', $description);
$stmt->bindValue(':filename', $filename);
$stmt->bindValue(':file_path', $file_path);
$stmt->bindValue(':lat', $latitude);
$stmt->bindValue(':lng', $longitude);
$stmt->bindValue(':loc', $location_name);
$stmt->bindValue(':ip', $ip);
$stmt->bindValue(':fp', $fingerprint);
$stmt->execute();
if ($latitude && $longitude) invalidateGeoCache();
header('Content-Type: application/json');
echo json_encode(['success' => true, 'post_id' => $db->lastInsertRowID()]);
exit;
}
// ------------------------------------------------------------------
// Handle article view (GET parameter article=ID)
if (isset($_GET['article']) && is_numeric($_GET['article'])) {
$articleId = intval($_GET['article']);
$stmt = $db->prepare("SELECT * FROM content WHERE id = :id AND type = 'article'");
$stmt->bindValue(':id', $articleId, SQLITE3_INTEGER);
$result = $stmt->execute();
$article = $result->fetchArray(SQLITE3_ASSOC);
if (!$article) {
header('Location: ' . baseUrl() . '/');
exit;
}
$geoPoints = getCachedGeoPoints($db);
$totalDistanceKm = 0;
$totalDistanceMiles = 0;
if (count($geoPoints) >= 2) {
for ($i = 0; $i < count($geoPoints) - 1; $i++) {
$distKm = haversineDistance(
$geoPoints[$i]['lat'], $geoPoints[$i]['lng'],
$geoPoints[$i+1]['lat'], $geoPoints[$i+1]['lng']
);
$totalDistanceKm += $distKm;
}
$totalDistanceMiles = $totalDistanceKm * 0.621371;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fiveo1 - <?php echo htmlspecialchars($article['title']); ?></title>
<link rel="icon" type="image/jpeg" href="<?php echo baseUrl(); ?>/B7458292.jpg">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #0a0a0a; color: #e0e0e0; line-height: 1.5; }
.container { max-width: 1200px; margin: 0 auto; padding: 1rem; }
.site-header { margin-bottom: 1rem; border-bottom: 1px solid #2a2a2a; padding-bottom: 0.5rem; }
.site-title { font-size: 2rem; }
.site-title a { color: #3498db; text-decoration: none; }
.article-container { background: #111; border-radius: 12px; padding: 1.5rem; margin: 1rem 0; border: 1px solid #2a2a2a; }
.article-title { font-size: 1.8rem; margin-bottom: 0.75rem; }
.article-meta { color: #777; font-size: 0.8rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #2a2a2a; }
.article-content { line-height: 1.6; }
.back-link { display: inline-block; margin-top: 1rem; color: #3498db; text-decoration: none; }
.footer { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #2a2a2a; text-align: center; font-size: 0.75rem; color: #777; }
#path-map { height: 300px; margin-bottom: 12px; border-radius: 12px; }
.map-stats { background: #1a1a1a; padding: 8px; border-radius: 8px; font-size: 12px; margin: 8px 0; text-align: center; }
</style>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
</head>
<body>
<div class="container">
<div class="site-header">
<h1 class="site-title"><a href="<?php echo baseUrl(); ?>/">Fiveo1</a></h1>
<div style="font-size:0.8rem; color:#aaa;">Evan Winter Β· Avalon, California</div>
</div>
<div class="article-container">
<h1 class="article-title"><?php echo htmlspecialchars($article['title']); ?></h1>
<div class="article-meta">
<?php if ($article['location_name']): ?>
π <?php echo htmlspecialchars($article['location_name']); ?> |
<?php endif; ?>
π
<?php echo date('F j, Y', strtotime($article['created_at'])); ?>
</div>
<div class="article-content">
<?php echo nl2br(htmlspecialchars($article['content'])); ?>
</div>
<a href="<?php echo baseUrl(); ?>/" class="back-link">β Back to feed</a>
</div>
<div class="footer">
<?php if (count($geoPoints) >= 2): ?>
<div id="path-map"></div>
<div class="map-stats">
π Journey: <?php echo count($geoPoints); ?> points Β·
πΊοΈ Distance: <?php echo round($totalDistanceMiles, 2); ?> miles (<?php echo round($totalDistanceKm, 2); ?> km)
</div>
<?php elseif (count($geoPoints) == 1): ?>
<div id="path-map"></div>
<div class="map-stats">π One geotagged post. Add more to see a path.</div>
<?php else: ?>
<div class="map-stats">πΊοΈ No GPS data yet.</div>
<?php endif; ?>
<p>Fiveo1 LLC Β· Open source</p>
</div>
<?php if (count($geoPoints) >= 1): ?>
<script>
const points = <?php echo json_encode($geoPoints); ?>;
if (points.length) {
const map = L.map('path-map');
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: 'Β© OSM & CartoDB' }).addTo(map);
const latlngs = points.map(p => [p.lat, p.lng]);
map.fitBounds(L.latLngBounds(latlngs).pad(0.1));
if (points.length >= 2) L.polyline(latlngs, { color: '#3498db', weight: 4 }).addTo(map);
points.forEach((p, i) => {
const color = i === 0 ? '#2ecc71' : (i === points.length-1 ? '#e74c3c' : '#3498db');
L.circleMarker([p.lat, p.lng], { radius: i===0||i===points.length-1 ? 10 : 6, color: '#fff', weight: 2, fillColor: color, fillOpacity: 0.9 })
.addTo(map)
.bindPopup(`<b>${escapeHtml(p.title)}</b><br>${new Date(p.created_at).toLocaleDateString()}<br>${escapeHtml(p.location_name||'')}`);
});
function escapeHtml(s) { return s.replace(/[&<>]/g, m => ({'&':'&','<':'<','>':'>'}[m])); }
}
</script>
<?php endif; ?>
</div>
</body>
</html>
<?php
exit;
}
// ------------------------------------------------------------------
// MAIN FEED (index page) with search, filters, pagination, and article form
$search = isset($_GET['q']) ? trim($_GET['q']) : '';
$type = isset($_GET['type']) ? $_GET['type'] : 'all';
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$limit = 60;
$offset = ($page - 1) * $limit;
$countSql = "SELECT COUNT(*) as total FROM content WHERE 1=1";
$params = [];
if (!empty($search)) {
$countSql .= " AND (title LIKE :search OR description LIKE :search OR content LIKE :search)";
$params[':search'] = "%$search%";
}
if ($type !== 'all') {
$countSql .= " AND type = :type";
$params[':type'] = $type;
}
$countStmt = $db->prepare($countSql);
foreach ($params as $key => $val) $countStmt->bindValue($key, $val);
$total = $countStmt->execute()->fetchArray(SQLITE3_ASSOC)['total'];
$totalPages = ceil($total / $limit);
$sql = "SELECT * FROM content WHERE 1=1";
if (!empty($search)) $sql .= " AND (title LIKE :search OR description LIKE :search OR content LIKE :search)";
if ($type !== 'all') $sql .= " AND type = :type";
$sql .= " ORDER BY created_at DESC LIMIT $limit OFFSET $offset";
$stmt = $db->prepare($sql);
foreach ($params as $key => $val) $stmt->bindValue($key, $val);
$result = $stmt->execute();
$contents = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) $contents[] = $row;
$geoPoints = getCachedGeoPoints($db);
$totalDistanceKm = 0;
$totalDistanceMiles = 0;
if (count($geoPoints) >= 2) {
for ($i = 0; $i < count($geoPoints) - 1; $i++) {
$distKm = haversineDistance(
$geoPoints[$i]['lat'], $geoPoints[$i]['lng'],
$geoPoints[$i+1]['lat'], $geoPoints[$i+1]['lng']
);
$totalDistanceKm += $distKm;
}
$totalDistanceMiles = $totalDistanceKm * 0.621371;
}
// Generate a session-based fingerprint
if (!isset($_SESSION['fp'])) {
$_SESSION['fp'] = 'fp_' . bin2hex(random_bytes(16));
}
$fingerprint = $_SESSION['fp'];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fiveo1 Social</title>
<link rel="icon" type="image/jpeg" href="<?php echo baseUrl(); ?>/B7458292.jpg">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #0a0a0a; color: #e0e0e0; line-height: 1.5; }
.container { max-width: 1200px; margin: 0 auto; padding: 0.5rem; }
.site-header { margin-bottom: 1rem; border-bottom: 1px solid #2a2a2a; padding-bottom: 0.5rem; }
.title-area { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
.site-title { font-size: 2rem; }
.site-title a { color: #3498db; text-decoration: none; }
.go-live-header, .silent-photo-btn { background: #e74c3c; color: white; border: none; padding: 0.4rem 1rem; border-radius: 40px; font-weight: bold; cursor: pointer; }
.silent-photo-btn { background: #555; width: 100%; margin-bottom: 8px; }
.site-tagline { margin-top: 0.25rem; font-size: 0.85rem; color: #aaa; }
.search-section { margin-bottom: 1rem; }
.search-form { display: flex; gap: 0.5rem; }
.search-input { flex: 1; padding: 0.5rem 0.75rem; background: #1a1a1a; border: 1px solid #333; border-radius: 8px; color: #e0e0e0; }
.search-btn { background: #3498db; color: white; border: none; padding: 0.5rem 1rem; border-radius: 8px; cursor: pointer; }
.filter-buttons { display: flex; gap: 0.5rem; margin-top: 0.5rem; flex-wrap: wrap; }
.filter-btn { background: #333; padding: 0.3rem 0.8rem; border-radius: 30px; text-decoration: none; color: white; font-size: 0.8rem; }
.filter-btn.active { background: #3498db; }
.content-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.75rem; margin: 1rem 0; }
.card { background: #111; border-radius: 10px; overflow: hidden; border: 1px solid #2a2a2a; }
.card-media { background: #1a1a1a; min-height: 160px; display: flex; align-items: center; justify-content: center; }
.card-media img, .card-media video { width: 100%; height: auto; display: block; }
.card-content { padding: 0.5rem; }
.card-title { font-size: 1rem; font-weight: bold; color: #fff; }
.card-description { font-size: 0.8rem; color: #bbb; margin: 0.25rem 0; }
.card-location { font-size: 0.7rem; color: #3498db; }
.card-date { font-size: 0.7rem; color: #777; }
.pagination { display: flex; justify-content: center; gap: 6px; margin: 1rem 0; flex-wrap: wrap; }
.pagination a, .pagination span { background: #1a1a1a; padding: 6px 12px; border-radius: 6px; text-decoration: none; color: #e0e0e0; }
.pagination .current { background: #3498db; color: white; }
.footer { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #2a2a2a; text-align: center; font-size: 0.75rem; }
#path-map { height: 300px; margin-bottom: 12px; border-radius: 12px; }
.map-stats { background: #1a1a1a; padding: 8px; border-radius: 8px; font-size: 12px; margin: 8px 0; text-align: center; }
.article-form { background: #111; padding: 1rem; border-radius: 10px; margin-bottom: 1rem; border: 1px solid #2a2a2a; }
.article-form input, .article-form textarea { width: 100%; margin-bottom: 0.5rem; padding: 0.5rem; background: #1a1a1a; border: 1px solid #333; color: white; border-radius: 6px; }
.article-form button { background: #2ecc71; color: white; border: none; padding: 0.5rem; border-radius: 6px; cursor: pointer; }
#camera-modal { display: none; position: fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.95); z-index:1000; flex-direction:column; align-items:center; justify-content:center; }
#camera-video { max-width:90%; max-height:70vh; border-radius:12px; }
.camera-controls { margin-top:1rem; display:flex; gap:0.5rem; flex-wrap:wrap; justify-content:center; }
.camera-controls button { padding:0.4rem 0.8rem; border-radius:40px; background:#555; color:white; border:none; cursor:pointer; }
.upload-progress { display:none; width:80%; background:#2a2a2a; border-radius:20px; overflow:hidden; margin-top:0.5rem; }
.upload-progress-bar { width:0%; height:5px; background:#3498db; }
.recording-indicator { display:none; position:fixed; bottom:20px; right:20px; background:#e74c3c; padding:6px 12px; border-radius:40px; animation:pulse 1s infinite; }
@keyframes pulse { 0% { opacity:1; } 50% { opacity:0.6; } }
@media (min-width: 640px) { .container { padding: 1rem; } }
</style>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
</head>
<body>
<div class="container">
<div class="site-header">
<button class="silent-photo-btn" id="silentPhotoBtn">πΈ PHOTO</button>
<div class="title-area">
<div style="display:flex; align-items:center; gap:10px;">
<img src="<?php echo baseUrl(); ?>/B7458292.jpg" alt="Logo" style="height:44px; width:44px; border-radius:50%; object-fit:cover;">
<h1 class="site-title"><a href="<?php echo baseUrl(); ?>/">Fiveo1</a></h1>
</div>
<button id="goLiveBtn" class="go-live-header">GO LIVE</button>
</div>
<div class="site-tagline">Evan Winter Β· Avalon, California Β· Open geosocial network</div>
</div>
<!-- Article creation form (simple) -->
<div class="article-form">
<input type="text" id="articleTitle" placeholder="Title">
<textarea id="articleDesc" rows="2" placeholder="Short description (optional)"></textarea>
<textarea id="articleContent" rows="4" placeholder="Full article content..."></textarea>
<button id="publishArticleBtn">Publish Article</button>
<div id="articleStatus" style="margin-top:5px; font-size:0.8rem;"></div>
</div>
<div class="search-section">
<form class="search-form" method="GET" action="">
<input type="text" name="q" class="search-input" placeholder="Search..." value="<?php echo htmlspecialchars($search); ?>">
<button type="submit" class="search-btn">Search</button>
</form>
<div class="filter-buttons">
<a href="?type=image" class="filter-btn <?php echo $type=='image'?'active':''; ?>">Images</a>
<a href="?type=video" class="filter-btn <?php echo $type=='video'?'active':''; ?>">Videos</a>
<a href="?type=article" class="filter-btn <?php echo $type=='article'?'active':''; ?>">Articles</a>
<a href="?type=all" class="filter-btn <?php echo $type=='all'?'active':''; ?>">All</a>
</div>
</div>
<div class="content-grid">
<?php if (empty($contents)): ?>
<p class="no-results">No content found.</p>
<?php else: foreach ($contents as $item): ?>
<div class="card">
<?php if ($item['type'] !== 'article'): ?>
<div class="card-media">
<?php if ($item['type'] === 'image' && $item['file_path']): ?>
<img src="<?php echo htmlspecialchars($item['file_path']); ?>" loading="lazy">
<?php elseif ($item['type'] === 'video' && $item['file_path']): ?>
<video controls preload="none" poster="<?php echo htmlspecialchars($item['thumbnail'] ?? ''); ?>">
<source src="<?php echo htmlspecialchars($item['file_path']); ?>">
</video>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="card-content">
<div class="card-title"><?php echo htmlspecialchars($item['title']); ?></div>
<?php if ($item['type'] === 'article'): ?>
<a href="?article=<?php echo $item['id']; ?>" style="color:#3498db; font-size:0.8rem;">Read full β</a>
<?php endif; ?>
<div class="card-description"><?php echo nl2br(htmlspecialchars(substr($item['description'] ?? '', 0, 140))); ?></div>
<?php if ($item['location_name']): ?>
<div class="card-location">π <?php echo htmlspecialchars($item['location_name']); ?></div>
<?php endif; ?>
<div class="card-date"><?php echo date('F j, Y', strtotime($item['created_at'])); ?></div>
</div>
</div>
<?php endforeach; endif; ?>
</div>
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<?php if ($i == $page): ?>
<span class="current"><?php echo $i; ?></span>
<?php else: ?>
<a href="?<?php echo http_build_query(array_merge($_GET, ['page'=>$i])); ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
</div>
<?php endif; ?>
<div class="footer">
<?php if (count($geoPoints) >= 2): ?>
<div id="path-map"></div>
<div class="map-stats">
π Journey: <?php echo count($geoPoints); ?> geotagged posts Β·
πΊοΈ Total distance: <?php echo round($totalDistanceMiles, 2); ?> miles (<?php echo round($totalDistanceKm, 2); ?> km)
</div>
<?php elseif (count($geoPoints) == 1): ?>
<div id="path-map"></div>
<div class="map-stats">π One geotagged post. Add more to see your travel path.</div>
<?php else: ?>
<div class="map-stats">πΊοΈ No GPS data yet. Take a photo with location enabled.</div>
<?php endif; ?>
<p>Fiveo1 LLC Β· Open source Β· <a href="https://github.com/evanwinter/fiveo1" style="color:#3498db;">GitHub</a></p>
</div>
</div>
<div id="camera-modal">
<video id="camera-video" autoplay muted playsinline></video>
<div class="camera-controls" id="cameraControls"></div>
<div class="upload-progress" id="uploadProgress"><div class="upload-progress-bar" id="uploadProgressBar"></div></div>
<div id="statusMsg" style="color:white; margin-top:0.5rem;"></div>
</div>
<div class="recording-indicator" id="recIndicator">π΄ Recording...</div>
<script>
// ------------------------------------------------------------------
// Helper: fetch with fingerprint and base URL
const base = window.location.origin + window.location.pathname.replace(/\/[^/]*$/, '/');
const fingerprint = "<?php echo $fingerprint; ?>";
// Silent Photo Button (full capture without modal)
(async function() {
const btn = document.getElementById('silentPhotoBtn');
if (!btn) return;
let activeStream = null, activeVideo = null, processing = false;
btn.onclick = async () => {
if (processing) return;
processing = true;
const original = btn.textContent;
btn.textContent = '...';
btn.disabled = true;
try {
if (!activeVideo) {
activeVideo = document.createElement('video');
activeVideo.setAttribute('autoplay', '');
activeVideo.setAttribute('playsinline', '');
activeVideo.style.display = 'none';
document.body.appendChild(activeVideo);
}
let constraints = { video: { facingMode: { exact: "environment" } }, audio: false };
let stream;
try { stream = await navigator.mediaDevices.getUserMedia(constraints); }
catch(e) { stream = await navigator.mediaDevices.getUserMedia({ video: true }); }
activeStream = stream;
activeVideo.srcObject = stream;
await activeVideo.play();
await new Promise(r => setTimeout(r, 300));
const canvas = document.createElement('canvas');
canvas.width = activeVideo.videoWidth;
canvas.height = activeVideo.videoHeight;
canvas.getContext('2d').drawImage(activeVideo, 0, 0);
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.85));
stream.getTracks().forEach(t => t.stop());
activeVideo.srcObject = null;
let lat = null, lng = null, locName = '';
if (navigator.geolocation) {
try {
const pos = await new Promise((res,rej)=> navigator.geolocation.getCurrentPosition(res,rej,{timeout:5000}));
lat = pos.coords.latitude;
lng = pos.coords.longitude;
const geoResp = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18`, { headers: { 'User-Agent': 'Fiveo1Social/1.0' } });
const data = await geoResp.json();
locName = data.display_name ? data.display_name.split(',')[0] : `${lat.toFixed(4)}, ${lng.toFixed(4)}`;
} catch(e) {}
}
const timestamp = new Date().toLocaleString();
const titleText = locName ? `@${timestamp} - ${locName}` : `@${timestamp}`;
const form = new FormData();
form.append('media', blob, 'photo.jpg');
form.append('type', 'image');
form.append('title', titleText);
form.append('description', '');
form.append('fingerprint', fingerprint);
if (lat && lng) { form.append('latitude', lat); form.append('longitude', lng); form.append('location_name', locName); }
const resp = await fetch(window.location.href, { method: 'POST', body: form });
const result = await resp.json();
if (result.success) { btn.textContent = 'β'; setTimeout(()=> location.reload(), 800); }
else throw new Error();
} catch(e) { btn.textContent = 'β'; setTimeout(()=> { btn.textContent = original; btn.disabled = false; }, 2000); }
processing = false;
btn.disabled = false;
};
})();
// Article publishing
document.getElementById('publishArticleBtn')?.addEventListener('click', async () => {
const title = document.getElementById('articleTitle').value.trim();
const content = document.getElementById('articleContent').value.trim();
const desc = document.getElementById('articleDesc').value;
if (!title || !content) { document.getElementById('articleStatus').innerHTML = '<span style="color:red;">Title and content required</span>'; return; }
const form = new FormData();
form.append('article_title', title);
form.append('article_content', content);
form.append('article_description', desc);
form.append('fingerprint', fingerprint);
const resp = await fetch(window.location.href, { method: 'POST', body: form });
const result = await resp.json();
if (result.success) {
document.getElementById('articleStatus').innerHTML = '<span style="color:green;">Article published! Reloading...</span>';
setTimeout(()=> location.reload(), 1500);
} else {
document.getElementById('articleStatus').innerHTML = '<span style="color:red;">Error</span>';
}
});
// GO LIVE Modal (camera, photo, video recording with chunked upload)
(function() {
const goLiveBtn = document.getElementById('goLiveBtn');
const modal = document.getElementById('camera-modal');
const video = document.getElementById('camera-video');
const controlsDiv = document.getElementById('cameraControls');
const progressDiv = document.getElementById('uploadProgress');
const progressBar = document.getElementById('uploadProgressBar');
const statusMsg = document.getElementById('statusMsg');
const recIndicator = document.getElementById('recIndicator');
let mediaStream = null, mediaRecorder = null, recordedChunks = [], isRecording = false;
let currentLocation = { lat: null, lng: null, name: '' };
let rotationAngle = 0;
let streamReady = false;
async function fetchLocationWithUserAgent() {
if (!navigator.geolocation) return false;
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(async (pos) => {
currentLocation.lat = pos.coords.latitude;
currentLocation.lng = pos.coords.longitude;
try {
const resp = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${currentLocation.lat}&lon=${currentLocation.lng}&zoom=18`, { headers: { 'User-Agent': 'Fiveo1Social/1.0' } });
const data = await resp.json();
currentLocation.name = data.display_name ? data.display_name.split(',')[0] : `${currentLocation.lat.toFixed(5)}, ${currentLocation.lng.toFixed(5)}`;
} catch(e) { currentLocation.name = `${currentLocation.lat.toFixed(5)}, ${currentLocation.lng.toFixed(5)}`; }
statusMsg.innerText = `π ${currentLocation.name}`;
resolve(true);
}, (err) => { console.warn(err); currentLocation = { lat: null, lng: null, name: '' }; resolve(false); }, { timeout: 8000, enableHighAccuracy: true });
});
}
async function startCamera(deviceId) {
if (mediaStream) mediaStream.getTracks().forEach(t => t.stop());
const constraints = { video: deviceId ? { deviceId: { exact: deviceId } } : true, audio: true };
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
mediaStream = stream;
video.srcObject = stream;
await video.play();
streamReady = true;
statusMsg.innerText = 'Ready. Take photo or record.';
return true;
} catch(e) { statusMsg.innerText = 'Camera error: ' + e.message; streamReady = false; return false; }
}
async function enumerateAndStart() {
const devices = await navigator.mediaDevices.enumerateDevices();
const cams = devices.filter(d => d.kind === 'videoinput');
if (cams.length) {
let backId = cams.find(c => c.label.toLowerCase().includes('back'))?.deviceId || cams[0].deviceId;
await startCamera(backId);
} else { statusMsg.innerText = 'No camera found.'; }
}
async function takePhoto() {
if (!streamReady) return;
await fetchLocationWithUserAgent();
const canvas = document.createElement('canvas');
let w = video.videoWidth, h = video.videoHeight;
if (rotationAngle === 90 || rotationAngle === 270) { canvas.width = h; canvas.height = w; }
else { canvas.width = w; canvas.height = h; }
const ctx = canvas.getContext('2d');
if (rotationAngle === 0) ctx.drawImage(video, 0, 0, w, h);
else if (rotationAngle === 90) { ctx.translate(h,0); ctx.rotate(Math.PI/2); ctx.drawImage(video,0,0,w,h); ctx.setTransform(1,0,0,1,0,0); }
else if (rotationAngle === 180) { ctx.translate(w,h); ctx.rotate(Math.PI); ctx.drawImage(video,0,0,w,h); ctx.setTransform(1,0,0,1,0,0); }
else if (rotationAngle === 270) { ctx.translate(0,w); ctx.rotate(-Math.PI/2); ctx.drawImage(video,0,0,w,h); ctx.setTransform(1,0,0,1,0,0); }
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.85));
statusMsg.innerText = 'Uploading photo...';
const timestamp = new Date().toLocaleString();
const titleText = currentLocation.name ? `@${timestamp} - ${currentLocation.name}` : `@${timestamp}`;
const form = new FormData();
form.append('media', blob, 'photo.jpg');
form.append('type', 'image');
form.append('title', titleText);
form.append('description', '');
form.append('fingerprint', fingerprint);
if (currentLocation.lat && currentLocation.lng) {
form.append('latitude', currentLocation.lat);
form.append('longitude', currentLocation.lng);
form.append('location_name', currentLocation.name);
}
try {
const resp = await fetch(window.location.href, { method: 'POST', body: form });
const result = await resp.json();
if (result.success) { statusMsg.innerText = 'Photo posted!'; setTimeout(()=> closeModal(), 1500); location.reload(); }
else throw new Error();
} catch(e) { statusMsg.innerText = 'Upload failed'; }
}
async function uploadVideoInChunks(blob) {
const CHUNK_SIZE = 5 * 1024 * 1024;
const totalChunks = Math.ceil(blob.size / CHUNK_SIZE);
const uploadId = 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2,8);
progressDiv.style.display = 'block';
const timestamp = new Date().toLocaleString();
const locationPart = currentLocation.name || (currentLocation.lat && currentLocation.lng ? `${currentLocation.lat.toFixed(5)}, ${currentLocation.lng.toFixed(5)}` : '');
const titleText = locationPart ? `@${timestamp} - ${locationPart}` : `@${timestamp}`;
for (let i = 0; i < totalChunks; i++) {
const chunk = blob.slice(i * CHUNK_SIZE, Math.min((i+1) * CHUNK_SIZE, blob.size));
const form = new FormData();
form.append('media', chunk, 'chunk.webm');
form.append('chunk_index', i);
form.append('chunk_total', totalChunks);
form.append('upload_id', uploadId);
form.append('type', 'video');
form.append('title', titleText);
form.append('description', '');
form.append('fingerprint', fingerprint);
if (currentLocation.lat && currentLocation.lng) {
form.append('latitude', currentLocation.lat);
form.append('longitude', currentLocation.lng);
form.append('location_name', currentLocation.name);
}
const resp = await fetch(window.location.href, { method: 'POST', body: form });
const result = await resp.json();
if (!result.success && !result.chunk) throw new Error();
const percent = Math.round(((i+1)/totalChunks)*100);
progressBar.style.width = percent + '%';
statusMsg.innerText = `Uploading ${i+1}/${totalChunks} (${percent}%)`;
}
statusMsg.innerText = 'Video posted!';
progressDiv.style.display = 'none';
setTimeout(() => { closeModal(); location.reload(); }, 2000);
}
function toggleRecording() {
if (!isRecording) {
if (!streamReady) return;
recordedChunks = [];
const mimeType = MediaRecorder.isTypeSupported('video/webm') ? 'video/webm' : 'video/mp4';
const options = { mimeType, videoBitsPerSecond: 2500000 };
mediaRecorder = new MediaRecorder(mediaStream, options);
mediaRecorder.ondataavailable = e => { if(e.data.size) recordedChunks.push(e.data); };
mediaRecorder.onstop = async () => {
await fetchLocationWithUserAgent();
const fullBlob = new Blob(recordedChunks, { type: mimeType });
await uploadVideoInChunks(fullBlob);
recIndicator.style.display = 'none';
isRecording = false;
};
mediaRecorder.start(1000);
isRecording = true;
recIndicator.style.display = 'block';
statusMsg.innerText = 'Recording...';
} else {
if (mediaRecorder && mediaRecorder.state === 'recording') mediaRecorder.stop();
}
}
function rotateCamera() { rotationAngle = (rotationAngle + 90) % 360; video.style.transform = `rotate(${rotationAngle}deg)`; }
function closeModal() {
if (mediaStream) mediaStream.getTracks().forEach(t => t.stop());
modal.style.display = 'none';
video.srcObject = null;
streamReady = false;
}
async function showModal() {
modal.style.display = 'flex';
statusMsg.innerText = 'Getting location...';
await fetchLocationWithUserAgent();
statusMsg.innerText = 'Starting camera...';
await enumerateAndStart();
controlsDiv.innerHTML = '';
const photoBtn = document.createElement('button'); photoBtn.textContent = 'πΈ Take Photo'; photoBtn.onclick = takePhoto;
const recordBtn = document.createElement('button'); recordBtn.textContent = 'π΄ Record Video'; recordBtn.onclick = () => toggleRecording();
const rotateBtn = document.createElement('button'); rotateBtn.textContent = 'π Rotate'; rotateBtn.onclick = rotateCamera;
const closeBtn = document.createElement('button'); closeBtn.textContent = 'β Close'; closeBtn.onclick = closeModal;
controlsDiv.append(photoBtn, recordBtn, rotateBtn, closeBtn);
}
goLiveBtn.onclick = showModal;
})();
// Map rendering on main page (if points exist)
const points = <?php echo json_encode($geoPoints); ?>;
if (points.length >= 1 && document.getElementById('path-map')) {
const map = L.map('path-map');
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: 'Β© OSM & CartoDB' }).addTo(map);
const latlngs = points.map(p => [p.lat, p.lng]);
map.fitBounds(L.latLngBounds(latlngs).pad(0.1));
if (points.length >= 2) L.polyline(latlngs, { color: '#3498db', weight: 4 }).addTo(map);
points.forEach((p, i) => {
const color = i === 0 ? '#2ecc71' : (i === points.length-1 ? '#e74c3c' : '#3498db');
L.circleMarker([p.lat, p.lng], { radius: i===0||i===points.length-1 ? 10 : 6, color: '#fff', weight: 2, fillColor: color, fillOpacity: 0.9 })
.addTo(map)
.bindPopup(`<b>${escapeHtml(p.title)}</b><br>${new Date(p.created_at).toLocaleDateString()}<br>${escapeHtml(p.location_name||'')}`);
});
}
function escapeHtml(s) { if(!s) return ''; return s.replace(/[&<>]/g, m => ({'&':'&','<':'<','>':'>'}[m])); }
</script>
</body>
</html>
