index.php

<?php
// ============================================================
// Fiveo1 – Header + Feed (pagination, search, filters) + Footer Map
// ============================================================
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);

$db = new SQLite3(__DIR__ . '/db.sqlite');
$db->busyTimeout(30000);

$cols = $db->query("PRAGMA table_info(content)");
$existing = [];
while ($c = $cols->fetchArray(SQLITE3_ASSOC)) $existing[] = $c['name'];
foreach (['content', 'thumbnail', 'ip_address', 'fingerprint'] as $col) {
if (!in_array($col, $existing)) $db->exec("ALTER TABLE content ADD COLUMN $col TEXT");
}

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;
}

// ------------------------------------------------------------------
// HANDLE UPLOADS (single image from PHOTO, chunked video from GO LIVE)
// ------------------------------------------------------------------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['media'])) {
$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'] ?? '';
$fingerprint = $_POST['fingerprint'] ?? '';
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$type = $_POST['type'] ?? '';
$isChunked = isset($_POST['chunk_index']) && isset($_POST['upload_id']) && isset($_POST['chunk_total']);

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;
}
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));
}
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'];
$type = $meta['type'];
$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 = '';
$web_path = '';
$thumbnail = null;
$filename = '';
if ($type === 'video') {
$upload_dir = __DIR__ . '/uploads/videos/';
$web_path = '/uploads/videos/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true);
$ext = 'mp4';
$filename = uniqid('live_') . '.' . $ext;
$finalPath = $upload_dir . $filename;
rename($tempFinal, $finalPath);
$file_path = $web_path . $filename;
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) . " 2>&1";
exec($cmd);
}
} else {
http_response_code(400);
echo json_encode(['error' => 'Invalid media type for chunked upload']);
exit;
}
$stmt = $db->prepare("INSERT INTO content (type, title, description, filename, file_path, thumbnail, latitude, longitude, location_name, created_at, ip_address, fingerprint)
VALUES (:type, :title, :description, :filename, :file_path, :thumbnail, :lat, :lng, :loc, datetime('now'), :ip, :fp)");
$stmt->bindValue(':type', $type);
$stmt->bindValue(':title', $title);
$stmt->bindValue(':description', $description);
$stmt->bindValue(':filename', $filename);
$stmt->bindValue(':file_path', $file_path);
$stmt->bindValue(':thumbnail', $thumbnail);
$stmt->bindValue(':lat', $latitude);
$stmt->bindValue(':lng', $longitude);
$stmt->bindValue(':loc', $location_name);
$stmt->bindValue(':ip', $ip);
$stmt->bindValue(':fp', $fingerprint);
$stmt->execute();
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;
}
}

if ($_FILES['media']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit('Upload failed');
}
if ($type !== 'image') {
http_response_code(400);
exit('Invalid media type for single upload');
}
$upload_dir = __DIR__ . '/uploads/images/';
$web_path = '/uploads/images/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true);
$ext = 'jpg';
$filename = uniqid('photo_') . '.' . $ext;
$file_path = $web_path . $filename;
move_uploaded_file($_FILES['media']['tmp_name'], $upload_dir . $filename);

$stmt = $db->prepare("INSERT INTO content (type, title, description, filename, file_path, thumbnail, latitude, longitude, location_name, created_at, ip_address, fingerprint)
VALUES (:type, :title, :description, :filename, :file_path, :thumbnail, :lat, :lng, :loc, datetime('now'), :ip, :fp)");
$stmt->bindValue(':type', $type);
$stmt->bindValue(':title', $title);
$stmt->bindValue(':description', $description);
$stmt->bindValue(':filename', $filename);
$stmt->bindValue(':file_path', $file_path);
$stmt->bindValue(':thumbnail', null);
$stmt->bindValue(':lat', $latitude);
$stmt->bindValue(':lng', $longitude);
$stmt->bindValue(':loc', $location_name);
$stmt->bindValue(':ip', $ip);
$stmt->bindValue(':fp', $fingerprint);
$stmt->execute();

header('Content-Type: application/json');
echo json_encode(['success' => true, 'post_id' => $db->lastInsertRowID()]);
exit;
}

