Replaces old list-format tags (which duplicated prompt content) with structured dict tags per category (origin_series, outfit_type, participants, style_type, scene_type, etc.). Tags are now purely organizational metadata — removed from the prompt pipeline entirely. Adds is_favourite and is_nsfw columns to all 8 resource models. Favourite is DB-only (user preference); NSFW is mirrored in JSON tags for rescan persistence. All library pages get filter controls and favourites-first sorting. Introduces a parallel LLM job queue (_enqueue_task + _llm_queue_worker) for background tag regeneration, with the same status polling UI as ComfyUI jobs. Fixes call_llm() to use has_request_context() fallback for background threads. Adds global search (/search) across resources and gallery images, with navbar search bar. Adds gallery image sidecar JSON for per-image favourite/NSFW metadata. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
127 lines
6.3 KiB
HTML
127 lines
6.3 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2>Search</h2>
|
|
</div>
|
|
|
|
<!-- Search form -->
|
|
<form method="get" class="mb-4">
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-md-5">
|
|
<input type="text" name="q" class="form-control" placeholder="Search resources and images..." value="{{ query }}" autofocus>
|
|
</div>
|
|
<div class="col-auto">
|
|
<select name="category" class="form-select form-select-sm" style="width:auto;">
|
|
<option value="all" {% if category == 'all' %}selected{% endif %}>All categories</option>
|
|
<option value="characters" {% if category == 'characters' %}selected{% endif %}>Characters</option>
|
|
<option value="looks" {% if category == 'looks' %}selected{% endif %}>Looks</option>
|
|
<option value="outfits" {% if category == 'outfits' %}selected{% endif %}>Outfits</option>
|
|
<option value="actions" {% if category == 'actions' %}selected{% endif %}>Actions</option>
|
|
<option value="styles" {% if category == 'styles' %}selected{% endif %}>Styles</option>
|
|
<option value="scenes" {% if category == 'scenes' %}selected{% endif %}>Scenes</option>
|
|
<option value="detailers" {% if category == 'detailers' %}selected{% endif %}>Detailers</option>
|
|
<option value="checkpoints" {% if category == 'checkpoints' %}selected{% endif %}>Checkpoints</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-auto">
|
|
<select name="nsfw" class="form-select form-select-sm" style="width:auto;">
|
|
<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>
|
|
</div>
|
|
<div class="col-auto">
|
|
<select name="type" class="form-select form-select-sm" style="width:auto;">
|
|
<option value="all" {% if search_type == 'all' %}selected{% endif %}>Resources & Images</option>
|
|
<option value="resources" {% if search_type == 'resources' %}selected{% endif %}>Resources only</option>
|
|
<option value="images" {% if search_type == 'images' %}selected{% endif %}>Images only</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button type="submit" class="btn btn-primary btn-sm">Search</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
{% if query %}
|
|
<p class="text-muted mb-3">Found {{ total_resources }} resource{{ 's' if total_resources != 1 }} and {{ total_images }} image{{ 's' if total_images != 1 }} for "<strong>{{ query }}</strong>"</p>
|
|
|
|
{% set type_labels = {
|
|
'characters': 'Characters', 'looks': 'Looks', 'outfits': 'Outfits',
|
|
'actions': 'Actions', 'styles': 'Styles', 'scenes': 'Scenes',
|
|
'detailers': 'Detailers', 'checkpoints': 'Checkpoints'
|
|
} %}
|
|
|
|
{% set type_url_prefix = {
|
|
'characters': '/character', 'looks': '/look', 'outfits': '/outfit',
|
|
'actions': '/action', 'styles': '/style', 'scenes': '/scene',
|
|
'detailers': '/detailer', 'checkpoints': '/checkpoint'
|
|
} %}
|
|
|
|
<!-- Resource results -->
|
|
{% if grouped_resources %}
|
|
{% for cat_name, items in grouped_resources.items() %}
|
|
<div class="mb-4">
|
|
<h5>{{ type_labels.get(cat_name, cat_name | capitalize) }} <span class="badge bg-secondary">{{ items | length }}</span></h5>
|
|
<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 item in items %}
|
|
<div class="col">
|
|
<div class="card h-100 character-card" onclick="window.location.href='{{ type_url_prefix.get(cat_name, '/' + cat_name[:-1]) }}/{{ item.slug }}'">
|
|
<div class="img-container">
|
|
{% if item.image_path %}
|
|
<img src="{{ url_for('static', filename='uploads/' + item.image_path) }}" alt="{{ item.name }}">
|
|
{% else %}
|
|
<span class="text-muted">No Image</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
<h6 class="card-title text-center mb-1">
|
|
{% if item.is_favourite %}<span class="text-warning">★</span> {% endif %}
|
|
{{ item.name }}
|
|
{% if item.is_nsfw %}<span class="badge bg-danger" style="font-size:0.55rem;vertical-align:middle;">NSFW</span>{% endif %}
|
|
</h6>
|
|
<p class="card-text small text-center text-muted text-truncate" title="{{ item.match_context }}">{{ item.match_context }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
<!-- Image results -->
|
|
{% if images %}
|
|
<div class="mb-4">
|
|
<h5>Gallery Images <span class="badge bg-secondary">{{ images | length }}</span></h5>
|
|
<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 img in images %}
|
|
<div class="col">
|
|
<div class="card h-100 character-card" onclick="window.location.href='/gallery?category={{ img.category }}&slug={{ img.slug }}'">
|
|
<div class="img-container">
|
|
<img src="{{ url_for('static', filename='uploads/' + img.path) }}" alt="{{ img.slug }}">
|
|
{% if img.is_favourite %}
|
|
<span class="gallery-fav-star active" style="position:absolute;top:4px;right:4px;font-size:1.2rem;color:#ffc107;">★</span>
|
|
{% endif %}
|
|
{% if img.is_nsfw %}
|
|
<span class="badge bg-danger" style="position:absolute;top:4px;left:4px;font-size:0.55rem;">NSFW</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body py-1">
|
|
<p class="card-text small text-center text-muted text-truncate">{{ img.category }}/{{ img.slug }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if not grouped_resources and not images %}
|
|
<p class="text-muted">No results found.</p>
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
{% endblock %}
|