feat(tags): fandom-less character nudges
Adds a header counter ('⚠ N characters need a fandom') that filters to
?null_fandom=1, plus an inline chip on each qualifying character card.
Backend accepts null_fandom_only in the list query.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+23
-6
@@ -440,7 +440,7 @@ def gallery_jump():
|
||||
)
|
||||
|
||||
|
||||
def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, search=None):
|
||||
def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, search=None, null_fandom_only=False):
|
||||
"""
|
||||
Helper function to get tags with counts and preview images.
|
||||
Returns (tag_data list, total_count, has_more).
|
||||
@@ -455,6 +455,8 @@ def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, sea
|
||||
count_q = count_q.filter(Tag.kind == kind)
|
||||
if search:
|
||||
count_q = count_q.filter(Tag.name.ilike(f'%{search}%'))
|
||||
if null_fandom_only:
|
||||
count_q = count_q.filter(Tag.kind == 'character', Tag.fandom_id.is_(None))
|
||||
total_count = count_q.scalar()
|
||||
|
||||
# Query 1: Get tags with image counts (paginated)
|
||||
@@ -467,6 +469,8 @@ def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, sea
|
||||
q = q.filter(Tag.kind == kind)
|
||||
if search:
|
||||
q = q.filter(Tag.name.ilike(f'%{search}%'))
|
||||
if null_fandom_only:
|
||||
q = q.filter(Tag.kind == 'character', Tag.fandom_id.is_(None))
|
||||
|
||||
tags_with_counts = q.order_by(Tag.name.asc()).offset(offset).limit(limit).all()
|
||||
|
||||
@@ -481,6 +485,7 @@ def _get_tags_with_previews(kind=None, limit=35, offset=0, images_per_tag=3, sea
|
||||
"tag": t.name,
|
||||
"images": [],
|
||||
"kind": t.kind,
|
||||
"fandom_id": t.fandom_id,
|
||||
"count": count
|
||||
}
|
||||
|
||||
@@ -527,17 +532,25 @@ def tag_list():
|
||||
/tags -> all tags
|
||||
/tags?kind=user -> only user tags
|
||||
/tags?search=xyz -> tags matching search term
|
||||
etc.
|
||||
/tags?null_fandom=1 -> only fandom-less character tags
|
||||
Shows a few preview images per tag.
|
||||
Initially loads 35 tags, more loaded via API as user scrolls.
|
||||
"""
|
||||
kind = request.args.get('kind')
|
||||
search = request.args.get('search', '').strip() or None
|
||||
null_fandom_only = request.args.get('null_fandom') == '1'
|
||||
limit = 35
|
||||
images_per_tag = 3
|
||||
|
||||
tag_data, total_count, has_more = _get_tags_with_previews(
|
||||
kind=kind, limit=limit, offset=0, images_per_tag=images_per_tag, search=search
|
||||
kind=kind, limit=limit, offset=0, images_per_tag=images_per_tag,
|
||||
search=search, null_fandom_only=null_fandom_only
|
||||
)
|
||||
|
||||
null_fandom_character_count = (
|
||||
db.session.query(func.count(Tag.id))
|
||||
.filter(Tag.kind == 'character', Tag.fandom_id.is_(None))
|
||||
.scalar() or 0
|
||||
)
|
||||
|
||||
return render_template('tags_list.html',
|
||||
@@ -547,7 +560,9 @@ def tag_list():
|
||||
search_query=search or '',
|
||||
total_count=total_count,
|
||||
has_more=has_more,
|
||||
page_size=limit)
|
||||
page_size=limit,
|
||||
null_fandom_character_count=null_fandom_character_count,
|
||||
show_null_fandom_filter=null_fandom_only)
|
||||
|
||||
|
||||
@main.route('/api/tags/list')
|
||||
@@ -558,16 +573,18 @@ def api_tags_list():
|
||||
"""
|
||||
kind = request.args.get('kind')
|
||||
search = request.args.get('search', '').strip() or None
|
||||
null_fandom_only = request.args.get('null_fandom') == '1'
|
||||
offset = request.args.get('offset', 0, type=int)
|
||||
limit = request.args.get('limit', 35, type=int)
|
||||
images_per_tag = 3
|
||||
|
||||
tag_data, total_count, has_more = _get_tags_with_previews(
|
||||
kind=kind, limit=limit, offset=offset, images_per_tag=images_per_tag, search=search
|
||||
kind=kind, limit=limit, offset=offset, images_per_tag=images_per_tag,
|
||||
search=search, null_fandom_only=null_fandom_only
|
||||
)
|
||||
|
||||
# Render tag cards to HTML
|
||||
cards_html = render_template('_tag_cards.html', tag_data=tag_data)
|
||||
cards_html = render_template('_tag_cards.html', tag_data=tag_data, show_null_fandom_filter=null_fandom_only)
|
||||
|
||||
return jsonify(
|
||||
ok=True,
|
||||
|
||||
@@ -2998,3 +2998,37 @@ body.select-mode .gallery-infinite-container {
|
||||
bottom: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.null-fandom-counter-link {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
background: rgba(255, 180, 0, 0.12);
|
||||
border: 1px solid rgba(255, 180, 0, 0.3);
|
||||
border-radius: 0.25rem;
|
||||
color: #ffb400;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.null-fandom-counter-link:hover {
|
||||
background: rgba(255, 180, 0, 0.2);
|
||||
}
|
||||
|
||||
.null-fandom-filter-active {
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
background: rgba(255, 180, 0, 0.08);
|
||||
border-left: 3px solid #ffb400;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.character-no-fandom-chip {
|
||||
display: inline-block;
|
||||
padding: 0.1rem 0.4rem;
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
background: rgba(255, 180, 0, 0.15);
|
||||
color: #ffb400;
|
||||
border-radius: 0.2rem;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
{%- else -%}#{%- endif -%}
|
||||
</span>
|
||||
<span class="tag-name" title="{{ tag.name }}">{{ tag.name }}</span>
|
||||
{% if tag.kind == 'character' and tag.fandom_id is none and not show_null_fandom_filter %}
|
||||
<span class="character-no-fandom-chip" title="This character has no fandom — click to assign">⚠ No fandom</span>
|
||||
{% endif %}
|
||||
<span class="tag-count">{{ tag.count }}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -80,6 +80,20 @@
|
||||
aria-current="{{ 'page' if active_kind=='user' else 'false' }}"># User</a>
|
||||
</div>
|
||||
|
||||
{% if null_fandom_character_count > 0 and not show_null_fandom_filter %}
|
||||
<a href="{{ url_for('main.tag_list', kind='character', null_fandom=1) }}"
|
||||
class="null-fandom-counter-link">
|
||||
⚠ {{ null_fandom_character_count }} character{{ 's' if null_fandom_character_count != 1 }} need a fandom
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if show_null_fandom_filter %}
|
||||
<div class="null-fandom-filter-active">
|
||||
Showing characters without a fandom.
|
||||
<a href="{{ url_for('main.tag_list', kind='character') }}">Clear filter</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if tag_data %}
|
||||
<div class="tag-grid" id="tagGrid">
|
||||
{% include '_tag_cards.html' %}
|
||||
|
||||
Reference in New Issue
Block a user