// ------------------------------------------------------------------
// MAIN FEED WITH PAGINATION
// ------------------------------------------------------------------
$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;

// Count total matching records
$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);

// Fetch current page
$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;

// ------------------------------------------------------------------
// FOOTER MAP & JOURNEY STATS (standalone)
// ------------------------------------------------------------------
$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);
$geoPoints = [];
$totalDistanceKm = 0;
$totalDistanceMiles = 0;

while ($row = $geoResult->fetchArray(SQLITE3_ASSOC)) {
$geoPoints[] = [
'id' => $row['id'],
'title' => $row['title'],
'lat' => (float)$row['latitude'],
'lng' => (float)$row['longitude'],
'location_name' => $row['location_name'],
'created_at' => $row['created_at']
];
}

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;
}

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 Β· Evan Winter</title>
<link rel="icon" type="image/jpeg" href="/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: 2rem 1rem; }
.site-header { margin-bottom: 2rem; border-bottom: 1px solid #2a2a2a; padding-bottom: 1rem; }
.title-area { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; }
.site-title { font-size: 2.2rem; letter-spacing: -0.5px; }
.site-title a { color: #3498db; text-decoration: none; }
.go-live-header { background: #e74c3c; color: white; border: none; padding: 0.5rem 1.2rem; border-radius: 40px; font-weight: bold; cursor: pointer; transition: background 0.2s; }
.go-live-header:hover { background: #c0392b; }
.site-tagline { margin-top: 0.5rem; font-size: 0.9rem; color: #aaa; }

/* Search section - full width, no overflow */
.search-section {
margin-bottom: 2rem;
width: 100%;
}
.search-form {
display: flex;
gap: 0.5rem;
width: 100%;
}
.search-input {
flex: 1;
min-width: 0;
padding: 0.75rem 1rem;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
color: #e0e0e0;
font-size: 1rem;
}
.search-btn {
flex-shrink: 0;
background: #3498db;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
white-space: nowrap;
}
.search-btn:hover {
background: #2980b9;
}
.filter-buttons {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
width: 100%;
}
.filter-row {
display: flex;
gap: 8px;
width: 100%;
}
.filter-btn {
flex: 1;
text-align: center;
background: #333;
color: white;
padding: 0.4rem 1rem;
border-radius: 30px;
text-decoration: none;
font-size: 0.85rem;
transition: all 0.2s;
}
.filter-btn.active, .filter-btn:hover {
background: #3498db;
}

.content-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.5rem; margin: 2rem 0; }
.card { background: #111; border-radius: 12px; overflow: hidden; transition: transform 0.2s; border: 1px solid #2a2a2a; }
.card:hover { transform: translateY(-4px); box-shadow: 0 10px 20px rgba(0,0,0,0.3); }
.card-media { position: relative; background: #1a1a1a; min-height: 180px; display: flex; align-items: center; justify-content: center; }
.card-media img, .card-media video { width: 100%; height: auto; display: block; }
.card-content { padding: 1rem; }
.card-title { font-size: 1.2rem; font-weight: bold; margin-bottom: 0.5rem; color: #fff; }
.card-description { font-size: 0.9rem; color: #bbb; margin-bottom: 0.75rem; line-height: 1.4; }
.card-location { font-size: 0.8rem; color: #3498db; margin-bottom: 0.5rem; }
.card-date { font-size: 0.75rem; color: #777; }
.no-results { text-align: center; padding: 3rem; color: #aaa; }

/* Pagination - arrows only */
.pagination {
display: flex;
justify-content: center;
gap: 8px;
margin: 2rem 0;
flex-wrap: wrap;
}
.pagination a, .pagination span {
background: #1a1a1a;
padding: 8px 16px;
border-radius: 6px;
text-decoration: none;
color: #e0e0e0;
transition: background 0.2s;
font-size: 1rem;
}
.pagination a:hover {
background: #3498db;
color: white;
}
.pagination .current {
background: #3498db;
color: white;
}
.pagination .disabled {
opacity: 0.5;
pointer-events: none;
}
.pagination .arrow {
font-size: 1.2rem;
font-weight: bold;
}

#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; backdrop-filter:blur(4px); }
#camera-video { max-width:90%; max-height:70vh; border-radius:12px; transition:transform 0.2s; }
.camera-controls-panel { margin-top:1rem; display:flex; flex-direction:column; align-items:center; gap:0.8rem; }
.controls { display:flex; gap:0.8rem; flex-wrap:wrap; justify-content:center; background:#111; padding:0.8rem 1.2rem; border-radius:60px; }
.controls button { padding:0.5rem 1rem; font-size:1rem; border:none; border-radius:40px; cursor:pointer; font-weight:bold; background:#555; color:white; transition:background 0.2s; }
.controls button:hover { background:#777; }
.upload-progress { display:none; width:80%; background:#2a2a2a; border-radius:20px; overflow:hidden; }
.upload-progress-bar { width:0%; height:6px; background:#3498db; transition:width 0.3s; }
.step { color:white; background:rgba(0,0,0,0.6); padding:0.3rem 1rem; border-radius:40px; font-size:0.9rem; }
.recording-indicator { display:none; position:fixed; bottom:20px; right:20px; background:#e74c3c; color:white; padding:8px 16px; border-radius:40px; font-weight:bold; z-index:1001; animation:pulse 1s infinite; }
@keyframes pulse { 0% { opacity:1; } 50% { opacity:0.6; } 100% { opacity:1; } }

/* Footer map styles */
.footer { margin-top:3rem; padding-top:2rem; border-top:1px solid #2a2a2a; text-align:center; font-size:0.8rem; color:#777; }
#path-map { height:400px; margin-bottom:20px; border-radius:12px; overflow:hidden; }
.map-stats { background:#1a1a1a; padding:10px 16px; border-radius:8px; font-size:14px; margin:12px 0; text-align:center; }
.no-map-message { background:#1a1a1a; padding:30px; text-align:center; border-radius:12px; color:#aaa; }
</style>
<?php if (count($geoPoints) >= 1): ?>
<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>
<?php endif; ?>
</head>
<body>
<div class="container">
<!-- ========== HEADER ========== -->
<div class="site-header">
<div style="margin-bottom: 12px;">
<button id="silentPhotoBtn" style="background:#555; color:white; border:none; padding:0.5rem 1rem; border-radius:40px; font-weight:bold; cursor:pointer; width:100%;">PHOTO</button>
</div>
<div class="title-area">
<div style="display: flex; align-items: center; gap: 12px;">
<img src="/B7458292.jpg" alt="Fiveo1 Falcon" style="height: 50px; width: 50px; object-fit: cover; border-radius: 50%;">
<h1 class="site-title"><a href="/">Fiveo1</a></h1>
</div>
<button id="goLiveBtn" class="go-live-header">GO LIVE</button>
</div>
<div class="site-tagline">Evan Winter Β· Avalon, California Β· Global Intelligence & Analysis</div>
</div>

<!-- ========== FEED SECTION ========== -->
<div class="search-section">
<form class="search-form" method="GET" action="">
<input type="text" name="q" class="search-input" placeholder="Search articles, analysis, resources..." value="<?php echo htmlspecialchars($search); ?>">
<button type="submit" class="search-btn">Search</button>
</form>
<div class="filter-buttons">
<div class="filter-row">
<a href="?<?php echo http_build_query(array_merge($_GET, ['type'=>'image', 'page'=>1])); ?>" class="filter-btn <?php echo $type === 'image' ? 'active' : ''; ?>">Images</a>
<a href="?<?php echo http_build_query(array_merge($_GET, ['type'=>'video', 'page'=>1])); ?>" class="filter-btn <?php echo $type === 'video' ? 'active' : ''; ?>">Videos</a>
</div>
<div class="filter-row">
<a href="?<?php echo http_build_query(array_merge($_GET, ['type'=>'article', 'page'=>1])); ?>" class="filter-btn <?php echo $type === 'article' ? 'active' : ''; ?>">Articles</a>
<a href="?<?php echo http_build_query(array_merge($_GET, ['type'=>'all', 'page'=>1])); ?>" class="filter-btn <?php echo $type === 'all' ? 'active' : ''; ?>">All</a>
</div>
</div>
</div>

<div class="content-grid">
<?php if (empty($contents)): ?>
<p class="no-results">No content found.</p>
<?php else: ?>
<?php 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']); ?>" alt="<?php echo htmlspecialchars($item['title']); ?>" 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']); ?>" type="video/mp4">
</video>
<?php else: ?>
<div style="background:#1a1a1a; padding:2rem; text-align:center; color:#888;"></div>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="card-content">
<div class="card-title"><?php echo htmlspecialchars($item['title']); ?></div>
<?php if ($item['type'] === 'article'): ?>
<div class="card-read-link" style="margin: 4px 0 8px 0;">
<a href="/?article=<?php echo intval($item['id']); ?>" style="color: #3498db; text-decoration: none; font-weight: 500;">Read full article β†’</a>
</div>
<?php endif; ?>
<div class="card-description"><?php echo nl2br(htmlspecialchars(substr($item['description'] ?? '', 0, 180))); ?><?php echo (strlen($item['description'] ?? '') > 180) ? '...' : ''; ?></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; ?>
<?php endif; ?>
</div>

<!-- ========== PAGINATION (ONLY ARROWS) ========== -->
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php
$currentParams = $_GET;
function paginationUrl($params, $newPage) {
$params['page'] = $newPage;
return '?' . http_build_query($params);
}
?>
<?php if ($page > 1): ?>
<a href="<?php echo paginationUrl($currentParams, $page-1); ?>" class="arrow">Β«</a>
<?php else: ?>
<span class="disabled arrow">Β«</span>
<?php endif; ?>

<?php
$start = max(1, $page - 2);
$end = min($totalPages, $page + 2);
for ($i = $start; $i <= $end; $i++):
?>
<?php if ($i == $page): ?>
<span class="current"><?php echo $i; ?></span>
<?php else: ?>
<a href="<?php echo paginationUrl($currentParams, $i); ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>

<?php if ($page < $totalPages): ?>
<a href="<?php echo paginationUrl($currentParams, $page+1); ?>" class="arrow">Β»</a>
<?php else: ?>
<span class="disabled arrow">Β»</span>
<?php endif; ?>
</div>
<?php endif; ?>

<!-- ========== CAMERA MODAL ========== -->
<div id="camera-modal">
<video id="camera-video" autoplay muted playsinline></video>
<div class="camera-controls-panel">
<div style="display: flex; gap: 12px; justify-content: center; margin-bottom: 12px;">
<select id="cameraSelect" style="background:#1a1a1a; color:white; border:1px solid #555; border-radius:40px; padding:0.5rem 1rem;"></select>
<select id="videoQuality" style="background:#1a1a1a; color:white; border:1px solid #555; border-radius:40px; padding:0.5rem 1rem;">
<option value="high">High</option>
<option value="medium" selected>Medium</option>
<option value="low">Low</option>
</select>
</div>
<div class="controls" id="controlsDiv"></div>
<div class="upload-progress" id="uploadProgress">
<div class="upload-progress-bar" id="uploadProgressBar"></div>
</div>
<div id="statusMsg" class="step"></div>
</div>
</div>
<div class="recording-indicator" id="recIndicator">Recording...</div>

<!-- ========== FOOTER MODULE ========== -->
<div class="footer">
<?php if (count($geoPoints) >= 2): ?>
<div id="path-map"></div>
<div class="map-stats">
πŸ“ Chronological journey: <strong><?php echo count($geoPoints); ?> geotagged posts</strong> Β·
πŸ—ΊοΈ Total path distance: <strong><?php echo round($totalDistanceMiles, 2); ?> miles</strong> (<?php echo round($totalDistanceKm, 2); ?> km)<br>
🟒 Start: <?php echo htmlspecialchars($geoPoints[0]['title']); ?> (<?php echo date('M j, Y', strtotime($geoPoints[0]['created_at'])); ?>) &nbsp;|&nbsp;
πŸ”΄ Current: <?php echo htmlspecialchars($geoPoints[count($geoPoints)-1]['title']); ?> (<?php echo date('M j, Y', strtotime($geoPoints[count($geoPoints)-1]['created_at'])); ?>)
</div>
<?php elseif (count($geoPoints) == 1): ?>
<div id="path-map"></div>
<div class="map-stats">πŸ“ One geotagged post found. Add more locations to create a travel path.</div>
<?php else: ?>
<div class="no-map-message">πŸ—ΊοΈ No GPS coordinates available yet. Start adding posts with location data to see a detailed map path.</div>
<?php endif; ?>
<p>Fiveo1 LLC Β· Evan Winter Β· Avalon, California 312786</p>
<p>Global financial intelligence Β· Independent analysis</p>
</div>
<?php if (count($geoPoints) >= 1): ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const points = <?php echo json_encode($geoPoints); ?>;
if (points.length === 0) return;
const latLngs = points.map(p => [p.lat, p.lng]);
const map = L.map('path-map');
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; CartoDB',
subdomains: 'abcd', maxZoom: 19, minZoom: 2
}).addTo(map);
map.fitBounds(L.latLngBounds(latLngs).pad(0.1));
if (points.length >= 2) {
L.polyline(latLngs, { color: '#3498db', weight: 4, opacity: 0.8 }).addTo(map);
}
points.forEach((point, index) => {
const isStart = index === 0;
const isEnd = index === points.length - 1;
const popup = `<strong>${escapeHtml(point.title)}</strong><br>πŸ“… ${new Date(point.created_at).toLocaleDateString()}<br>πŸ“ ${escapeHtml(point.location_name || 'Unknown')}<br><small>${index+1}/${points.length}</small>`;
if (isStart) {
L.circleMarker([point.lat, point.lng], { radius: 10, fillColor: '#2ecc71', color: '#fff', weight: 2, fillOpacity: 1 }).addTo(map).bindPopup(popup).openPopup();
} else if (isEnd) {
L.circleMarker([point.lat, point.lng], { radius: 10, fillColor: '#e74c3c', color: '#fff', weight: 2, fillOpacity: 1 }).addTo(map).bindPopup(popup).openPopup();
} else {
L.circleMarker([point.lat, point.lng], { radius: 6, fillColor: '#3498db', color: '#fff', weight: 2, fillOpacity: 0.8 }).addTo(map).bindPopup(popup);
}
});
function escapeHtml(s) { if(!s) return ''; return s.replace(/[&<>]/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[m])); }
});
</script>
<?php endif; ?>
</div>

<!-- ========== JAVASCRIPT (PHOTO, GO LIVE) ========== -->
<script>
// ============================================================
// IMMEDIATE PERMISSION REQUEST (on page load)
// ============================================================
(async function requestAllPermissions() {
try {
const tempStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
tempStream.getTracks().forEach(track => track.stop());
} catch (err) {}
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(() => {}, () => {}, { timeout: 10000 });
}
})();

// ============================================================
// PHOTO BUTTON
// ============================================================
(function() {
const photoBtn = document.getElementById('silentPhotoBtn');
if (!photoBtn) return;

let activeStream = null;
let activeVideo = null;
let isProcessing = false;

async function captureAndPost() {
if (isProcessing) return;
isProcessing = true;
const originalText = photoBtn.textContent;
photoBtn.textContent = '...';
photoBtn.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 = null;
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
} catch (e) {
constraints = { video: true, audio: false };
stream = await navigator.mediaDevices.getUserMedia(constraints);
}
activeStream = stream;
activeVideo.srcObject = stream;
await activeVideo.play();
await new Promise(r => setTimeout(r, 300));
if (activeVideo.videoWidth === 0) await new Promise(r => setTimeout(r, 500));

const canvas = document.createElement('canvas');
canvas.width = activeVideo.videoWidth;
canvas.height = activeVideo.videoHeight;
canvas.getContext('2d').drawImage(activeVideo, 0, 0, canvas.width, canvas.height);
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.85));

if (activeStream) {
activeStream.getTracks().forEach(track => track.stop());
activeStream = null;
}
activeVideo.srcObject = null;

if (!blob) throw new Error('Capture failed');

let lat = null, lng = null, locName = '';
if (navigator.geolocation) {
try {
const pos = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 5000 });
});
lat = pos.coords.latitude;
lng = pos.coords.longitude;
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18`);
const data = await res.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 formData = new FormData();
formData.append('media', blob, 'photo.jpg');
formData.append('type', 'image');
formData.append('title', titleText);
formData.append('description', '');
formData.append('fingerprint', sessionStorage.getItem('fp') || '');
if (lat && lng) {
formData.append('latitude', lat);
formData.append('longitude', lng);
formData.append('location_name', locName);
}

const response = await fetch(window.location.href, { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
photoBtn.textContent = 'βœ“';
setTimeout(() => location.reload(), 800);
} else {
throw new Error('Upload failed');
}
} catch (err) {
console.error(err);
photoBtn.textContent = 'βœ—';
setTimeout(() => {
photoBtn.textContent = originalText;
photoBtn.disabled = false;
}, 2000);
} finally {
isProcessing = false;
photoBtn.disabled = false;
}
}

photoBtn.addEventListener('click', captureAndPost);
})();

// ============================================================
// GO LIVE SCRIPT (location in title using coordinates)
// ============================================================
(function() {
const goLiveBtn = document.getElementById('goLiveBtn');
if (!goLiveBtn) return;

const modal = document.getElementById('camera-modal');
const video = document.getElementById('camera-video');
const controlsDiv = document.getElementById('controlsDiv');
const uploadProgress = document.getElementById('uploadProgress');
const uploadProgressBar = document.getElementById('uploadProgressBar');
const statusMsg = document.getElementById('statusMsg');
const cameraSelect = document.getElementById('cameraSelect');
const recIndicator = document.getElementById('recIndicator');

let mediaStream = null;
let mediaRecorder = null;
let recordedChunks = [];
let isRecording = false;
let currentUploadId = null;
let currentLocation = { lat: null, lng: null, name: '' };
let currentFingerprint = '';
let availableCameras = [];
let rotationAngle = 0;
let streamReady = false;
let modalOpen = false;
let locationReady = false;
let locationPromise = null;
const CHUNK_SIZE = 5 * 1024 * 1024;

function applyRotation() {
video.style.transform = 'rotate(' + rotationAngle + 'deg)';
if (rotationAngle === 90 || rotationAngle === 270) {
video.style.width = 'auto';
video.style.height = '70vh';
} else {
video.style.width = 'auto';
video.style.maxHeight = '70vh';
}
}

function rotateCamera() {
rotationAngle = (rotationAngle + 90) % 360;
applyRotation();
statusMsg.innerText = 'Rotated ' + rotationAngle + 'Β°';
}

async function getFingerprint() {
if (currentFingerprint) return currentFingerprint;
if (!sessionStorage.getItem('fp')) {
sessionStorage.setItem('fp', 'fp_' + Math.random().toString(36).substr(2, 16));
}
currentFingerprint = sessionStorage.getItem('fp');
return currentFingerprint;
}

async function fetchLocation() {
return new Promise((resolve) => {
if (!navigator.geolocation) {
currentLocation = { lat: null, lng: null, name: '' };
statusMsg.innerText = '⚠️ Geolocation not supported';
resolve(false);
return;
}
navigator.geolocation.getCurrentPosition(async (pos) => {
currentLocation.lat = pos.coords.latitude;
currentLocation.lng = pos.coords.longitude;
currentLocation.name = `${currentLocation.lat.toFixed(5)}, ${currentLocation.lng.toFixed(5)}`;
statusMsg.innerText = `πŸ“ Location: ${currentLocation.name}`;
resolve(true);
}, (err) => {
console.error('Geolocation error:', err.message);
let errorMsg = '';
switch(err.code) {
case err.PERMISSION_DENIED: errorMsg = 'Location permission denied.'; break;
case err.POSITION_UNAVAILABLE: errorMsg = 'Position unavailable.'; break;
case err.TIMEOUT: errorMsg = 'Location timeout.'; break;
default: errorMsg = 'Location error.';
}
statusMsg.innerText = `❌ ${errorMsg}`;
currentLocation = { lat: null, lng: null, name: '' };
resolve(false);
}, { timeout: 8000, enableHighAccuracy: true });
});
}

async function ensureLocation() {
if (locationReady) return true;
if (!locationPromise) {
locationPromise = fetchLocation();
}
const success = await locationPromise;
locationReady = success;
return success;
}

async function enumerateCameras() {
const devices = await navigator.mediaDevices.enumerateDevices();
availableCameras = devices.filter(d => d.kind === 'videoinput');
cameraSelect.innerHTML = '';
if (availableCameras.length === 0) {
cameraSelect.style.display = 'none';
return;
}
cameraSelect.style.display = 'block';
availableCameras.forEach((cam, idx) => {
const option = document.createElement('option');
option.value = cam.deviceId;
let label = cam.label || 'Camera ' + (idx + 1);
if (label.toLowerCase().includes('front')) label = 'Front Camera';
else if (label.toLowerCase().includes('back')) label = 'Back Camera';
else if (label.toLowerCase().includes('environment')) label = 'Back Camera';
else if (label.toLowerCase().includes('user')) label = 'Front Camera';
option.textContent = label;
cameraSelect.appendChild(option);
});
let backIndex = 0;
for (let i = 0; i < availableCameras.length; i++) {
if (availableCameras[i].label.toLowerCase().includes('back')) {
backIndex = i;
break;
}
}
cameraSelect.value = availableCameras[backIndex].deviceId;
}

async function startCamera(deviceId) {
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
video.srcObject = null;
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 (err) {
statusMsg.innerText = 'Camera error: ' + err.message;
streamReady = false;
return false;
}
}

async function switchToCamera(deviceId) {
if (!deviceId) return;
statusMsg.innerText = 'Switching camera...';
await startCamera(deviceId);
rotationAngle = 0;
applyRotation();
}

async function takePhoto() {
if (!streamReady || !mediaStream || video.videoWidth === 0) {
statusMsg.innerText = 'Camera not ready.';
return;
}
await ensureLocation();
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));
if (!blob) return;
statusMsg.innerText = 'Uploading photo...';
const timestamp = new Date().toLocaleString();
const titleText = currentLocation.name ? `@${timestamp} - ${currentLocation.name}` : `@${timestamp}`;
const formData = new FormData();
formData.append('media', blob, 'photo.jpg');
formData.append('type', 'image');
formData.append('title', titleText);
formData.append('description', '');
formData.append('fingerprint', currentFingerprint);
if (currentLocation.lat && currentLocation.lng) {
formData.append('latitude', currentLocation.lat);
formData.append('longitude', currentLocation.lng);
formData.append('location_name', currentLocation.name);
}
try {
const resp = await fetch(window.location.href, { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
statusMsg.innerText = 'Photo posted!';
setTimeout(() => { closeModal(); location.reload(); }, 1500);
} else throw new Error();
} catch(e) { statusMsg.innerText = 'Upload failed'; }
}

function toggleRecording() {
if (!isRecording) {
if (!streamReady || !mediaStream) {
statusMsg.innerText = 'No camera stream';
return;
}
recordedChunks = [];
const mimeType = MediaRecorder.isTypeSupported('video/webm') ? 'video/webm' : 'video/mp4';
const quality = document.getElementById('videoQuality').value;
let bitrate = 2500000;
if (quality === 'high') bitrate = 5000000;
if (quality === 'low') bitrate = 1000000;
const options = { mimeType: mimeType, videoBitsPerSecond: bitrate, audioBitsPerSecond: 128000 };
try {
mediaRecorder = new MediaRecorder(mediaStream, options);
} catch(e) {
mediaRecorder = new MediaRecorder(mediaStream);
}
mediaRecorder.ondataavailable = function(event) {
if (event.data && event.data.size > 0) recordedChunks.push(event.data);
};
mediaRecorder.onstop = async function() {
await ensureLocation();
const fullBlob = new Blob(recordedChunks, { type: mimeType });
uploadVideoInChunks(fullBlob);
recIndicator.style.display = 'none';
isRecording = false;
recordBtn.textContent = 'Record Video';
recordBtn.style.background = '#555';
};
mediaRecorder.start(1000);
isRecording = true;
recordBtn.textContent = 'Stop Recording';
recordBtn.style.background = '#e74c3c';
recIndicator.style.display = 'block';
statusMsg.innerText = 'Recording...';
} else {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
statusMsg.innerText = 'Stopping, uploading...';
}
}
}

async function uploadVideoInChunks(blob) {
const totalChunks = Math.ceil(blob.size / CHUNK_SIZE);
currentUploadId = 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2,8);
uploadProgress.style.display = 'block';
const timestamp = new Date().toLocaleString();
const locationPart = currentLocation.name ? 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 formData = new FormData();
formData.append('media', chunk, 'chunk.webm');
formData.append('chunk_index', i);
formData.append('chunk_total', totalChunks);
formData.append('upload_id', currentUploadId);
formData.append('type', 'video');
formData.append('title', titleText);
formData.append('description', '');
formData.append('fingerprint', currentFingerprint);
if (currentLocation.lat && currentLocation.lng) {
formData.append('latitude', currentLocation.lat);
formData.append('longitude', currentLocation.lng);
formData.append('location_name', currentLocation.name);
}
try {
const resp = await fetch(window.location.href, { method: 'POST', body: formData });
const result = await resp.json();
if (!result.success && !result.chunk) throw new Error();
const percent = Math.round(((i+1)/totalChunks)*100);
uploadProgressBar.style.width = percent + '%';
statusMsg.innerText = 'Uploading ' + (i+1) + '/' + totalChunks + ' (' + percent + '%)';
} catch(e) {
statusMsg.innerText = 'Upload failed';
uploadProgress.style.display = 'none';
return;
}
}
statusMsg.innerText = 'Video posted!';
uploadProgress.style.display = 'none';
setTimeout(() => { closeModal(); location.reload(); }, 2000);
}

function closeModal() {
if (mediaStream) {
mediaStream.getTracks().forEach(t => t.stop());
mediaStream = null;
}
modal.style.display = 'none';
video.srcObject = null;
uploadProgress.style.display = 'none';
recIndicator.style.display = 'none';
isRecording = false;
streamReady = false;
modalOpen = false;
locationReady = false;
locationPromise = null;
}

async function showCameraModal() {
if (modalOpen) return;
modalOpen = true;
modal.style.display = 'flex';
statusMsg.innerText = 'Getting location...';
await ensureLocation();
statusMsg.innerText = 'Loading cameras...';
await getFingerprint();
await enumerateCameras();
if (availableCameras.length > 0) {
await startCamera(cameraSelect.value);
} else {
statusMsg.innerText = 'No camera found.';
}
controlsDiv.innerHTML = '';
const takePhotoBtn = document.createElement('button');
takePhotoBtn.textContent = 'Take Photo';
takePhotoBtn.onclick = takePhoto;
const rotateBtn = document.createElement('button');
rotateBtn.textContent = 'Rotate';
rotateBtn.onclick = rotateCamera;
const recordBtn = document.createElement('button');
recordBtn.textContent = 'Record Video';
recordBtn.onclick = toggleRecording;
const closeModalBtn = document.createElement('button');
closeModalBtn.textContent = 'Close';
closeModalBtn.onclick = closeModal;
controlsDiv.appendChild(takePhotoBtn);
controlsDiv.appendChild(rotateBtn);
controlsDiv.appendChild(recordBtn);
controlsDiv.appendChild(closeModalBtn);
}

goLiveBtn.onclick = showCameraModal;
cameraSelect.addEventListener('change', async (e) => {
if (e.target.value) await switchToCamera(e.target.value);
});
})();
</script>

</body>
</html>
← Back to feed