.
This commit is contained in:
@@ -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 %}
|
||||
|
||||
@@ -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 }} — {{ card.get_diet_display }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user