Files
ImpTune/imptune/templates/partials/driver_edit_modal.html
T
kawaandClaude Opus 5 2c06806814 feat: driver rename, driver icons, web image + driver search
Driver rename and icons: `Driver.display_name` plus a `DriverIcon` table, both
global/shared like the `Driver` row they hang off, so a rename or an icon is
what every Owner sees. The rename/icon dialog keeps its forms as siblings
(nested forms are invalid HTML) and the icon routes return an `hx-swap-oob`
thumbnail refresh rather than re-rendering the table, which would tear the open
`<dialog>` out of the DOM.

Web image picker: `GET /web/images` renders a pickable grid for a printer or a
driver icon, with the search term prefilled from the entity name and editable.
Picking one downloads it server-side and normalizes it.

Driver download search: `GET /web/drivers` searches for a vendor-wide driver
(the term is rewritten into the vendor's real product name for 15 brands) or for
the exact model as typed. Links only — nothing is downloaded, and the fragment
says the results are unvetted.

Icon uploads no longer reject off-size or non-PNG files: `normalize_icon()`
letterboxes any decodable raster into a 256x256 PNG. An already-exact 256x256
PNG is returned byte-identical, because icon storage is content-addressed and
re-encoding would move the file on every save.

`fetch_image()` makes the request from the server, so `assert_fetchable()`
refuses any URL resolving to a private, loopback, or link-local address, and
re-runs on every redirect. ImpTune sits on the same LAN as the printers it
configures; an unguarded fetcher would be a port scanner for anyone who can
reach the UI.

DuckDuckGo is scraped, not called through an API — no key needed, but fragile,
so both search functions swallow parse failures and return [] instead of 500ing
a page. `WEB_SEARCH=false` disables every outbound request and hides the
controls, for air-gapped installs.

Also: one shared `Jinja2Templates` in `templating.py` instead of five per-router
instances, so a template global is declared once; `_add_missing_columns()` in
`database.py` adds new nullable columns to a pre-existing table, which
`create_tables(safe=True)` skips; `db_env` in test_db.py now closes its
connection on teardown, or the next test's ORM writes land in the previous
test's DB file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:44:47 +02:00

110 lines
4.8 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{#
Rename + icon dialog for one driver row. `item` is a build_driver_data() entry.
The rename form and the icon forms are siblings, not nested — a form inside a
form is invalid HTML and the browser drops the inner one. The footer's Save
reaches the rename form through `form="..."`.
Trigger is located by `button[onclick*='showModal']` in E2E specs, never by its
label: the default language follows navigator.language.
#}
{% import "partials/icons.html" as ico %}
{% set d = item.driver %}
<button class="btn ghost sm"
onclick="document.getElementById('driver-modal-{{ d.id }}').showModal()"
x-data>
{{ ico.i('pencil', 14) }}<span x-text="$store.i18n.t('edit')">Edit</span>
</button>
<dialog id="driver-modal-{{ d.id }}">
<article>
<div class="dialog-head">
<h3 x-data x-text="$store.i18n.t('edit_driver_title')">Edit driver</h3>
<button type="button" class="icon-btn close" x-data
:aria-label="$store.i18n.t('cancel')"
onclick="document.getElementById('driver-modal-{{ d.id }}').close()">&times;</button>
</div>
<div class="dialog-body">
<form id="driver-rename-{{ d.id }}"
hx-patch="/drivers/{{ d.id }}"
hx-target="#driver-list"
hx-swap="outerHTML"
hx-on::after-request="document.getElementById('driver-modal-{{ d.id }}').close()">
<fieldset class="form-section">
<legend x-data x-text="$store.i18n.t('section_driver_name')">Name</legend>
<label>
<span class="label-text" x-data x-text="$store.i18n.t('driver_display_name')">Display name</span>
<input type="text" name="display_name" maxlength="120"
value="{{ d.display_name or '' }}"
placeholder="{{ d.original_filename }}">
</label>
<span class="hint" x-data x-text="$store.i18n.t('hint_driver_rename')">Shared with everyone — the driver library is global. Leave empty to show the ZIP filename.</span>
</fieldset>
</form>
<fieldset class="form-section">
<legend x-data x-text="$store.i18n.t('icon_section')">Icon</legend>
<div id="driver-icon-status-{{ d.id }}">
{% if item.has_icon %}
<div class="icon-preview">
<img src="/drivers/{{ d.id }}/icon" width="56" height="56" alt="">
<span class="meta">256&times;256 PNG</span>
</div>
{% else %}
<p class="dim" x-data x-text="$store.i18n.t('no_driver_icon')">No icon yet.</p>
{% endif %}
</div>
<form hx-post="/drivers/{{ d.id }}/icon"
hx-target="#driver-icon-status-{{ d.id }}"
hx-swap="innerHTML"
hx-encoding="multipart/form-data">
<div class="uploader">
<input type="file" name="file" accept="image/*" required
x-data :aria-label="$store.i18n.t('upload_icon')">
<div class="btn-row">
<button type="submit" class="btn ghost sm" x-data>
{{ ico.i('upload', 13) }}<span x-text="$store.i18n.t('upload_icon')">Upload icon</span>
</button>
</div>
<p class="hint" x-data x-text="$store.i18n.t('hint_any_image')">Any image — resized to 256 × 256 PNG. 750 KB max.</p>
</div>
</form>
{% if web_search_enabled() %}
<div class="web-picker">
<form hx-get="/web/images"
hx-target="#driver-image-results-{{ d.id }}"
hx-swap="innerHTML"
hx-indicator="#driver-image-spinner-{{ d.id }}">
<input type="hidden" name="target" value="driver">
<input type="hidden" name="id" value="{{ d.id }}">
<label>
<span class="label-text" x-data x-text="$store.i18n.t('search_web_image')">Search the web for an image</span>
<input type="search" name="q" value="{{ item.image_query }}">
</label>
<div class="btn-row">
<button type="submit" class="btn ghost sm" x-data>
{{ ico.i('search', 13) }}<span x-text="$store.i18n.t('search_btn')">Search</span>
</button>
<span id="driver-image-spinner-{{ d.id }}" class="htmx-indicator" aria-busy="true"
x-data x-text="$store.i18n.t('searching')">Searching…</span>
</div>
</form>
<div id="driver-image-results-{{ d.id }}"></div>
</div>
{% endif %}
</fieldset>
</div>
<div class="dialog-foot">
<button type="button" class="btn ghost" x-data
onclick="document.getElementById('driver-modal-{{ d.id }}').close()"
x-text="$store.i18n.t('cancel')">Cancel</button>
<button type="submit" class="btn" form="driver-rename-{{ d.id }}" x-data
x-text="$store.i18n.t('save')">Save</button>
</div>
</article>
</dialog>