This commit is contained in:
Ross
2025-12-06 20:46:04 +00:00
parent c30cb45fc3
commit 0e4e52fce6
5 changed files with 173 additions and 0 deletions
+3
View File
@@ -15,6 +15,9 @@
<div class="level-item">
<a class="button is-light" href="{% url 'cards:export_table' %}">Export table</a>
</div>
<div class="level-item">
<a class="button is-light" href="{% url 'cards:print' %}">Print cards</a>
</div>
</div>
</div>
{% if card_count is not None %}
+55
View File
@@ -0,0 +1,55 @@
{% extends 'cards/base.html' %}
{% block content %}
<div class="container">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
<a class="button is-light" href="{% url 'cards:list' %}">← Back to list</a>
<h2 class="title" style="margin:0">Print cards</h2>
</div>
<p class="subtitle">Select which cards to include and choose a design. Click <strong>Open printable view</strong> to open the print preview in a new tab (you can then print to PDF or to an A4 printer).</p>
<form id="print-form" method="post" action="{% url 'cards:print_preview' %}" target="_blank">
{% csrf_token %}
<div style="display:flex; gap:12px; align-items:center; margin-bottom:10px;">
<div>
<label class="label">Design</label>
<div class="select">
<select name="design">
<option value="1">Design 1</option>
<option value="2">Design 2</option>
<option value="3">Design 3</option>
<option value="4">Design 4</option>
<option value="5">Design 5</option>
</select>
</div>
</div>
<div>
<label class="label">Scale</label>
<div class="control"><input class="input" type="number" name="scale" value="1" step="0.05" min="0.5" max="2"></div>
</div>
<div style="margin-left:auto">
<button class="button is-primary" type="submit">Open printable view</button>
</div>
</div>
<div style="display:flex; flex-direction:column; gap:8px;">
{% for card in cards %}
<label style="display:flex; align-items:center; gap:10px; padding:6px; border:1px solid rgba(0,0,0,0.04); border-radius:6px;">
<input type="checkbox" name="cards" value="{{ card.pk }}" checked>
<div style="display:flex; align-items:center; gap:10px;">
{% if card.image %}
<img src="{{ card.image.url }}" style="width:72px; height:56px; object-fit:cover; border-radius:6px;">
{% else %}
<div style="width:72px; height:56px; background:#f2f2f2; border-radius:6px"></div>
{% endif %}
<div>
<div style="font-weight:700">{{ card.name }}</div>
<div class="is-size-7">{{ card.get_era_display }} &mdash; {{ card.get_diet_display }}</div>
</div>
</div>
</label>
{% endfor %}
</div>
</form>
</div>
{% endblock %}
+71
View File
@@ -0,0 +1,71 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Printable cards</title>
<style>
/* A4 portrait: 210mm x 297mm. Use mm units to better match print sizing. */
@page { size: A4 portrait; margin: 12mm; }
html,body{height:100%; margin:0; padding:0;}
body{font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; color:#111}
.sheet{width:210mm; min-height:297mm; box-sizing:border-box; padding:6mm; display:block}
.cards-grid{display:grid; grid-template-columns:repeat(2, 1fr); gap:8mm}
.print-card{break-inside:avoid; page-break-inside:avoid; width:100%;}
/* Scale the toptrump-card down a bit for printing */
.toptrump-card{box-shadow:none; border:none;}
/* Force designs to sizes that fit two-up per A4 page */
.toptrump-card.design-1,
.toptrump-card.design-2,
.toptrump-card.design-3,
.toptrump-card.design-5 { width: 98%; min-height: 120mm; }
.toptrump-card.design-4 { width: 98%; min-height: 135mm; }
/* Ensure hero images scale inside print cards */
.print-card .hero-image{height:auto; min-height:40mm;}
.print-card img.card-hero-img{max-width:100%; height:auto; object-fit:contain}
/* Small caption */
.caption{font-size:12px; color:#333; margin-top:6px}
@media print{
body{background:transparent}
.sheet{box-shadow:none}
.no-print{display:none}
}
</style>
</head>
<body>
<div style="display:flex; justify-content:space-between; align-items:center; padding:8px 12px;">
<div>
<a class="no-print" href="{% url 'cards:print' %}" style="text-decoration:none; color:#111; font-weight:700;">← Back to selection</a>
</div>
<div>
<button class="no-print" onclick="window.print();" style="padding:8px 12px; font-weight:700;">Print</button>
</div>
</div>
{% for page_cards in pages %}
<div class="sheet">
<div class="cards-grid">
{% for card in page_cards %}
<div class="print-card">
<div class="toptrump-card design-{{ design }}">
{% comment %} Reuse minimal inline markup for print: image, title, era/diet {% endcomment %}
<div class="hero-image" style="background:{% if card.background and card.background.image %}url('{{ card.background.image.url }}'){% endif %}; background-size:cover; background-position:center">
{% if card.image %}
<img class="card-hero-img" src="{{ card.image.url }}" alt="{{ card.name }}">
{% endif %}
</div>
<div style="padding:6px">
<div style="font-weight:800; font-size:14pt">{{ card.name }}</div>
<div style="margin-top:6px; font-size:11pt">{{ card.get_era_display }} — {{ card.get_diet_display }}</div>
{% if card.facts_list %}
<div class="caption">{{ card.facts_list|join:', ' }}</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</body>
</html>
+2
View File
@@ -9,6 +9,8 @@ urlpatterns = [
path('<int:pk>/edit/', views.card_edit, name='edit'),
path('<int:pk>/delete/', views.card_delete, name='delete'),
path('preview/<int:pk>/', views.preview_card, name='preview'),
path('print/', views.print_cards, name='print'),
path('print/preview/', views.print_preview, name='print_preview'),
path('backgrounds/', views.background_list, name='backgrounds'),
path('backgrounds/create/', views.background_create, name='background_create'),
path('backgrounds/<int:pk>/edit/', views.background_edit, name='background_edit'),
+42
View File
@@ -412,6 +412,48 @@ def export_table(request):
return render(request, 'cards/export_table.html', {'json_text': json_text})
def print_cards(request):
"""Selection UI where the user picks which cards to print and which design."""
qs = Dinosaur.objects.order_by('name')
background = BackgroundImage.get_active() if BackgroundImage.objects.exists() else None
return render(request, 'cards/print.html', {'cards': qs, 'background': background})
def print_preview(request):
"""Render a printable view of selected cards.
POST params:
- cards: repeated card pks
- design: design number
- scale: optional scale factor
Returns an HTML A4-friendly document that the browser can print or save as PDF.
"""
# Accept POST or GET for easier testing
data = request.POST if request.method == 'POST' else request.GET
pks = data.getlist('cards')
if not pks:
return HttpResponseBadRequest('No cards selected')
try:
design = int(data.get('design', '1'))
except Exception:
design = 1
scale = float(data.get('scale', '1')) if data.get('scale') else 1.0
# Fetch cards preserving the given order
cards = list(Dinosaur.objects.filter(pk__in=pks))
# Reorder according to provided pks
pk_to_card = {str(c.pk): c for c in cards}
ordered = [pk_to_card[pk] for pk in pks if pk in pk_to_card]
# Build pages: two cards per A4 page (2-up)
pages = []
per_page = 2
for i in range(0, len(ordered), per_page):
pages.append(ordered[i:i+per_page])
return render(request, 'cards/printable.html', {'pages': pages, 'design': design, 'scale': scale})
def _parse_pasted_input(text):
"""Try to parse pasted LLM output into a list of suggestion dicts.