Tape page: combine/split buttons, text size slider, print guard
Tick labels under the preview to put them on one label; the grouping is rebuilt from the whole plan, not just the twelve previewed. Text size gains a slider and Normal/Large/Huge/Max presets. The print button disables itself after a click so a double-click can't spend the tape twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vunz4VLxAYDBnU5tLciLDC
This commit is contained in:
1 parent
fbfd66d889
commit
085fe1d701
1 file changed
+165
-4
@@ -18,6 +18,9 @@
|
|||||||
opened with no options at all. Without it, a default-on checkbox could
|
opened with no options at all. Without it, a default-on checkbox could
|
||||||
never be turned off. #}
|
never be turned off. #}
|
||||||
<input type="hidden" name="_opts" value="1">
|
<input type="hidden" name="_opts" value="1">
|
||||||
|
{# Which parts share a physical label: "1,2|3". Rewritten by the Combine
|
||||||
|
and Split buttons under the preview. Empty means "group by drawer". #}
|
||||||
|
<input type="hidden" name="groups" id="groupSpec" value="{{ request.values.get('groups', '') }}">
|
||||||
|
|
||||||
<div class="row g-2 mt-0">
|
<div class="row g-2 mt-0">
|
||||||
<!-- Options -->
|
<!-- Options -->
|
||||||
@@ -67,9 +70,29 @@
|
|||||||
value="{{ options.copies }}">
|
value="{{ options.copies }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="fld">Text size</label>
|
<label class="fld">Text size
|
||||||
<input type="number" name="font_scale" step="0.05" min="0.5" max="2.5"
|
<span class="num" id="fontOut">{{ '%.2f'|format(options.font_scale) }}×</span>
|
||||||
class="w-100" value="{{ options.font_scale }}">
|
</label>
|
||||||
|
<input type="number" name="font_scale" step="0.05" min="0.5" max="3"
|
||||||
|
class="w-100" value="{{ options.font_scale }}" id="fontNum">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<input type="range" min="0.5" max="3" 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.5, 'Large'), (2.0, 'Huge'), (3.0, 'Max')] %}
|
||||||
|
<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 part name is sized first and the image gives way to
|
||||||
|
it, so turning this up shrinks the picture rather than
|
||||||
|
clipping the text. The whole name always prints - it wraps
|
||||||
|
and settles at the largest size that fits, so a long name
|
||||||
|
stops growing before a short one does. Untick
|
||||||
|
<em>Part image</em> for the biggest type of all.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="fld">Image width %</label>
|
<label class="fld">Image width %</label>
|
||||||
@@ -161,6 +184,13 @@
|
|||||||
<i class="bi bi-file-pdf"></i> Download PDF
|
<i class="bi bi-file-pdf"></i> Download PDF
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="k mt-2" style="text-transform:none;">
|
||||||
|
Jobs are sent one at a time: LIM waits for the printer to
|
||||||
|
finish each label before sending the next, because the
|
||||||
|
VC-500W answers "busy" to anything that arrives mid-print.
|
||||||
|
So a big run takes a while - leave the page alone until it
|
||||||
|
comes back rather than refreshing, or you'll print twice.
|
||||||
|
</div>
|
||||||
<div id="testResult" class="mt-2" style="font-size:12px;"></div>
|
<div id="testResult" class="mt-2" style="font-size:12px;"></div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
@@ -195,9 +225,32 @@
|
|||||||
will be scaled down. Load 50mm tape, or pick a different size.</div>
|
will be scaled down. Load 50mm tape, or pick a different size.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 flex-wrap align-items-center mb-2" id="groupBar"
|
||||||
|
data-plan="{{ group_spec }}">
|
||||||
|
<span class="k" style="text-transform:none;">Tick labels, then:</span>
|
||||||
|
<button type="button" class="bl-btn bl-btn-sm" id="combineBtn">
|
||||||
|
<i class="bi bi-box-arrow-in-down"></i> Combine onto one label
|
||||||
|
</button>
|
||||||
|
<button type="button" class="bl-btn bl-btn-sm" id="splitBtn">
|
||||||
|
<i class="bi bi-scissors"></i> Split apart
|
||||||
|
</button>
|
||||||
|
<button type="button" class="bl-btn bl-btn-sm" id="resetGroupsBtn">
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i> Back to drawers
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="k mb-2" style="text-transform:none;">
|
||||||
|
For customs and MOC drawers, where the parts have no drawer in
|
||||||
|
common. A combined label grows 5mm for each part past the second,
|
||||||
|
and hand-made groups are kept whatever the size setting says.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bl-tape-preview">
|
<div class="bl-tape-preview">
|
||||||
{% for item in plan[:12] %}
|
{% for item in plan[:12] %}
|
||||||
<div class="mb-2">
|
<div class="mb-2 plan-card" data-ids="{{ item.labels | map(attribute='id') | join(',') }}">
|
||||||
|
<label class="d-flex align-items-center gap-2 mb-1" style="font-size:12px;">
|
||||||
|
<input type="checkbox" class="plan-pick">
|
||||||
|
<span>{{ item.labels | map(attribute='display_title') | join(' + ') }}</span>
|
||||||
|
</label>
|
||||||
<img alt="{{ item.labels | map(attribute='display_title') | join(' + ') }}"
|
<img alt="{{ item.labels | map(attribute='display_title') | join(' + ') }}"
|
||||||
src="{{ url_for('tape.preview',
|
src="{{ url_for('tape.preview',
|
||||||
ids=item.labels | map(attribute='id') | join(','),
|
ids=item.labels | map(attribute='id') | join(','),
|
||||||
@@ -248,6 +301,17 @@
|
|||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
|
// One click, one print run. A double-click used to queue the batch twice,
|
||||||
|
// which on tape costs real money.
|
||||||
|
document.getElementById('tapeForm')?.addEventListener('submit', function (ev) {
|
||||||
|
const btn = ev.submitter;
|
||||||
|
if (btn && btn.formAction && btn.formAction.indexOf('/print') !== -1) {
|
||||||
|
if (btn.dataset.sent) { ev.preventDefault(); return; }
|
||||||
|
btn.dataset.sent = '1';
|
||||||
|
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Printing…';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('testBtn')?.addEventListener('click', function () {
|
document.getElementById('testBtn')?.addEventListener('click', function () {
|
||||||
const out = document.getElementById('testResult');
|
const out = document.getElementById('testResult');
|
||||||
out.innerHTML = '<span class="bl-tag">Probing…</span>';
|
out.innerHTML = '<span class="bl-tag">Probing…</span>';
|
||||||
@@ -283,5 +347,102 @@ document.getElementById('testBtn')?.addEventListener('click', function () {
|
|||||||
document.getElementById(id)?.addEventListener('change',
|
document.getElementById(id)?.addEventListener('change',
|
||||||
() => document.getElementById('tapeForm').submit());
|
() => document.getElementById('tapeForm').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('tapeForm').submit());
|
||||||
|
document.querySelectorAll('.font-preset').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
num.value = range.value = btn.dataset.scale;
|
||||||
|
show(btn.dataset.scale);
|
||||||
|
document.getElementById('tapeForm').submit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Combining parts onto one label by hand.
|
||||||
|
//
|
||||||
|
// The grouping lives in the hidden `groups` field as "1,2|3". These buttons
|
||||||
|
// rewrite it from what is ticked and resubmit, so the preview always shows
|
||||||
|
// the real physical labels rather than a guess.
|
||||||
|
//
|
||||||
|
// The starting point is the whole current plan (data-plan), not just the
|
||||||
|
// cards on screen - only the first 12 labels are previewed, and a spec built
|
||||||
|
// from those alone would quietly un-group everything after them.
|
||||||
|
(function () {
|
||||||
|
const spec = document.getElementById('groupSpec');
|
||||||
|
const bar = document.getElementById('groupBar');
|
||||||
|
const form = document.getElementById('tapeForm');
|
||||||
|
if (!spec || !bar || !form) { return; }
|
||||||
|
|
||||||
|
const plan = () => (bar.dataset.plan || '')
|
||||||
|
.split('|').filter(Boolean).map(g => g.split(',').filter(Boolean));
|
||||||
|
|
||||||
|
function tickedIds() {
|
||||||
|
const ids = new Set();
|
||||||
|
document.querySelectorAll('.plan-card').forEach(card => {
|
||||||
|
if (card.querySelector('.plan-pick')?.checked) {
|
||||||
|
(card.dataset.ids || '').split(',').filter(Boolean)
|
||||||
|
.forEach(id => ids.add(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(groups) {
|
||||||
|
spec.value = groups.filter(g => g.length).map(g => g.join(',')).join('|');
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('combineBtn')?.addEventListener('click', () => {
|
||||||
|
const picked = tickedIds();
|
||||||
|
if (picked.size < 2) {
|
||||||
|
alert('Tick two or more labels to combine them onto one.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groups = [];
|
||||||
|
const merged = [];
|
||||||
|
let placed = false;
|
||||||
|
plan().forEach(group => {
|
||||||
|
const mine = group.filter(id => picked.has(id));
|
||||||
|
const rest = group.filter(id => !picked.has(id));
|
||||||
|
if (mine.length) {
|
||||||
|
merged.push(...mine);
|
||||||
|
// Keep the combined label where the first ticked part was,
|
||||||
|
// so the preview does not jump to the end of a long list.
|
||||||
|
if (!placed) { groups.push(merged); placed = true; }
|
||||||
|
}
|
||||||
|
if (rest.length) { groups.push(rest); }
|
||||||
|
});
|
||||||
|
write(groups);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('splitBtn')?.addEventListener('click', () => {
|
||||||
|
const picked = tickedIds();
|
||||||
|
if (!picked.size) {
|
||||||
|
alert('Tick the labels you want split apart first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groups = [];
|
||||||
|
plan().forEach(group => {
|
||||||
|
const rest = group.filter(id => !picked.has(id));
|
||||||
|
group.filter(id => picked.has(id)).forEach(id => groups.push([id]));
|
||||||
|
if (rest.length) { groups.push(rest); }
|
||||||
|
});
|
||||||
|
write(groups);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('resetGroupsBtn')?.addEventListener('click', () => {
|
||||||
|
spec.value = '';
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in new issue
Block a user