This commit is contained in:
@@ -3,21 +3,38 @@
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Твои камеры</h2>
|
||||
<span class="badge bg-success">NFS смонтирована</span>
|
||||
{% if storage_available %}
|
||||
<span class="badge bg-success">Storage доступен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Storage недоступен</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-3 g-4">
|
||||
{% for camera in cameras %}
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<img
|
||||
src="{% url 'camlaps:camera_preview' camera.id %}"
|
||||
class="card-img-top"
|
||||
style="object-fit: cover; height: 220px;"
|
||||
alt="{{ camera.name }}"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary">Camera 1 (Вишня)</h5>
|
||||
<p class="card-text text-muted">Путь: /app/storage/Camera1</p>
|
||||
<h5 class="card-title text-primary">{{ camera.name }}</h5>
|
||||
<p class="card-text text-muted">Путь: {{ camera.storage_path }}</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary">Собрать таймлапс</button>
|
||||
<button class="btn btn-outline-secondary btn-sm">История видео</button>
|
||||
<a class="btn btn-primary" href="{% url 'camlaps:job_create' camera.id %}">Собрать таймлапс</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="{% url 'camlaps:job_list' %}?camera={{ camera.id }}">История</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col">
|
||||
<div class="alert alert-warning mb-0">Нет активных камер. Добавь их в админке.</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,8 +1,13 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'camlaps'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('cameras/<int:camera_id>/preview/', views.camera_preview, name='camera_preview'),
|
||||
path('cameras/<int:camera_id>/jobs/new/', views.job_create, name='job_create'),
|
||||
path('jobs/', views.job_list, name='job_list'),
|
||||
path('jobs/<int:job_id>/', views.job_detail, name='job_detail'),
|
||||
]
|
||||
@@ -1,5 +1,62 @@
|
||||
from django.shortcuts import render
|
||||
from django.http import FileResponse, Http404
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from .forms import TimelapseJobCreateForm
|
||||
from .models import Camera, TimelapseJob
|
||||
from .services.cameras import get_camera_lastsnap_path, is_storage_available, list_active_cameras
|
||||
|
||||
|
||||
# Create your views here.
|
||||
def index(request):
|
||||
return render(request, 'camlaps/index.html')
|
||||
cameras = list_active_cameras()
|
||||
context = {
|
||||
'cameras': cameras,
|
||||
'storage_available': is_storage_available(),
|
||||
}
|
||||
return render(request, 'camlaps/index.html', context)
|
||||
|
||||
|
||||
def camera_preview(request, camera_id: int):
|
||||
camera = get_object_or_404(Camera, pk=camera_id, is_active=True)
|
||||
path = get_camera_lastsnap_path(camera)
|
||||
if not path:
|
||||
raise Http404
|
||||
return FileResponse(path.open('rb'), content_type='image/jpeg')
|
||||
|
||||
|
||||
def job_list(request):
|
||||
qs = TimelapseJob.objects.select_related('camera').all()
|
||||
camera_id = request.GET.get('camera')
|
||||
if camera_id:
|
||||
qs = qs.filter(camera_id=camera_id)
|
||||
jobs = qs.order_by('-created_at')[:200]
|
||||
return render(request, 'camlaps/job_list.html', {'jobs': jobs})
|
||||
|
||||
|
||||
def job_detail(request, job_id: int):
|
||||
job = get_object_or_404(TimelapseJob.objects.select_related('camera'), pk=job_id)
|
||||
video_url = None
|
||||
if job.output_rel_path:
|
||||
rel = job.output_rel_path.lstrip('/')
|
||||
if rel.startswith('timelapses/'):
|
||||
video_url = '/' + rel
|
||||
else:
|
||||
video_url = '/timelapses/' + rel.split('/')[-1]
|
||||
|
||||
return render(request, 'camlaps/job_detail.html', {'job': job, 'video_url': video_url})
|
||||
|
||||
|
||||
def job_create(request, camera_id: int):
|
||||
camera = get_object_or_404(Camera, pk=camera_id, is_active=True)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = TimelapseJobCreateForm(request.POST)
|
||||
if form.is_valid():
|
||||
job = form.save(commit=False)
|
||||
job.camera = camera
|
||||
job.save()
|
||||
return redirect(reverse('camlaps:job_detail', kwargs={'job_id': job.id}))
|
||||
else:
|
||||
form = TimelapseJobCreateForm()
|
||||
|
||||
return render(request, 'camlaps/job_create.html', {'camera': camera, 'form': form})
|
||||
|
||||
BIN
requirements
Normal file
BIN
requirements
Normal file
Binary file not shown.
@@ -10,7 +10,7 @@
|
||||
<a class="nav-link active" href="/">Камеры</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Очередь задач</a>
|
||||
<a class="nav-link" href="{% url 'camlaps:job_list' %}">Очередь задач</a>
|
||||
</li>
|
||||
<li class="nav-item ms-lg-3">
|
||||
<button class="btn btn-link nav-link shadow-none py-2 px-0 px-lg-2" id="theme-toggle" type="button">
|
||||
|
||||
Reference in New Issue
Block a user