Add /emotes/

This commit is contained in:
Joakim Hellsén 2026-01-10 23:15:09 +01:00
commit b8df95e317
No known key found for this signature in database
4 changed files with 61 additions and 9 deletions

View file

@ -162,6 +162,7 @@
<a href="{% url 'twitch:channel_list' %}">Channels</a> | <a href="{% url 'twitch:channel_list' %}">Channels</a> |
<a href="{% url 'twitch:docs_rss' %}">RSS</a> | <a href="{% url 'twitch:docs_rss' %}">RSS</a> |
<a href="{% url 'twitch:debug' %}">Debug</a> | <a href="{% url 'twitch:debug' %}">Debug</a> |
<a href="{% url 'twitch:emote_gallery' %}">Emotes</a>
<form action="{% url 'twitch:search' %}" <form action="{% url 'twitch:search' %}"
method="get" method="get"
style="display: inline"> style="display: inline">

View file

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}Emotes{% endblock %}
{% block content %}
<h1>Emotes</h1>
<div class="emote-gallery" style="display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: flex-start;">
{% for emote in emotes %}
<a href="{% url 'twitch:campaign_detail' emote.campaign.twitch_id %}" title="{{ emote.campaign.name }}" style="display: inline-block;">
<img src="{{ emote.image_url }}" alt="Emote" style="max-width: 96px; max-height: 96px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.12); padding: 4px;" loading="lazy" />
</a>
{% empty %}
<p>No drop campaigns with emotes found.</p>
{% endfor %}
</div>
{% endblock %}

View file

@ -18,21 +18,22 @@ app_name = "twitch"
urlpatterns: list[URLPattern] = [ urlpatterns: list[URLPattern] = [
path("", views.dashboard, name="dashboard"), path("", views.dashboard, name="dashboard"),
path("search/", views.search_view, name="search"),
path("debug/", views.debug_view, name="debug"),
path("campaigns/", views.drop_campaign_list_view, name="campaign_list"), path("campaigns/", views.drop_campaign_list_view, name="campaign_list"),
path("campaigns/<str:twitch_id>/", views.drop_campaign_detail_view, name="campaign_detail"), path("campaigns/<str:twitch_id>/", views.drop_campaign_detail_view, name="campaign_detail"),
path("games/", views.GamesGridView.as_view(), name="game_list"),
path("games/list/", views.GamesListView.as_view(), name="game_list_simple"),
path("games/<str:twitch_id>/", views.GameDetailView.as_view(), name="game_detail"),
path("organizations/", views.org_list_view, name="org_list"),
path("organizations/<str:twitch_id>/", views.organization_detail_view, name="organization_detail"),
path("channels/", views.ChannelListView.as_view(), name="channel_list"), path("channels/", views.ChannelListView.as_view(), name="channel_list"),
path("channels/<str:twitch_id>/", views.ChannelDetailView.as_view(), name="channel_detail"), path("channels/<str:twitch_id>/", views.ChannelDetailView.as_view(), name="channel_detail"),
path("rss/organizations/", OrganizationFeed(), name="organization_feed"), path("debug/", views.debug_view, name="debug"),
path("rss/organizations/<str:twitch_id>/campaigns/", OrganizationCampaignFeed(), name="organization_campaign_feed"), path("docs/rss/", views.docs_rss_view, name="docs_rss"),
path("emotes/", views.emote_gallery_view, name="emote_gallery"),
path("games/", views.GamesGridView.as_view(), name="game_list"),
path("games/<str:twitch_id>/", views.GameDetailView.as_view(), name="game_detail"),
path("games/list/", views.GamesListView.as_view(), name="game_list_simple"),
path("organizations/", views.org_list_view, name="org_list"),
path("organizations/<str:twitch_id>/", views.organization_detail_view, name="organization_detail"),
path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"),
path("rss/games/", GameFeed(), name="game_feed"), path("rss/games/", GameFeed(), name="game_feed"),
path("rss/games/<str:twitch_id>/campaigns/", GameCampaignFeed(), name="game_campaign_feed"), path("rss/games/<str:twitch_id>/campaigns/", GameCampaignFeed(), name="game_campaign_feed"),
path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"), path("rss/organizations/", OrganizationFeed(), name="organization_feed"),
path("docs/rss/", views.docs_rss_view, name="docs_rss"), path("rss/organizations/<str:twitch_id>/campaigns/", OrganizationCampaignFeed(), name="organization_campaign_feed"),
path("search/", views.search_view, name="search"),
] ]

View file

@ -49,6 +49,42 @@ MIN_QUERY_LENGTH_FOR_FTS = 3
MIN_SEARCH_RANK = 0.05 MIN_SEARCH_RANK = 0.05
def emote_gallery_view(request: HttpRequest) -> HttpResponse:
"""View to display all emote images (distribution_type='EMOTE'), clickable to their campaign.
Args:
request: The HTTP request.
Returns:
HttpResponse: The rendered emote gallery page.
"""
emote_benefits: QuerySet[DropBenefit, DropBenefit] = (
DropBenefit.objects
.filter(distribution_type="EMOTE")
.select_related()
.prefetch_related(
Prefetch(
"drops",
queryset=TimeBasedDrop.objects.select_related("campaign"),
to_attr="_emote_drops",
),
)
)
emotes: list[dict[str, str | DropCampaign]] = []
for benefit in emote_benefits:
# Find the first drop with a campaign for this benefit
drop: TimeBasedDrop | None = next((d for d in getattr(benefit, "_emote_drops", []) if d.campaign), None)
if drop and drop.campaign:
emotes.append({
"image_url": benefit.image_best_url,
"campaign": drop.campaign,
})
context: dict[str, list[dict[str, Any]]] = {"emotes": emotes}
return render(request, "twitch/emote_gallery.html", context)
# MARK: /search/ # MARK: /search/
def search_view(request: HttpRequest) -> HttpResponse: def search_view(request: HttpRequest) -> HttpResponse:
"""Search view for all models. """Search view for all models.