- Add pagination to effects list with infinite scroll - Refactor home stats into reusable get_user_stats function - Add /api/stats endpoint for CLI/API clients - Add has_more flag to recipes listing - Add JSON API support to storage type page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
150 lines
5.5 KiB
HTML
150 lines
5.5 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Effects - Art-DAG L1{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-6xl mx-auto">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h1 class="text-3xl font-bold">Effects</h1>
|
|
<label class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded font-medium cursor-pointer">
|
|
Upload Effect
|
|
<input type="file" accept=".py" class="hidden" id="effect-upload" />
|
|
</label>
|
|
</div>
|
|
|
|
<p class="text-gray-400 mb-8">
|
|
Effects are Python scripts that process video frames or whole videos.
|
|
Each effect is stored in IPFS and can be referenced by CID in recipes.
|
|
</p>
|
|
|
|
{% if effects %}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" id="effects-list">
|
|
{% for effect in effects %}
|
|
{% set meta = effect.meta or effect %}
|
|
<a href="/effects/{{ effect.cid }}"
|
|
class="effect-card bg-gray-800 border border-gray-700 rounded-lg p-4 hover:border-gray-600 transition-colors">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="font-medium text-white">{{ meta.name or 'Unnamed' }}</span>
|
|
<span class="text-gray-500 text-sm">v{{ meta.version or '1.0.0' }}</span>
|
|
</div>
|
|
|
|
{% if meta.description %}
|
|
<p class="text-gray-400 text-sm mb-3 line-clamp-2">{{ meta.description }}</p>
|
|
{% endif %}
|
|
|
|
<div class="flex items-center justify-between text-sm mb-2">
|
|
{% if meta.author %}
|
|
<span class="text-gray-500">by {{ meta.author }}</span>
|
|
{% else %}
|
|
<span></span>
|
|
{% endif %}
|
|
{% if meta.temporal %}
|
|
<span class="bg-purple-900 text-purple-300 px-2 py-0.5 rounded text-xs">temporal</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if meta.params %}
|
|
<div class="text-gray-500 text-sm">
|
|
{{ meta.params | length }} parameter{{ 's' if meta.params | length != 1 else '' }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if meta.dependencies %}
|
|
<div class="mt-2 flex flex-wrap gap-1">
|
|
{% for dep in meta.dependencies[:3] %}
|
|
<span class="bg-gray-700 text-gray-300 px-2 py-0.5 rounded text-xs">{{ dep }}</span>
|
|
{% endfor %}
|
|
{% if meta.dependencies | length > 3 %}
|
|
<span class="text-gray-500 text-xs">+{{ meta.dependencies | length - 3 }} more</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="mt-3 text-xs">
|
|
{% if effect.friendly_name %}
|
|
<span class="text-blue-400 font-medium">{{ effect.friendly_name }}</span>
|
|
{% else %}
|
|
<span class="text-gray-600 font-mono truncate">{{ effect.cid[:24] }}...</span>
|
|
{% endif %}
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% if has_more %}
|
|
<div hx-get="/effects?offset={{ offset + limit }}&limit={{ limit }}"
|
|
hx-trigger="revealed"
|
|
hx-swap="afterend"
|
|
hx-select="#effects-list > *"
|
|
class="h-20 flex items-center justify-center text-gray-500">
|
|
Loading more...
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<div class="bg-gray-800 border border-gray-700 rounded-lg p-12 text-center">
|
|
<p class="text-gray-500 mb-4">No effects uploaded yet.</p>
|
|
<p class="text-gray-600 text-sm mb-6">
|
|
Effects are Python files with @effect metadata in a docstring.
|
|
</p>
|
|
<label class="bg-blue-600 hover:bg-blue-700 px-6 py-3 rounded font-medium cursor-pointer inline-block">
|
|
Upload Your First Effect
|
|
<input type="file" accept=".py" class="hidden" id="effect-upload-empty" />
|
|
</label>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div id="upload-result" class="fixed bottom-4 right-4 max-w-sm"></div>
|
|
|
|
<script>
|
|
function handleEffectUpload(input) {
|
|
const file = input.files[0];
|
|
if (!file) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
fetch('/effects/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) throw new Error('Upload failed');
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
const resultDiv = document.getElementById('upload-result');
|
|
resultDiv.innerHTML = `
|
|
<div class="bg-green-900 border border-green-700 rounded-lg p-4">
|
|
<p class="text-green-300 font-medium">Effect uploaded!</p>
|
|
<p class="text-green-400 text-sm mt-1">${data.name} v${data.version}</p>
|
|
<p class="text-gray-400 text-xs mt-2 font-mono">${data.cid}</p>
|
|
</div>
|
|
`;
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1500);
|
|
})
|
|
.catch(error => {
|
|
const resultDiv = document.getElementById('upload-result');
|
|
resultDiv.innerHTML = `
|
|
<div class="bg-red-900 border border-red-700 rounded-lg p-4">
|
|
<p class="text-red-300 font-medium">Upload failed</p>
|
|
<p class="text-red-400 text-sm mt-1">${error.message}</p>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
input.value = '';
|
|
}
|
|
|
|
document.getElementById('effect-upload')?.addEventListener('change', function() {
|
|
handleEffectUpload(this);
|
|
});
|
|
document.getElementById('effect-upload-empty')?.addEventListener('change', function() {
|
|
handleEffectUpload(this);
|
|
});
|
|
</script>
|
|
{% endblock %}
|