feat: add gallery page with image and video details, including upload and generation status
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -187,6 +187,63 @@ def dashboard():
|
||||
generated_videos=generated_videos)
|
||||
|
||||
|
||||
@app.get("/gallery")
|
||||
@login_required
|
||||
def gallery():
|
||||
token = session["access_token"]
|
||||
|
||||
# Fetch all content types
|
||||
uploads_resp = _api("GET", "/images/", token=token)
|
||||
uploads = uploads_resp.json() if uploads_resp.status_code == 200 else []
|
||||
|
||||
gen_images_resp = _api("GET", "/generate/images", token=token)
|
||||
generated_images = gen_images_resp.json(
|
||||
) if gen_images_resp.status_code == 200 else []
|
||||
|
||||
videos_resp = _api("GET", "/generate/videos", token=token)
|
||||
videos = videos_resp.json() if videos_resp.status_code == 200 else []
|
||||
|
||||
# Separate pending videos
|
||||
pending_videos = [v for v in videos if v.get(
|
||||
"status") not in ("completed", "failed")]
|
||||
completed_videos = [v for v in videos if v.get("status") == "completed"]
|
||||
|
||||
return render_template(
|
||||
"gallery.html",
|
||||
uploads=uploads,
|
||||
generated_images=generated_images,
|
||||
pending_videos=pending_videos,
|
||||
completed_videos=completed_videos,
|
||||
)
|
||||
|
||||
|
||||
@app.get("/gallery/image/<image_id>")
|
||||
@login_required
|
||||
def image_detail(image_id: str):
|
||||
token = session["access_token"]
|
||||
resp = _api("GET", f"/generate/images/{image_id}", token=token)
|
||||
image = resp.json() if resp.status_code == 200 else None
|
||||
return render_template("image_detail.html", image=image)
|
||||
|
||||
|
||||
@app.get("/gallery/video/<video_id>")
|
||||
@login_required
|
||||
def video_detail(video_id: str):
|
||||
token = session["access_token"]
|
||||
resp = _api("GET", f"/generate/videos/{video_id}", token=token)
|
||||
video = resp.json() if resp.status_code == 200 else None
|
||||
return render_template("video_detail.html", video=video)
|
||||
|
||||
|
||||
@app.get("/gallery/upload/<image_id>")
|
||||
@login_required
|
||||
def upload_detail(image_id: str):
|
||||
token = session["access_token"]
|
||||
resp = _api("GET", f"/images/{image_id}", token=token)
|
||||
image = resp.json() if resp.status_code == 200 else None
|
||||
return render_template("upload_detail.html", image=image)
|
||||
|
||||
|
||||
# ── Generate ──────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/images/<image_id>/file")
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<div class="nav-links">
|
||||
{% if session.get('access_token') %}
|
||||
<a href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||
<a href="{{ url_for('gallery') }}">Gallery</a>
|
||||
|
||||
<a href="{{ url_for('generate_text') }}">Generate Text</a>
|
||||
<a href="{{ url_for('generate_image') }}">Generate Image</a>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
{% extends "base.html" %} {% block title %}My Gallery{% endblock %} {% block
|
||||
content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-6">My Gallery</h1>
|
||||
|
||||
<!-- Pending Creations -->
|
||||
{% if pending_videos %}
|
||||
<div class="mb-12">
|
||||
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||
Pending Creations
|
||||
</h2>
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
>
|
||||
{% for video in pending_videos %}
|
||||
<a
|
||||
href="{{ url_for('video_detail', video_id=video.id) }}"
|
||||
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
|
||||
>
|
||||
<div class="p-4">
|
||||
<p class="font-bold text-lg truncate">{{ video.prompt }}</p>
|
||||
<p class="text-sm text-gray-400">
|
||||
Video Job Status:
|
||||
<span class="font-semibold text-yellow-400"
|
||||
>{{ video.status }}</span
|
||||
>
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Started: {{ video.created_at | fromisoformat | humantime }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Generated Images -->
|
||||
<div class="mb-12">
|
||||
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||
Generated Images
|
||||
</h2>
|
||||
{% if generated_images %}
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
>
|
||||
{% for image in generated_images %}
|
||||
<a
|
||||
href="{{ url_for('image_detail', image_id=image.id) }}"
|
||||
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
|
||||
>
|
||||
<img
|
||||
src="{{ image.image_data }}"
|
||||
alt="{{ image.prompt }}"
|
||||
class="w-full h-48 object-cover"
|
||||
/>
|
||||
<div class="p-4">
|
||||
<p class="text-sm truncate">{{ image.prompt }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-400">
|
||||
You haven't generated any images yet.
|
||||
<a
|
||||
href="{{ url_for('generate_image') }}"
|
||||
class="text-blue-400 hover:underline"
|
||||
>Generate one now</a
|
||||
>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Generated Videos -->
|
||||
<div class="mb-12">
|
||||
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||
Generated Videos
|
||||
</h2>
|
||||
{% if completed_videos %}
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
>
|
||||
{% for video in completed_videos %}
|
||||
<a
|
||||
href="{{ url_for('video_detail', video_id=video.id) }}"
|
||||
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
|
||||
>
|
||||
<div class="w-full h-48 bg-black flex items-center justify-center">
|
||||
<svg
|
||||
class="w-12 h-12 text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||
></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<p class="text-sm truncate">{{ video.prompt }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-400">
|
||||
You haven't generated any videos yet.
|
||||
<a
|
||||
href="{{ url_for('generate_video') }}"
|
||||
class="text-blue-400 hover:underline"
|
||||
>Generate one now</a
|
||||
>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Uploaded Images -->
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2">
|
||||
My Uploads
|
||||
</h2>
|
||||
{% if uploads %}
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
>
|
||||
{% for image in uploads %}
|
||||
<a
|
||||
href="{{ url_for('upload_detail', image_id=image.id) }}"
|
||||
class="block bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-2xl transition-shadow duration-300"
|
||||
>
|
||||
<img
|
||||
src="{{ url_for('serve_uploaded_image', image_id=image.id) }}"
|
||||
alt="{{ image.filename }}"
|
||||
class="w-full h-48 object-cover"
|
||||
/>
|
||||
<div class="p-4">
|
||||
<p class="text-sm truncate">{{ image.filename }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-400">You haven't uploaded any images.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %} {% block title %}Generated Image{% endblock %} {%
|
||||
block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<a
|
||||
href="{{ url_for('gallery') }}"
|
||||
class="text-blue-400 hover:underline mb-4 inline-block"
|
||||
>← Back to Gallery</a
|
||||
>
|
||||
|
||||
{% if image %}
|
||||
<h1 class="text-2xl font-bold mb-4">Generated Image</h1>
|
||||
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||||
<img
|
||||
src="{{ image.image_data }}"
|
||||
alt="{{ image.prompt }}"
|
||||
class="w-full object-contain"
|
||||
/>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-semibold mb-2">Prompt</h2>
|
||||
<p class="text-gray-300 bg-gray-900 p-3 rounded-md">{{ image.prompt }}</p>
|
||||
<div class="mt-4 text-sm text-gray-400">
|
||||
<p><strong>Model:</strong> {{ image.model_id }}</p>
|
||||
<p>
|
||||
<strong>Created:</strong> {{ image.created_at | fromisoformat |
|
||||
humantime }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<h1 class="text-2xl font-bold">Image not found</h1>
|
||||
<p class="text-gray-400 mt-2">Could not find details for this image.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %} {% block title %}Uploaded Image{% endblock %} {% block
|
||||
content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<a
|
||||
href="{{ url_for('gallery') }}"
|
||||
class="text-blue-400 hover:underline mb-4 inline-block"
|
||||
>← Back to Gallery</a
|
||||
>
|
||||
|
||||
{% if image %}
|
||||
<h1 class="text-2xl font-bold mb-4">Uploaded Image</h1>
|
||||
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||||
<img
|
||||
src="{{ url_for('serve_uploaded_image', image_id=image.id) }}"
|
||||
alt="{{ image.filename }}"
|
||||
class="w-full object-contain"
|
||||
/>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-semibold mb-2">Details</h2>
|
||||
<div class="mt-4 text-sm text-gray-400">
|
||||
<p><strong>Filename:</strong> {{ image.filename }}</p>
|
||||
<p><strong>Content Type:</strong> {{ image.content_type }}</p>
|
||||
<p>
|
||||
<strong>Size:</strong> {{ (image.size_bytes / 1024) | round(2) }} KB
|
||||
</p>
|
||||
<p>
|
||||
<strong>Uploaded:</strong> {{ image.created_at | fromisoformat |
|
||||
humantime }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<h1 class="text-2xl font-bold">Image not found</h1>
|
||||
<p class="text-gray-400 mt-2">
|
||||
Could not find details for this uploaded image.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,70 @@
|
||||
{% extends "base.html" %} {% block title %}Generated Video{% endblock %} {%
|
||||
block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<a
|
||||
href="{{ url_for('gallery') }}"
|
||||
class="text-blue-400 hover:underline mb-4 inline-block"
|
||||
>← Back to Gallery</a
|
||||
>
|
||||
|
||||
{% if video %}
|
||||
<h1 class="text-2xl font-bold mb-4">Video Generation Job</h1>
|
||||
<div class="bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||||
{% if video.status == 'completed' and video.video_url %}
|
||||
<video src="{{ video.video_url }}" controls class="w-full"></video>
|
||||
{% elif video.status in ('queued', 'processing') and video.polling_url %}
|
||||
<div
|
||||
class="w-full bg-black aspect-video flex flex-col items-center justify-center p-6 text-center"
|
||||
id="video-poll-status"
|
||||
data-polling-url="{{ video.polling_url }}"
|
||||
>
|
||||
<p class="text-xl font-semibold">
|
||||
Status: <strong id="poll-status-text">{{ video.status }}</strong>
|
||||
</p>
|
||||
<p class="text-gray-400 mt-2">
|
||||
Your video is being processed. This page will update automatically when
|
||||
it's ready.
|
||||
</p>
|
||||
<div class="spinner mt-4"></div>
|
||||
</div>
|
||||
{% elif video.status == 'failed' %}
|
||||
<div
|
||||
class="w-full bg-black aspect-video flex flex-col items-center justify-center p-6 text-center"
|
||||
>
|
||||
<p class="text-xl font-semibold text-red-500">Generation Failed</p>
|
||||
<p class="text-gray-400 mt-2">
|
||||
{{ video.error or 'An unknown error occurred.' }}
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div
|
||||
class="w-full bg-black aspect-video flex flex-col items-center justify-center p-6 text-center"
|
||||
>
|
||||
<p class="text-xl font-semibold">Video Not Available</p>
|
||||
<p class="text-gray-400 mt-2">Status: {{ video.status }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-semibold mb-2">Prompt</h2>
|
||||
<p class="text-gray-300 bg-gray-900 p-3 rounded-md">{{ video.prompt }}</p>
|
||||
<div class="mt-4 text-sm text-gray-400">
|
||||
<p><strong>Model:</strong> {{ video.model_id }}</p>
|
||||
<p><strong>Job ID:</strong> <code>{{ video.job_id }}</code></p>
|
||||
<p>
|
||||
<strong>Created:</strong> {{ video.created_at | fromisoformat |
|
||||
humantime }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Last Update:</strong> {{ video.updated_at | fromisoformat |
|
||||
humantime }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<h1 class="text-2xl font-bold">Video job not found</h1>
|
||||
<p class="text-gray-400 mt-2">Could not find details for this video job.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user