The Dymo page: stock, grid, cut guides and a sheet preview
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vunz4VLxAYDBnU5tLciLDC
This commit is contained in:
1 parent
3b6cb77388
commit
5d118eb286
1 file changed
+267
@@ -0,0 +1,267 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dymo Label Sheets - Admin{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/labels-bricklink.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bl">
|
||||
{% set section = 'labels' %}
|
||||
{% set bar_sub = 'Dymo LabelWriter die-cut stock' %}
|
||||
{% set crumbs = [{'name': 'My Labels', 'url': url_for('labels.index')}, {'name': 'Dymo sheets'}] %}
|
||||
{% include 'admin/labels/_nav.html' %}
|
||||
|
||||
<form method="POST" action="{{ url_for('dymo.index') }}" id="dymoForm">
|
||||
<input type="hidden" name="ids" value="{{ ids }}">
|
||||
{# Same hidden marker as the tape page: it is how an unticked checkbox is
|
||||
told apart from a page opened with no options at all. #}
|
||||
<input type="hidden" name="_opts" value="1">
|
||||
<input type="hidden" name="groups" value="{{ request.values.get('groups', '') }}">
|
||||
|
||||
<div class="row g-2 mt-0">
|
||||
<div class="col-lg-4">
|
||||
<div class="bl-panel">
|
||||
<div class="bl-panel-head"><span>Label stock</span></div>
|
||||
<div class="bl-panel-body">
|
||||
<label class="fld">Loaded in the LabelWriter</label>
|
||||
<select name="stock" class="w-100 mb-2" id="stockSelect">
|
||||
{% for key, preset in stocks.items() %}
|
||||
<option value="{{ key }}" {% if key == sheet.stock %}selected{% endif %}>{{ preset.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="k mb-2" style="text-transform:none;">{{ stock.note }}</div>
|
||||
|
||||
<label class="fld">Labels per sticker</label>
|
||||
<select name="layout" class="w-100 mb-2" id="layoutSelect">
|
||||
{% for key, text in layouts %}
|
||||
<option value="{{ key }}" {% if key == sheet.layout %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="k mb-2" style="text-transform:none;">
|
||||
{{ stock.width_mm }} × {{ stock.height_mm }}mm fits
|
||||
<strong>{{ per_sheet }}</strong> standard 40 × 15mm labels
|
||||
({{ grid }}). A two-part drawer label is taller, so a mixed
|
||||
run fits fewer.
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="fld">Margin (mm)</label>
|
||||
<input type="number" name="margin_mm" step="0.5" min="0" max="10"
|
||||
class="w-100" value="{{ sheet.margin_mm }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="fld">Gap (mm)</label>
|
||||
<input type="number" name="gap_mm" step="0.5" min="0" max="10"
|
||||
class="w-100" value="{{ sheet.gap_mm }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="fld">Skip slots</label>
|
||||
<input type="number" name="start_at" min="0" max="50"
|
||||
class="w-100" value="{{ sheet.start_at }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="fld">Copies each</label>
|
||||
<input type="number" name="copies" min="1" max="99"
|
||||
class="w-100" value="{{ options.copies }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-2">
|
||||
<label class="d-flex align-items-center gap-2" style="font-size:12px;">
|
||||
<input type="checkbox" name="cut_guides" value="1"
|
||||
{% if sheet.cut_guides %}checked{% endif %}>
|
||||
Dashed cut guides
|
||||
</label>
|
||||
<label class="d-flex align-items-center gap-2" style="font-size:12px;">
|
||||
<input type="checkbox" name="centre" value="1"
|
||||
{% if sheet.centre %}checked{% endif %}>
|
||||
Centre on the sticker
|
||||
</label>
|
||||
<label class="d-flex align-items-center gap-2" style="font-size:12px;">
|
||||
<input type="checkbox" name="monochrome" value="1"
|
||||
{% if sheet.monochrome %}checked{% endif %}>
|
||||
Monochrome (thermal)
|
||||
</label>
|
||||
<div class="k mt-1" style="text-transform:none;">
|
||||
A LabelWriter is direct thermal: black dots on white, no
|
||||
colour and no greys of its own. Converting here means the
|
||||
preview shows what will actually come out.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bl-panel">
|
||||
<div class="bl-panel-head"><span>The labels themselves</span></div>
|
||||
<div class="bl-panel-body">
|
||||
<label class="fld">Size</label>
|
||||
<select name="size_mode" class="w-100 mb-2" id="sizeSelect">
|
||||
{% for key, text in size_modes %}
|
||||
<option value="{{ key }}" {% if key == options.size_mode %}selected{% endif %}>{{ text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label class="d-flex align-items-center gap-2 mb-2" style="font-size:12px;">
|
||||
<input type="checkbox" name="group_drawers" value="1"
|
||||
{% if options.group_drawers %}checked{% endif %}>
|
||||
Combine parts that share a drawer
|
||||
</label>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="fld">Text size
|
||||
<span class="num" id="fontOut">{{ '%.2f'|format(options.font_scale) }}×</span>
|
||||
</label>
|
||||
<input type="number" name="font_scale" step="0.05" min="0.5" max="2"
|
||||
class="w-100" value="{{ options.font_scale }}" id="fontNum">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="fld">Image width %</label>
|
||||
<input type="number" name="image_percent" min="0" max="60"
|
||||
class="w-100" value="{{ options.image_percent }}">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input type="range" min="0.5" max="2" step="0.05" class="w-100"
|
||||
value="{{ options.font_scale }}" id="fontRange">
|
||||
<div class="d-flex gap-1 flex-wrap">
|
||||
{% for preset, text in [(1.0, 'Normal'), (1.4, 'Large'), (2.0, 'Largest')] %}
|
||||
<button type="button" class="bl-btn bl-btn-sm font-preset"
|
||||
data-scale="{{ preset }}">{{ text }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="k mt-1" style="text-transform:none;">
|
||||
The name is sized first and the image gives way to it. The
|
||||
whole name always prints, so once it spans the 40mm label
|
||||
turning this up changes nothing — most names hit that by
|
||||
about 1.4×. Untick <em>Part image</em> to give the text
|
||||
the whole width.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="fld">Padding (mm)</label>
|
||||
<input type="number" name="padding_mm" step="0.1" min="0" max="10"
|
||||
class="w-100" value="{{ options.padding_mm }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="fld">Align</label>
|
||||
<select name="align" class="w-100">
|
||||
<option value="left" {% if options.align == 'left' %}selected{% endif %}>Left</option>
|
||||
<option value="center" {% if options.align == 'center' %}selected{% endif %}>Centre</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bl-panel">
|
||||
<div class="bl-panel-head"><span>Show on label</span></div>
|
||||
<div class="bl-panel-body">
|
||||
{% for value, text in field_options %}
|
||||
<label class="d-flex align-items-center gap-2" style="font-size:12px;">
|
||||
<input type="checkbox" name="fields" value="{{ value }}"
|
||||
{% if value in options.fields %}checked{% endif %}> {{ text }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="bl-panel">
|
||||
<div class="bl-panel-head"><span>Print</span></div>
|
||||
<div class="bl-panel-body">
|
||||
<div class="d-flex gap-2 flex-wrap align-items-center">
|
||||
<button type="submit" formaction="{{ url_for('dymo.download_pdf') }}"
|
||||
class="bl-btn bl-btn-go">
|
||||
<i class="bi bi-file-pdf"></i>
|
||||
Download PDF — {{ pages|length }} sticker{{ '' if pages|length == 1 else 's' }}
|
||||
</button>
|
||||
<button type="submit" class="bl-btn">Refresh preview</button>
|
||||
<a href="{{ url_for('tape.index', ids=ids) }}" class="bl-btn">
|
||||
<i class="bi bi-upc"></i> Tape instead
|
||||
</a>
|
||||
</div>
|
||||
<div class="k mt-2" style="text-transform:none;">
|
||||
Each PDF page is exactly {{ stock.width_mm }} × {{ stock.height_mm }}mm,
|
||||
so print it with the Dymo driver set to this label and
|
||||
<strong>scale 100% / Actual size</strong> — not "Fit to page",
|
||||
which shrinks everything by a few percent and spoils the 40mm width.
|
||||
The LabelWriter 4XL is USB, so LIM can't print to it over the
|
||||
network the way it does the tape printer.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bl-panel">
|
||||
<div class="bl-panel-head">
|
||||
<span>Preview — {{ pages|length }} sticker{{ '' if pages|length == 1 else 's' }}
|
||||
from {{ plan|length }} label{{ '' if plan|length == 1 else 's' }}</span>
|
||||
<span class="num">{{ stock.width_mm }} × {{ stock.height_mm }}mm</span>
|
||||
</div>
|
||||
<div class="bl-panel-body">
|
||||
{% if not options.fields %}
|
||||
<div class="mb-2"><span class="bl-tag warn">Nothing selected</span>
|
||||
Every field is unticked, so the labels will print blank.</div>
|
||||
{% endif %}
|
||||
|
||||
{% for page in pages[:6] %}
|
||||
<div class="mb-3">
|
||||
<div class="k mb-1" style="text-transform:none;">
|
||||
Sticker {{ loop.index }} of {{ pages|length }} ·
|
||||
{{ page | rejectattr('blank') | list | length }} label{{ '' if page|length == 1 else 's' }}
|
||||
</div>
|
||||
<img style="border:1px solid #c9d3dd;background:#fff;max-width:100%;"
|
||||
alt="Dymo sticker {{ loop.index }}"
|
||||
src="{{ url_for('dymo.preview', page=loop.index, **query) }}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if pages|length > 6 %}
|
||||
<div class="k" style="text-transform:none;">
|
||||
Showing the first 6 of {{ pages|length }}. All of them are in the PDF.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="k mt-2" style="text-transform:none;">
|
||||
300 dpi, the LabelWriter's native resolution. Drawn by the same
|
||||
code as the tape labels, so a drawer label is the same
|
||||
{{ '%.0f'|format(40) }} × 15mm sticker whichever printer makes it.
|
||||
Cut along the dashed lines.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Changing the stock, the grid or the label size re-renders the sheet.
|
||||
['stockSelect', 'layoutSelect', 'sizeSelect'].forEach(id => {
|
||||
document.getElementById(id)?.addEventListener('change',
|
||||
() => document.getElementById('dymoForm').submit());
|
||||
});
|
||||
|
||||
// Text size: slider, box and presets all drive the one value.
|
||||
(function () {
|
||||
const num = document.getElementById('fontNum');
|
||||
const range = document.getElementById('fontRange');
|
||||
const out = document.getElementById('fontOut');
|
||||
if (!num || !range) { return; }
|
||||
const show = v => { if (out) { out.textContent = Number(v).toFixed(2) + '×'; } };
|
||||
num.addEventListener('input', () => { range.value = num.value; show(num.value); });
|
||||
range.addEventListener('input', () => { num.value = range.value; show(range.value); });
|
||||
range.addEventListener('change', () => document.getElementById('dymoForm').submit());
|
||||
document.querySelectorAll('.font-preset').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
num.value = range.value = btn.dataset.scale;
|
||||
show(btn.dataset.scale);
|
||||
document.getElementById('dymoForm').submit();
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user