Files
timelapse-app-working-edition/templates/index.html
T
2026-08-30 05:16:30 +00:00

343 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Timelapse Camera Manager</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { margin-bottom: 20px; color: #333; }
.form-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
.form-row { display: flex; gap: 15px; flex-wrap: wrap; align-items: flex-end; }
.form-group { flex: 1; min-width: 120px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; color: #555; }
.form-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
.btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn-primary { background: #007bff; color: white; }
.btn-primary:hover { background: #0056b3; }
.btn-danger { background: #dc3545; color: white; }
.btn-danger:hover { background: #c82333; }
.btn-success { background: #28a745; color: white; }
.btn-success:hover { background: #218838; }
.btn-secondary { background: #6c757d; color: white; }
.btn-secondary:hover { background: #545b62; }
.btn-small { padding: 6px 12px; font-size: 12px; }
.table-card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 15px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; color: #555; }
tr:hover { background: #f8f9fa; }
.status-badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; }
.status-running { background: #d4edda; color: #155724; }
.empty-state { text-align: center; padding: 40px; color: #888; }
#message { padding: 15px; border-radius: 4px; margin-bottom: 20px; display: none; }
.message-success { background: #d4edda; color: #155724; }
.message-error { background: #f8d7da; color: #721c24; }
.auth-badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 11px; background: #e2e3e5; color: #383d41; margin-left: 5px; }
.last-image-thumb { width: 80px; height: 60px; object-fit: cover; border-radius: 4px; border: 1px solid #ddd; background: #f0f0f0; }
.no-image { width: 80px; height: 60px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; border-radius: 4px; color: #999; font-size: 11px; }
/* Gallery Modal */
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 1000; }
.modal-content { max-width: 90%; max-height: 90%; margin: 2% auto; position: relative; }
.modal-img { max-width: 100%; max-height: 85vh; display: block; margin: 0 auto; }
.modal-close { position: absolute; top: 20px; right: 30px; color: white; font-size: 40px; cursor: pointer; z-index: 1001; }
.modal-prev, .modal-next { position: absolute; top: 50%; transform: translateY(-50%); color: white; font-size: 40px; cursor: pointer; padding: 20px; user-select: none; }
.modal-prev { left: 20px; }
.modal-next { right: 20px; }
.modal-caption { text-align: center; color: white; padding: 10px; font-size: 14px; }
/* Gallery Grid */
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; padding: 20px; }
.gallery-item { cursor: pointer; position: relative; aspect-ratio: 4/3; overflow: hidden; border-radius: 4px; background: #f0f0f0; }
.gallery-item img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.2s; }
.gallery-item:hover img { transform: scale(1.05); }
.gallery-item .overlay { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.7); color: white; padding: 5px; font-size: 11px; }
.gallery-container { display: none; background: white; border-radius: 8px; margin-top: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.gallery-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid #eee; }
.gallery-header h3 { margin: 0; }
.loading { text-align: center; padding: 40px; color: #888; }
</style>
</head>
<body>
<div class="container">
<h1>📷 Timelapse Camera Manager</h1>
<div id="message"></div>
<div class="form-card">
<div class="form-row">
<div class="form-group">
<label>Project Name *</label>
<input type="text" id="projectName" placeholder="My Camera">
</div>
<div class="form-group">
<label>IP Address *</label>
<input type="text" id="ipAddress" placeholder="192.168.1.100">
</div>
<div class="form-group">
<label>Port *</label>
<input type="number" id="port" placeholder="80" min="1" max="65535">
</div>
<div class="form-group">
<label>Period (min) *</label>
<input type="number" id="period" placeholder="5" min="1">
</div>
<div class="form-group">
<label>Username</label>
<input type="text" id="username" placeholder="root">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="password" placeholder="pass">
</div>
<button class="btn btn-primary" onclick="createProject()">Add Project</button>
</div>
</div>
<div class="table-card">
<table>
<thead>
<tr>
<th>Project</th>
<th>Camera</th>
<th>Period</th>
<th>Runtime</th>
<th>Last Image</th>
<th>Images</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="projectsTable">
<tr><td colspan="7" class="empty-state">No projects yet. Add one above!</td></tr>
</tbody>
</table>
</div>
<!-- Gallery Container -->
<div id="galleryContainer" class="gallery-container">
<div class="gallery-header">
<h3 id="galleryTitle">Gallery</h3>
<button class="btn btn-secondary btn-small" onclick="closeGallery()">Close Gallery</button>
</div>
<div id="galleryGrid" class="gallery-grid"></div>
</div>
</div>
<!-- Image Modal -->
<div id="imageModal" class="modal">
<span class="modal-close" onclick="closeModal()">&times;</span>
<span class="modal-prev" onclick="prevImage()">&#10094;</span>
<span class="modal-next" onclick="nextImage()">&#10095;</span>
<img class="modal-img" id="modalImg">
<div class="modal-caption" id="modalCaption"></div>
</div>
<script>
let currentGallery = [];
let currentIndex = 0;
let currentProjectName = '';
function showMessage(msg, type) {
const el = document.getElementById('message');
el.textContent = msg;
el.className = type === 'success' ? 'message-success' : 'message-error';
el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 5000);
}
async function loadProjects() {
const res = await fetch('/api/projects');
const projects = await res.json();
const tbody = document.getElementById('projectsTable');
if (projects.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No projects yet. Add one above!</td></tr>';
return;
}
tbody.innerHTML = projects.map(p => `
<tr>
<td>
<strong>${p.name}</strong>
${p.has_auth ? '<span class="auth-badge">🔒 Auth</span>' : ''}
</td>
<td>${p.ip_address}:${p.port}</td>
<td>${p.period_minutes} min</td>
<td><span class="status-badge status-running">${p.runtime_hours} hrs</span></td>
<td>
${p.last_image
? `<img src="${p.last_image}" class="last-image-thumb" alt="Latest" onload="this.style.background='none'">`
: `<div class="no-image">No images</div>`
}
</td>
<td>${p.image_count}</td>
<td>
<button class="btn btn-success btn-small" onclick="createTimelapse(${p.id})">Timelapse</button>
<button class="btn btn-secondary btn-small" onclick="openGallery(${p.id}, '${p.name}')">Gallery</button>
<button class="btn btn-danger btn-small" onclick="deleteProject(${p.id})">Delete</button>
</td>
</tr>
`).join('');
}
async function createProject() {
const name = document.getElementById('projectName').value;
const ip_address = document.getElementById('ipAddress').value;
const port = parseInt(document.getElementById('port').value);
const period_minutes = parseInt(document.getElementById('period').value);
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const res = await fetch('/api/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, ip_address, port, period_minutes, username, password })
});
const data = await res.json();
if (res.ok) {
showMessage('Project created successfully!', 'success');
document.getElementById('projectName').value = '';
document.getElementById('ipAddress').value = '';
document.getElementById('port').value = '';
document.getElementById('period').value = '';
document.getElementById('username').value = '';
document.getElementById('password').value = '';
loadProjects();
} else {
showMessage(data.error || 'Failed to create project', 'error');
}
}
async function deleteProject(id) {
if (!confirm('Are you sure? This will stop captures and delete the project.')) return;
const res = await fetch(`/api/projects/${id}`, { method: 'DELETE' });
if (res.ok) {
showMessage('Project deleted', 'success');
loadProjects();
} else {
showMessage('Failed to delete project', 'error');
}
}
async function createTimelapse(id) {
showMessage('Creating timelapse... this may take a moment', 'success');
const res = await fetch(`/api/projects/${id}/timelapse`, { method: 'POST' });
if (res.ok) {
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `timelapse_${id}.mp4`;
a.click();
window.URL.revokeObjectURL(url);
showMessage('Timelapse downloaded!', 'success');
} else {
const data = await res.json();
showMessage(data.error || 'Failed to create timelapse', 'error');
}
}
async function openGallery(projectId, projectName) {
currentProjectName = projectName;
document.getElementById('galleryTitle').textContent = `Gallery: ${projectName}`;
document.getElementById('galleryContainer').style.display = 'block';
document.getElementById('galleryGrid').innerHTML = '<div class="loading">Loading images...</div>';
// Scroll to gallery
document.getElementById('galleryContainer').scrollIntoView({ behavior: 'smooth' });
try {
const res = await fetch(`/api/projects/${projectId}/gallery`);
const images = await res.json();
if (images.error) {
document.getElementById('galleryGrid').innerHTML = `<div class="loading">${images.error}</div>`;
return;
}
currentGallery = images;
if (images.length === 0) {
document.getElementById('galleryGrid').innerHTML = '<div class="loading">No images yet</div>';
return;
}
// Lazy load gallery grid
document.getElementById('galleryGrid').innerHTML = images.map((img, idx) => `
<div class="gallery-item" onclick="showModal(${idx})">
<img data-src="${img.path}" alt="${img.filename}" loading="lazy">
<div class="overlay">${img.created.split('T')[0]}</div>
</div>
`).join('');
// Lazy load images in grid
document.querySelectorAll('.gallery-item img').forEach(img => {
img.src = img.dataset.src;
});
} catch (e) {
document.getElementById('galleryGrid').innerHTML = '<div class="loading">Failed to load gallery</div>';
}
}
function closeGallery() {
document.getElementById('galleryContainer').style.display = 'none';
currentGallery = [];
}
function showModal(index) {
if (currentGallery.length === 0) return;
currentIndex = index;
const img = currentGallery[index];
document.getElementById('modalImg').src = img.path;
document.getElementById('modalCaption').textContent = `${img.filename} - ${img.created.replace('T', ' ').split('.')[0]}`;
document.getElementById('imageModal').style.display = 'block';
}
function closeModal() {
document.getElementById('imageModal').style.display = 'none';
}
function prevImage() {
if (currentIndex > 0) {
showModal(currentIndex - 1);
}
}
function nextImage() {
if (currentIndex < currentGallery.length - 1) {
showModal(currentIndex + 1);
}
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (document.getElementById('imageModal').style.display === 'block') {
if (e.key === 'Escape') closeModal();
if (e.key === 'ArrowLeft') prevImage();
if (e.key === 'ArrowRight') nextImage();
}
});
// Load projects on page load
loadProjects();
// Refresh every 30 seconds
setInterval(loadProjects, 30000);
</script>
</body>
</html>