Files
character-browser/templates/styles/index.html
Aodhan Collins 55ff58aba6 Major refactor: deduplicate routes, sync, JS, and fix bugs
- Extract 8 common route patterns into factory functions in routes/shared.py
  (favourite, upload, replace cover, save defaults, clone, save JSON,
  get missing, clear covers) — removes ~1,100 lines across 9 route files
- Extract generic _sync_category() in sync.py — 7 sync functions become
  one-liner wrappers, removing ~350 lines
- Extract shared detail page JS into static/js/detail-common.js — all 9
  detail templates now call initDetailPage() with minimal config
- Extract layout inline JS into static/js/layout-utils.js (~185 lines)
- Extract library toolbar JS into static/js/library-toolbar.js
- Fix finalize missing-image bug: raise RuntimeError instead of logging
  warning so job is marked failed
- Fix missing scheduler default in _default_checkpoint_data()
- Fix N+1 query in Character.get_available_outfits() with batch IN query
- Convert all print() to logger across services and routes
- Add missing tags display to styles, scenes, detailers, checkpoints detail
- Update delete buttons to use trash.png icon with solid red background
- Update CLAUDE.md to reflect new architecture

Net reduction: ~1,600 lines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 23:06:58 +00:00

103 lines
5.2 KiB
HTML

{% extends "layout.html" %}
{% from "partials/library_toolbar.html" import library_toolbar %}
{% block content %}
{{ library_toolbar(
title="Style",
category="styles",
create_url=url_for('create_style'),
create_label="Style",
has_batch_gen=true,
has_regen_all=true,
has_lora_create=true,
bulk_create_url=url_for('bulk_create_styles_from_loras'),
has_tags=true,
regen_tags_category="styles",
rescan_url=url_for('rescan_styles'),
get_missing_url="/get_missing_styles",
clear_covers_url="/clear_all_style_covers",
generate_url_pattern="/style/{slug}/generate"
) }}
<!-- 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">&#9733; 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 style in styles %}
<div class="col" id="card-{{ style.slug }}">
<div class="card h-100 character-card {% if request.args.get('highlight') == style.slug %}border-success border-3 highlight-card{% endif %}" onclick="window.location.href='/style/{{ style.slug }}'">
<div class="img-container">
{% if style.image_path %}
<img id="img-{{ style.slug }}" src="{{ url_for('static', filename='uploads/' + style.image_path) }}" alt="{{ style.name }}">
<span id="no-img-{{ style.slug }}" class="text-muted d-none">No Image</span>
{% else %}
{% set fallback = random_gen_image('styles', style.slug) %}
{% if fallback %}
<img id="img-{{ style.slug }}" src="{{ url_for('static', filename='uploads/' + fallback) }}" alt="{{ style.name }}" class="fallback-cover">
<span id="no-img-{{ style.slug }}" class="text-muted d-none">No Image</span>
{% else %}
<img id="img-{{ style.slug }}" src="" alt="{{ style.name }}" class="d-none">
<span id="no-img-{{ style.slug }}" class="text-muted">No Image</span>
{% endif %}
{% endif %}
</div>
<div class="card-body">
<h5 class="card-title text-center">{% if style.is_favourite %}<span class="text-warning">&#9733;</span> {% endif %}{{ style.name }}{% if style.is_nsfw %} <span class="badge bg-danger" style="font-size:0.6rem;vertical-align:middle;">NSFW</span>{% endif %}</h5>
<p class="card-text small text-center text-muted">
{% set ns = namespace(parts=[]) %}
{% if style.data.style is mapping %}
{% for v in style.data.style.values() %}
{% if v %}{% set ns.parts = ns.parts + [v] %}{% endif %}
{% endfor %}
{% endif %}
{% if style.data.lora and style.data.lora.lora_triggers %}
{% set ns.parts = ns.parts + [style.data.lora.lora_triggers] %}
{% endif %}
{{ ns.parts | join(', ') }}
</p>
</div>
<div class="card-footer d-flex justify-content-between align-items-center p-1">
{% if style.data.lora and style.data.lora.lora_name %}
{% set lora_name = style.data.lora.lora_name.split('/')[-1].replace('.safetensors', '') %}
<small class="text-muted text-truncate" title="{{ style.data.lora.lora_name }}">{{ lora_name }}</small>
{% else %}<span></span>{% endif %}
<button class="btn btn-sm btn-danger py-0 px-1 flex-shrink-0 ms-1 resource-delete-btn" title="Delete"
data-category="styles" data-slug="{{ style.slug }}" data-name="{{ style.name | e }}"><img src="/static/icons/trash.png" alt="Delete" style="width:16px;height:16px;"></button>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block scripts %}
<style>
.highlight-card {
animation: highlight-pulse 2s ease-in-out 3;
box-shadow: 0 0 20px rgba(25, 135, 84, 0.5) !important;
}
@keyframes highlight-pulse {
0%, 100% { box-shadow: 0 0 20px rgba(25, 135, 84, 0.5); }
50% { box-shadow: 0 0 30px rgba(25, 135, 84, 0.8); }
}
</style>
<script>
const highlightSlug = new URLSearchParams(window.location.search).get('highlight');
if (highlightSlug) {
const card = document.getElementById(`card-${highlightSlug}`);
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
</script>
<script src="{{ url_for('static', filename='js/library-toolbar.js') }}"></script>
{% endblock %}