mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-08-03 23:21:44 +00:00
Adds a "Print to Niimbot" panel to the label generator dialog that sends the generated label straight to a Niimbot thermal printer (e.g. B1) over Web Bluetooth, without a PDF print dialog or printer driver. The existing server-side DomPDF label is reused as-is: a new Stimulus controller rasterizes the PDF preview page-by-page with pdf.js at the printer's native resolution, converts it to a 1-bit bitmap and prints it via the niimbluelib library. No changes to the PHP label pipeline are required. The feature is disabled by default and gated behind the NIIMBOT_ENABLED environment variable, since it is only useful for users who own such a printer. - Add @mmote/niimbluelib and pdfjs-dist dependencies - New assets/controllers/pages/niimbot_print_controller.js - Print options (copies, density, label type, rotation, B/W threshold) in the label dialog, gracefully disabled when Web Bluetooth is unavailable - NIIMBOT_ENABLED env flag (off by default), wired through parameters.yaml and a Twig global, documented in .env - English translations and documentation Requires a Chromium-based browser and a secure context (HTTPS or localhost).
214 lines
12 KiB
Twig
214 lines
12 KiB
Twig
{% extends 'main_card.html.twig' %}
|
|
|
|
{% block title %}{% trans %}label_generator.title{% endtrans %}{% endblock %}
|
|
|
|
{%- block card_title -%}
|
|
<i class="fas fa-qrcode fa-fw"></i> {% trans %}label_generator.title{% endtrans %}
|
|
{% if profile %}({{ profile.name }}){% endif %}
|
|
{%- endblock -%}
|
|
|
|
{% block card_content %}
|
|
{{ form_start(form) }}
|
|
|
|
{# Default submit to use when pressing enter. #}
|
|
<input type="submit" name="label_dialog[update]" class="d-none">
|
|
|
|
<ul class="nav nav-tabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" data-bs-toggle="tab" id="common-tab" role="tab" aria-controls="common" aria-selected="true" href="#common"
|
|
>{% trans %}label_generator.common{% endtrans %}</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" id="advanced-tab" role="tab" aria-controls="advanced" aria-selected="false" href="#advanced"
|
|
>{% trans %}label_generator.advanced{% endtrans %}</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" id="profiles-tab" role="tab" aria-controls="profiles" aria-selected="false" href="#profiles"
|
|
>{% trans %}label_generator.profiles{% endtrans %}</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content mt-2">
|
|
|
|
<div class="tab-pane active" id="common" role="tabpanel" aria-labelledby="common-tab">
|
|
{{ form_row(form.target_id) }}
|
|
|
|
{{ form_row(form.options.supported_element) }}
|
|
<div class="mb-2 row">
|
|
{{ form_label(form.options.width) }}
|
|
<div class="{{ col_input }}">
|
|
<div class="input-group">
|
|
{{ form_widget(form.options.width) }}
|
|
|
|
<span class="input-group-text">x</span>
|
|
|
|
{{ form_widget(form.options.height) }}
|
|
|
|
<span class="input-group-text">mm</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ form_row(form.options.barcode_type) }}
|
|
{{ form_row(form.options.lines) }}
|
|
</div>
|
|
|
|
<div class="tab-pane" id="advanced" role="tabpanel" aria-labelledby="advanced-tab">
|
|
{{ form_row(form.options.additional_css) }}
|
|
{{ form_widget(form.options) }}
|
|
</div>
|
|
|
|
<div class="tab-pane" id="profiles" role="tabpanel" aria-labelledby="profiles-tab">
|
|
<div class="form-group row">
|
|
<label class="{{ col_label }} col-form-label">{% trans %}label_generator.selected_profile{% endtrans %}</label>
|
|
<div class="{{ col_input }}">
|
|
<span class="form-control-plaintext">{{ profile.name ?? '-' }}
|
|
{% if profile and is_granted("edit", profile) %}
|
|
<a href="{{ entity_url(profile, 'edit') }}" title="{% trans %}label_generator.edit_profile{% endtrans %}"
|
|
><i class="fas fa-edit"></i></a>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<div class="{{ offset_label }} {{ col_input }}">
|
|
<div class="dropdown">
|
|
<button class="btn btn-info dropdown-toggle" type="button" id="loadProfilesButton"
|
|
{% if not is_granted("@labels.create_labels") %}disabled{% endif %}
|
|
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
{% trans %}label_generator.load_profile{% endtrans %}
|
|
</button>
|
|
<div class="dropdown-menu" aria-labelledby="loadProfilesButton">
|
|
{% if is_granted("@labels.create_labels") %}
|
|
{% for type in enum_cases("App\\Entity\\LabelSystem\\LabelSupportedElement") %}
|
|
{% set profiles = label_profile_dropdown_helper.dropdownProfiles(type) %}
|
|
{% if profiles is not empty %}
|
|
<h6 class="dropdown-header">{{ (type.value~'.label') | trans }}</h6>
|
|
{% endif %}
|
|
{% for profile in profiles %}
|
|
<a class="dropdown-item" href="{{ path('label_dialog_profile', {'profile': profile.id }) }}">{{ profile.name }}</a>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if is_granted("@labels.read_profiles") %}
|
|
<div class="form-group row">
|
|
<div class="{{ offset_label }} {{ col_input }}">
|
|
<a class="btn btn-link" href="{{ path('label_profile_new') }}">{% trans %}label_generator.edit_profiles{% endtrans %}</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if form.update_profile is defined %}
|
|
{{ form_row(form.update_profile) }}
|
|
{% endif %}
|
|
|
|
<div class="form-group row">
|
|
<div class="{{ offset_label }} {{ col_input }}">
|
|
<div class="input-group">
|
|
{{ form_widget(form.save_profile_name) }}
|
|
{{ form_widget(form.save_profile) }}
|
|
</div>
|
|
{{ form_errors(form.save_profile_name) }}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
{{ form_end(form) }}
|
|
{% if pdf_data %}
|
|
<div class="row">
|
|
<div class="{{ col_input }} {{ offset_label }}">
|
|
<a data-turbo="false" class="btn btn-secondary" href="#" {{ stimulus_controller('pages/label_download_btn')}} {{ stimulus_action('pages/label_download_btn', 'download')}} download="{{ filename ?? '' }}">
|
|
{% trans %}label_generator.download{% endtrans %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if niimbot_enabled %}
|
|
<div class="row mt-3" {{ stimulus_controller('pages/niimbot_print', {
|
|
messages: {
|
|
no_bluetooth: 'label_generator.thermal.no_bluetooth'|trans,
|
|
no_label: 'label_generator.thermal.no_label'|trans,
|
|
connecting: 'label_generator.thermal.connecting'|trans,
|
|
connected: 'label_generator.thermal.connected'|trans,
|
|
rendering: 'label_generator.thermal.rendering'|trans,
|
|
printing: 'label_generator.thermal.printing'|trans,
|
|
done: 'label_generator.thermal.done'|trans,
|
|
too_wide: 'label_generator.thermal.too_wide'|trans,
|
|
error: 'label_generator.thermal.error'|trans
|
|
}
|
|
}) }}>
|
|
<div class="{{ col_input }} {{ offset_label }}">
|
|
<div class="card border-secondary">
|
|
<div class="card-body">
|
|
<h6 class="card-title mb-1">
|
|
<i class="fas fa-print fa-fw"></i> {% trans %}label_generator.thermal.title{% endtrans %}
|
|
</h6>
|
|
<p class="text-muted small mb-2">{% trans %}label_generator.thermal.hint{% endtrans %}</p>
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-6 col-md-3">
|
|
<label class="form-label small mb-0" for="niimbot_copies">{% trans %}label_generator.thermal.copies{% endtrans %}</label>
|
|
<input type="number" min="1" value="1" class="form-control form-control-sm" id="niimbot_copies" {{ stimulus_target('pages/niimbot_print', 'copies') }}>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="form-label small mb-0" for="niimbot_density">{% trans %}label_generator.thermal.density{% endtrans %}</label>
|
|
<select class="form-select form-select-sm" id="niimbot_density" {{ stimulus_target('pages/niimbot_print', 'density') }}>
|
|
<option value="">{% trans %}label_generator.thermal.density_auto{% endtrans %}</option>
|
|
<option value="1">1</option>
|
|
<option value="2">2</option>
|
|
<option value="3">3</option>
|
|
<option value="4">4</option>
|
|
<option value="5">5</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="form-label small mb-0" for="niimbot_labeltype">{% trans %}label_generator.thermal.label_type{% endtrans %}</label>
|
|
<select class="form-select form-select-sm" id="niimbot_labeltype" {{ stimulus_target('pages/niimbot_print', 'labelType') }}>
|
|
<option value="1">{% trans %}label_generator.thermal.label_type_gaps{% endtrans %}</option>
|
|
<option value="3">{% trans %}label_generator.thermal.label_type_continuous{% endtrans %}</option>
|
|
<option value="2">{% trans %}label_generator.thermal.label_type_black{% endtrans %}</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="form-label small mb-0" for="niimbot_rotation">{% trans %}label_generator.thermal.rotation{% endtrans %}</label>
|
|
<select class="form-select form-select-sm" id="niimbot_rotation" {{ stimulus_target('pages/niimbot_print', 'rotation') }}>
|
|
<option value="0">0°</option>
|
|
<option value="90">90°</option>
|
|
<option value="180">180°</option>
|
|
<option value="270">270°</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<label class="form-label small mb-0" for="niimbot_threshold">{% trans %}label_generator.thermal.threshold{% endtrans %}</label>
|
|
<input type="number" min="1" max="254" value="128" class="form-control form-control-sm" id="niimbot_threshold" {{ stimulus_target('pages/niimbot_print', 'threshold') }}>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2">
|
|
<button type="button" class="btn btn-primary btn-sm" {{ stimulus_target('pages/niimbot_print', 'button') }} {{ stimulus_action('pages/niimbot_print', 'print') }}>
|
|
<i class="fas fa-print fa-fw"></i> {% trans %}label_generator.thermal.print{% endtrans %}
|
|
</button>
|
|
</div>
|
|
<div class="mt-2 small text-muted" {{ stimulus_target('pages/niimbot_print', 'status') }}></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block additional_content %}
|
|
{% if pdf_data %}
|
|
<div class="card mt-2 p-1 border-secondary" style="resize: vertical; overflow: scroll; height: 280px">
|
|
<object id="pdf_preview" data="{{ pdf_data | data_uri(mime='application/pdf') }}" style="height: inherit">
|
|
</object>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|