- Migrate 11 character JSONs from old wardrobe keys to _BODY_GROUP_KEYS format - Add is_favourite/is_nsfw columns to Preset model - Add HTTP response validation and timeouts to ComfyUI client - Add path traversal protection on replace cover route - Deduplicate services/mcp.py (4 functions → 2 generic + 2 wrappers) - Extract apply_library_filters() and clean_html_text() shared helpers - Add named constants for 17 ComfyUI workflow node IDs - Fix bare except clauses in services/llm.py - Fix tags schema in ensure_default_outfit() (list → dict) - Convert f-string logging to lazy % formatting - Add 5-minute polling timeout to frontend waitForJob() - Improve migration error handling (non-duplicate errors log at WARNING) - Update CLAUDE.md to reflect all changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
82 lines
4.3 KiB
HTML
82 lines
4.3 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2>Preset Library</h2>
|
|
<div class="d-flex gap-1 align-items-center">
|
|
<a href="{{ url_for('create_preset') }}" class="btn btn-sm btn-success">Create New Preset</a>
|
|
<form action="{{ url_for('rescan_presets') }}" method="post" class="d-contents">
|
|
<button type="submit" class="btn btn-sm btn-outline-primary btn-icon" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Rescan preset files from disk"><img src="{{ url_for('static', filename='icons/refresh.png') }}"></button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">{{ message }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<!-- Filters -->
|
|
<form method="get" class="mb-3 d-flex gap-3 align-items-center">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="favourite" value="on" id="favFilter" {% if favourite_filter == 'on' %}checked{% endif %} onchange="this.form.submit()">
|
|
<label class="form-check-label small" for="favFilter">★ Favourites</label>
|
|
</div>
|
|
<select name="nsfw" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()">
|
|
<option value="all" {% if nsfw_filter == 'all' %}selected{% endif %}>All ratings</option>
|
|
<option value="sfw" {% if nsfw_filter == 'sfw' %}selected{% endif %}>SFW only</option>
|
|
<option value="nsfw" {% if nsfw_filter == 'nsfw' %}selected{% endif %}>NSFW only</option>
|
|
</select>
|
|
</form>
|
|
|
|
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-3">
|
|
{% for preset in presets %}
|
|
<div class="col">
|
|
<div class="card h-100 character-card" onclick="window.location.href='{{ url_for('preset_detail', slug=preset.slug) }}'">
|
|
<div class="img-container">
|
|
{% if preset.image_path %}
|
|
<img src="{{ url_for('static', filename='uploads/' + preset.image_path) }}" alt="{{ preset.name }}">
|
|
{% else %}
|
|
{% set fallback = random_gen_image('presets', preset.slug) %}
|
|
{% if fallback %}
|
|
<img src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ preset.name }}" class="fallback-cover">
|
|
{% else %}
|
|
<span class="text-muted">No Image</span>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body p-2">
|
|
<h6 class="card-title text-center mb-1">{{ preset.name }}</h6>
|
|
<p class="card-text small text-center text-muted mb-0">
|
|
{% set parts = [] %}
|
|
{% if preset.data.character and preset.data.character.character_id %}
|
|
{% set _ = parts.append(preset.data.character.character_id | replace('_', ' ') | title) %}
|
|
{% endif %}
|
|
{% if preset.data.action and preset.data.action.action_id %}
|
|
{% set _ = parts.append('+ action') %}
|
|
{% endif %}
|
|
{% if preset.data.scene and preset.data.scene.scene_id %}
|
|
{% set _ = parts.append('+ scene') %}
|
|
{% endif %}
|
|
{{ parts | join(' · ') }}
|
|
</p>
|
|
</div>
|
|
<div class="card-footer d-flex justify-content-between align-items-center p-1">
|
|
<small class="text-muted">preset</small>
|
|
<div class="d-flex gap-1">
|
|
<a href="{{ url_for('edit_preset', slug=preset.slug) }}" class="btn btn-xs btn-outline-secondary" onclick="event.stopPropagation()">Edit</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="col-12">
|
|
<p class="text-muted">No presets found. <a href="{{ url_for('create_preset') }}">Create your first preset</a> or add JSON files to <code>data/presets/</code> and rescan.</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endblock %}
|