Bump version to 0.1.13 in API and package.json
This commit is contained in:
@@ -72,7 +72,7 @@ async def lifespan(app: FastAPI): # noqa: ARG001
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Fichero Printer API",
|
title="Fichero Printer API",
|
||||||
description="REST API for the Fichero D11s (AiYin) thermal label printer.",
|
description="REST API for the Fichero D11s (AiYin) thermal label printer.",
|
||||||
version="0.1.9",
|
version="0.1.13",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
docs_url=None,
|
docs_url=None,
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ async def lifespan(app: FastAPI): # noqa: ARG001
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Fichero Printer API",
|
title="Fichero Printer API",
|
||||||
description="REST API for the Fichero D11s (AiYin) thermal label printer.",
|
description="REST API for the Fichero D11s (AiYin) thermal label printer.",
|
||||||
version="0.1.9",
|
version="0.1.13",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
docs_url=None,
|
docs_url=None,
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
@@ -96,139 +96,270 @@ def _ui_html() -> str:
|
|||||||
default_address = _DEFAULT_ADDRESS or ""
|
default_address = _DEFAULT_ADDRESS or ""
|
||||||
default_transport = "classic" if _DEFAULT_CLASSIC else "ble"
|
default_transport = "classic" if _DEFAULT_CLASSIC else "ble"
|
||||||
return f"""<!doctype html>
|
return f"""<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Fichero Printer</title>
|
<title>Fichero Printer</title>
|
||||||
<style>
|
<style>
|
||||||
|
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
|
||||||
:root {{
|
:root {{
|
||||||
--bg: #f4efe6;
|
--s0: #161819;
|
||||||
--panel: #fffaf2;
|
--s1: #1c1e20;
|
||||||
--line: #d8cdbd;
|
--s2: #232628;
|
||||||
--ink: #2d241d;
|
--s3: #2a2d30;
|
||||||
--muted: #6c6258;
|
--ink: #e4e7eb;
|
||||||
--accent: #b55e33;
|
--muted: #949ba4;
|
||||||
--accent-2: #245b4b;
|
--dim: #5d6670;
|
||||||
|
--brand: #07addf;
|
||||||
|
--brand-h: #0699c7;
|
||||||
|
--brand-glow: rgba(7,173,223,.18);
|
||||||
|
--ok: #6bb88a;
|
||||||
|
--warn: #d4a24c;
|
||||||
|
--err: #d45454;
|
||||||
|
--border: rgba(228,231,235,.08);
|
||||||
|
--border-e: rgba(228,231,235,.14);
|
||||||
|
--ctrl-bg: #141617;
|
||||||
|
--ctrl-border: rgba(228,231,235,.10);
|
||||||
|
--r: 10px;
|
||||||
|
--r-lg: 14px;
|
||||||
}}
|
}}
|
||||||
* {{ box-sizing: border-box; }}
|
|
||||||
|
html {{ height: 100%; }}
|
||||||
body {{
|
body {{
|
||||||
margin: 0;
|
min-height: 100dvh;
|
||||||
font-family: "Noto Sans", system-ui, sans-serif;
|
font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background:
|
background-color: var(--s0);
|
||||||
radial-gradient(circle at top left, #fff8ed 0, transparent 35%),
|
background-image:
|
||||||
linear-gradient(180deg, #efe4d3 0%, var(--bg) 100%);
|
radial-gradient(ellipse at 10% 90%, rgba(7,173,223,.07) 0%, transparent 55%),
|
||||||
|
radial-gradient(ellipse at 90% 5%, rgba(7,173,223,.04) 0%, transparent 55%);
|
||||||
|
background-attachment: fixed;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
/* ── Header ─────────────────────────────────────── */
|
||||||
|
header {{
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 10px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(22,24,25,.82);
|
||||||
|
backdrop-filter: blur(16px) saturate(1.5);
|
||||||
|
-webkit-backdrop-filter: blur(16px) saturate(1.5);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}}
|
||||||
|
.brand {{ font-size: 1.05rem; font-weight: 700; letter-spacing: -.025em; }}
|
||||||
|
.brand em {{ color: var(--brand); font-style: normal; }}
|
||||||
|
.header-right {{ margin-left: auto; display: flex; align-items: center; gap: 8px; }}
|
||||||
|
.status-pill {{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: .78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--s2);
|
||||||
|
border: 1px solid var(--border-e);
|
||||||
|
color: var(--muted);
|
||||||
|
transition: .25s;
|
||||||
|
cursor: default;
|
||||||
|
white-space: nowrap;
|
||||||
|
}}
|
||||||
|
.status-pill .dot {{
|
||||||
|
width: 7px; height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--dim);
|
||||||
|
transition: .25s;
|
||||||
|
}}
|
||||||
|
.status-pill.ok {{ color: var(--ok); border-color: rgba(107,184,138,.3); }}
|
||||||
|
.status-pill.ok .dot {{ background: var(--ok); box-shadow: 0 0 6px var(--ok); }}
|
||||||
|
.status-pill.err {{ color: var(--err); border-color: rgba(212,84,84,.3); }}
|
||||||
|
.status-pill.err .dot {{ background: var(--err); box-shadow: 0 0 6px var(--err); }}
|
||||||
|
|
||||||
|
/* ── Layout ─────────────────────────────────────── */
|
||||||
main {{
|
main {{
|
||||||
max-width: 980px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 24px 16px 40px;
|
padding: 24px 16px 60px;
|
||||||
}}
|
|
||||||
.hero {{
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 24px;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 18px;
|
|
||||||
background: rgba(255, 250, 242, 0.92);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}}
|
|
||||||
h1, h2 {{ margin: 0 0 12px; }}
|
|
||||||
.muted {{ color: var(--muted); }}
|
|
||||||
.grid {{
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
|
||||||
}}
|
}}
|
||||||
|
main > .full {{ grid-column: 1 / -1; }}
|
||||||
|
|
||||||
|
/* ── Cards ──────────────────────────────────────── */
|
||||||
.card {{
|
.card {{
|
||||||
padding: 18px;
|
padding: 20px;
|
||||||
border: 1px solid var(--line);
|
background: var(--s1);
|
||||||
border-radius: 16px;
|
border: 1px solid var(--border);
|
||||||
background: var(--panel);
|
border-radius: var(--r-lg);
|
||||||
box-shadow: 0 8px 24px rgba(45, 36, 29, 0.06);
|
box-shadow: 0 8px 32px rgba(0,0,0,.3);
|
||||||
}}
|
}}
|
||||||
|
.card-title {{
|
||||||
|
font-size: .7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--dim);
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* ── Form ───────────────────────────────────────── */
|
||||||
label {{
|
label {{
|
||||||
display: block;
|
display: block;
|
||||||
margin: 10px 0 6px;
|
font-size: .8rem;
|
||||||
font-size: 0.92rem;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 12px 0 5px;
|
||||||
}}
|
}}
|
||||||
input, select, textarea, button {{
|
label:first-child {{ margin-top: 0; }}
|
||||||
|
input, select, textarea {{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 10px;
|
padding: 8px 11px;
|
||||||
border: 1px solid var(--line);
|
border-radius: var(--r);
|
||||||
padding: 10px 12px;
|
border: 1px solid var(--ctrl-border);
|
||||||
|
background: var(--ctrl-bg);
|
||||||
|
color: var(--ink);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
transition: border-color .15s, box-shadow .15s;
|
||||||
|
outline: none;
|
||||||
}}
|
}}
|
||||||
textarea {{ min-height: 110px; resize: vertical; }}
|
input:focus, select:focus, textarea:focus {{
|
||||||
.row {{
|
border-color: var(--brand);
|
||||||
display: grid;
|
box-shadow: 0 0 0 3px rgba(7,173,223,.2);
|
||||||
gap: 12px;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}}
|
}}
|
||||||
.inline {{
|
textarea {{ min-height: 90px; resize: vertical; }}
|
||||||
display: flex;
|
.two-col {{ display: grid; gap: 10px; grid-template-columns: 1fr 1fr; }}
|
||||||
gap: 10px;
|
.check-row {{ display: flex; align-items: center; gap: 8px; margin-top: 14px; }}
|
||||||
|
.check-row input[type=checkbox] {{ width: 15px; height: 15px; accent-color: var(--brand); }}
|
||||||
|
.check-row label {{ margin: 0; font-size: .85rem; color: var(--ink); }}
|
||||||
|
|
||||||
|
/* ── Buttons ────────────────────────────────────── */
|
||||||
|
.btn {{
|
||||||
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 12px;
|
justify-content: center;
|
||||||
}}
|
gap: 6px;
|
||||||
.inline input[type="checkbox"] {{ width: auto; }}
|
padding: 8px 16px;
|
||||||
button {{
|
border-radius: var(--r);
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
background: var(--accent);
|
|
||||||
border: none;
|
border: none;
|
||||||
|
font: inherit;
|
||||||
|
font-size: .85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .15s, box-shadow .15s, opacity .15s;
|
||||||
}}
|
}}
|
||||||
button.alt {{ background: var(--accent-2); }}
|
.btn:disabled {{ opacity: .5; cursor: not-allowed; }}
|
||||||
|
.btn-primary {{
|
||||||
|
background: var(--brand);
|
||||||
|
color: #fff;
|
||||||
|
}}
|
||||||
|
.btn-primary:not(:disabled):hover {{
|
||||||
|
background: var(--brand-h);
|
||||||
|
box-shadow: 0 0 0 3px var(--brand-glow);
|
||||||
|
}}
|
||||||
|
.btn-secondary {{
|
||||||
|
background: var(--s3);
|
||||||
|
color: var(--ink);
|
||||||
|
border: 1px solid var(--border-e);
|
||||||
|
}}
|
||||||
|
.btn-secondary:not(:disabled):hover {{
|
||||||
|
background: var(--s3);
|
||||||
|
border-color: var(--brand);
|
||||||
|
}}
|
||||||
|
.btn-group {{ display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }}
|
||||||
|
.btn-group .btn {{ flex: 1; min-width: 110px; }}
|
||||||
|
|
||||||
|
/* ── Address row ────────────────────────────────── */
|
||||||
|
.addr-row {{ display: flex; gap: 8px; }}
|
||||||
|
.addr-row input {{ flex: 1; min-width: 0; }}
|
||||||
|
.addr-row .btn {{ padding: 8px 12px; white-space: nowrap; flex-shrink: 0; }}
|
||||||
|
|
||||||
|
/* ── Output / pre ───────────────────────────────── */
|
||||||
pre {{
|
pre {{
|
||||||
|
background: var(--s0);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r);
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace;
|
||||||
|
font-size: .78rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 14px;
|
||||||
|
min-height: 130px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
margin: 0;
|
white-space: pre-wrap;
|
||||||
padding: 12px;
|
word-break: break-all;
|
||||||
border-radius: 12px;
|
|
||||||
background: #241f1a;
|
|
||||||
color: #f7efe4;
|
|
||||||
min-height: 140px;
|
|
||||||
}}
|
}}
|
||||||
.actions {{
|
pre.ok {{ color: var(--ok); border-color: rgba(107,184,138,.2); }}
|
||||||
display: flex;
|
pre.err {{ color: var(--err); border-color: rgba(212,84,84,.2); }}
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
/* ── Spinner ────────────────────────────────────── */
|
||||||
margin-top: 12px;
|
@keyframes spin {{ to {{ transform: rotate(360deg); }} }}
|
||||||
|
.spinner {{
|
||||||
|
display: inline-block;
|
||||||
|
width: 13px; height: 13px;
|
||||||
|
border: 2px solid rgba(255,255,255,.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin .65s linear infinite;
|
||||||
}}
|
}}
|
||||||
.actions button {{
|
|
||||||
width: auto;
|
/* ── Footer ─────────────────────────────────────── */
|
||||||
min-width: 140px;
|
footer {{
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: .72rem;
|
||||||
|
color: var(--dim);
|
||||||
}}
|
}}
|
||||||
@media (max-width: 640px) {{
|
footer a {{ color: var(--dim); text-decoration: none; }}
|
||||||
.row {{ grid-template-columns: 1fr; }}
|
footer a:hover {{ color: var(--brand); }}
|
||||||
.actions button {{ width: 100%; }}
|
|
||||||
|
@media (max-width: 520px) {{
|
||||||
|
.two-col {{ grid-template-columns: 1fr; }}
|
||||||
|
.btn-group .btn {{ min-width: 0; }}
|
||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
|
||||||
<section class="hero">
|
|
||||||
<h1>Fichero Printer</h1>
|
|
||||||
<p class="muted">Home Assistant print console for status, text labels, and image uploads.</p>
|
|
||||||
<p class="muted">API docs remain available at <a href="docs">/docs</a>.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="grid">
|
<header>
|
||||||
|
<span class="brand">Fichero<em>Printer</em></span>
|
||||||
|
<div class="header-right">
|
||||||
|
<span class="status-pill" id="status-pill">
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span id="status-text">Unknown</span>
|
||||||
|
</span>
|
||||||
|
<button class="btn btn-secondary" style="padding:6px 10px;font-size:.78rem" onclick="refreshStatus()">↻</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<!-- Connection card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Connection</h2>
|
<p class="card-title">Connection</p>
|
||||||
<label for="address">Printer address</label>
|
|
||||||
<div style="display:flex;gap:8px;align-items:center">
|
<label for="address">Bluetooth address</label>
|
||||||
<input id="address" value="{default_address}" placeholder="C9:48:8A:69:D5:C0" style="flex:1;width:auto">
|
<div class="addr-row">
|
||||||
<button type="button" id="scan-btn" class="alt" style="width:auto;white-space:nowrap" onclick="scanAddress()">🔌 Scan</button>
|
<input id="address" value="{default_address}" placeholder="AA:BB:CC:DD:EE:FF">
|
||||||
|
<button class="btn btn-secondary" id="scan-btn" onclick="scanAddress()">
|
||||||
|
<span id="scan-icon">⟳</span> Scan
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="two-col">
|
||||||
<div>
|
<div>
|
||||||
<label for="transport">Transport</label>
|
<label for="transport">Transport</label>
|
||||||
<select id="transport">
|
<select id="transport">
|
||||||
<option value="ble"{" selected" if default_transport == "ble" else ""}>BLE</option>
|
<option value="ble"{" selected" if default_transport == "ble" else ""}>BLE</option>
|
||||||
<option value="classic"{" selected" if default_transport == "classic" else ""}>Classic</option>
|
<option value="classic"{" selected" if default_transport == "classic" else ""}>Classic BT</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -237,28 +368,32 @@ def _ui_html() -> str:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="btn-group">
|
||||||
<button type="button" class="alt" onclick="runGet('status')">Get Status</button>
|
<button class="btn btn-secondary" onclick="runGet('status')">Status</button>
|
||||||
<button type="button" class="alt" onclick="runGet('info')">Get Info</button>
|
<button class="btn btn-secondary" onclick="runGet('info')">Device info</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Output card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Output</h2>
|
<p class="card-title">Response</p>
|
||||||
<pre id="output">Ready.</pre>
|
<pre id="output">Waiting for a command…</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Print text card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Print Text</h2>
|
<p class="card-title">Print text</p>
|
||||||
|
|
||||||
<label for="text">Text</label>
|
<label for="text">Text</label>
|
||||||
<textarea id="text" placeholder="Hello from Home Assistant"></textarea>
|
<textarea id="text" placeholder="Hello from Home Assistant"></textarea>
|
||||||
<div class="row">
|
|
||||||
|
<div class="two-col">
|
||||||
<div>
|
<div>
|
||||||
<label for="text_density">Density</label>
|
<label for="text_density">Density</label>
|
||||||
<select id="text_density">
|
<select id="text_density">
|
||||||
<option value="0">0</option>
|
<option value="0">0 – Light</option>
|
||||||
<option value="1">1</option>
|
<option value="1">1 – Medium</option>
|
||||||
<option value="2" selected>2</option>
|
<option value="2" selected>2 – Dark</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -266,9 +401,9 @@ def _ui_html() -> str:
|
|||||||
<input id="text_copies" type="number" min="1" max="99" value="1">
|
<input id="text_copies" type="number" min="1" max="99" value="1">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="two-col">
|
||||||
<div>
|
<div>
|
||||||
<label for="text_font_size">Font size</label>
|
<label for="text_font_size">Font size (pt)</label>
|
||||||
<input id="text_font_size" type="number" min="6" max="200" value="30">
|
<input id="text_font_size" type="number" min="6" max="200" value="30">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -276,28 +411,33 @@ def _ui_html() -> str:
|
|||||||
<input id="text_label_length" type="number" min="5" max="500" value="30">
|
<input id="text_label_length" type="number" min="5" max="500" value="30">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label for="text_paper">Paper</label>
|
|
||||||
|
<label for="text_paper">Paper type</label>
|
||||||
<select id="text_paper">
|
<select id="text_paper">
|
||||||
<option value="gap" selected>gap</option>
|
<option value="gap" selected>Gap / label</option>
|
||||||
<option value="black">black</option>
|
<option value="black">Black mark</option>
|
||||||
<option value="continuous">continuous</option>
|
<option value="continuous">Continuous</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="actions">
|
|
||||||
<button type="button" onclick="printText()">Print Text</button>
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-primary" onclick="printText()">🖨 Print text</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Print image card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Print Image</h2>
|
<p class="card-title">Print image</p>
|
||||||
<label for="image_file">Image file</label>
|
|
||||||
|
<label for="image_file">Image file (PNG / JPEG / BMP / WEBP)</label>
|
||||||
<input id="image_file" type="file" accept="image/*">
|
<input id="image_file" type="file" accept="image/*">
|
||||||
<div class="row">
|
|
||||||
|
<div class="two-col">
|
||||||
<div>
|
<div>
|
||||||
<label for="image_density">Density</label>
|
<label for="image_density">Density</label>
|
||||||
<select id="image_density">
|
<select id="image_density">
|
||||||
<option value="0">0</option>
|
<option value="0">0 – Light</option>
|
||||||
<option value="1">1</option>
|
<option value="1">1 – Medium</option>
|
||||||
<option value="2" selected>2</option>
|
<option value="2" selected>2 – Dark</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -305,80 +445,112 @@ def _ui_html() -> str:
|
|||||||
<input id="image_copies" type="number" min="1" max="99" value="1">
|
<input id="image_copies" type="number" min="1" max="99" value="1">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="two-col">
|
||||||
<div>
|
<div>
|
||||||
<label for="image_label_length">Label length (mm)</label>
|
<label for="image_label_length">Label length (mm)</label>
|
||||||
<input id="image_label_length" type="number" min="5" max="500" value="30">
|
<input id="image_label_length" type="number" min="5" max="500" value="30">
|
||||||
</div>
|
</div>
|
||||||
<div class="inline">
|
<div>
|
||||||
<input id="image_dither" type="checkbox" checked>
|
<label for="image_paper">Paper type</label>
|
||||||
<label for="image_dither">Enable dithering</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<label for="image_paper">Paper</label>
|
|
||||||
<select id="image_paper">
|
<select id="image_paper">
|
||||||
<option value="gap" selected>gap</option>
|
<option value="gap" selected>Gap / label</option>
|
||||||
<option value="black">black</option>
|
<option value="black">Black mark</option>
|
||||||
<option value="continuous">continuous</option>
|
<option value="continuous">Continuous</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="actions">
|
|
||||||
<button type="button" onclick="printImage()">Print Image</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
<div class="check-row">
|
||||||
|
<input id="image_dither" type="checkbox" checked>
|
||||||
|
<label for="image_dither">Floyd-Steinberg dithering</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-primary" onclick="printImage()">🖨 Print image</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Fichero Printer REST API ·
|
||||||
|
<a href="docs">Swagger UI</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── Helpers ──────────────────────────────────────────
|
||||||
function commonParams() {{
|
function commonParams() {{
|
||||||
const address = document.getElementById("address").value.trim();
|
const address = document.getElementById("address").value.trim();
|
||||||
const classic = document.getElementById("transport").value === "classic";
|
const classic = document.getElementById("transport").value === "classic";
|
||||||
const channel = document.getElementById("channel").value;
|
const channel = document.getElementById("channel").value;
|
||||||
const params = new URLSearchParams();
|
const p = new URLSearchParams();
|
||||||
if (address) params.set("address", address);
|
if (address) p.set("address", address);
|
||||||
params.set("classic", String(classic));
|
p.set("classic", String(classic));
|
||||||
params.set("channel", channel);
|
p.set("channel", channel);
|
||||||
return params;
|
return p;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function setOutput(obj, isOk) {{
|
||||||
|
const pre = document.getElementById("output");
|
||||||
|
pre.textContent = JSON.stringify(obj, null, 2);
|
||||||
|
pre.className = isOk ? "ok" : "err";
|
||||||
}}
|
}}
|
||||||
|
|
||||||
async function showResponse(response) {{
|
async function showResponse(response) {{
|
||||||
const output = document.getElementById("output");
|
|
||||||
let data;
|
let data;
|
||||||
try {{
|
try {{ data = await response.json(); }} catch {{ data = {{ detail: await response.text() }}; }}
|
||||||
data = await response.json();
|
setOutput({{ status: response.status, ok: response.ok, data }}, response.ok);
|
||||||
}} catch {{
|
}}
|
||||||
data = {{ detail: await response.text() }};
|
|
||||||
|
// ── Status pill ──────────────────────────────────────
|
||||||
|
async function refreshStatus() {{
|
||||||
|
try {{
|
||||||
|
const r = await fetch("status?" + commonParams().toString());
|
||||||
|
const d = await r.json();
|
||||||
|
const pill = document.getElementById("status-pill");
|
||||||
|
const txt = document.getElementById("status-text");
|
||||||
|
if (r.ok && d.ok) {{
|
||||||
|
pill.className = "status-pill ok";
|
||||||
|
txt.textContent = d.no_paper ? "No paper" : d.charging ? "Charging" : "Ready";
|
||||||
|
}} else {{
|
||||||
|
pill.className = "status-pill err";
|
||||||
|
txt.textContent = r.ok ? "Not ready" : "Not found";
|
||||||
|
}}
|
||||||
|
}} catch {{ /* ignore */ }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// auto-refresh status on load
|
||||||
|
window.addEventListener("DOMContentLoaded", refreshStatus);
|
||||||
|
|
||||||
|
// ── Scan ─────────────────────────────────────────────
|
||||||
|
async function scanAddress() {{
|
||||||
|
const btn = document.getElementById("scan-btn");
|
||||||
|
const icon = document.getElementById("scan-icon");
|
||||||
|
btn.disabled = true;
|
||||||
|
icon.outerHTML = '<span id="scan-icon" class="spinner"></span>';
|
||||||
|
setOutput({{ message: "Scanning for printer (up to 8 s)…" }}, true);
|
||||||
|
try {{
|
||||||
|
const r = await fetch("scan");
|
||||||
|
const d = await r.json();
|
||||||
|
if (r.ok && d.address) {{
|
||||||
|
document.getElementById("address").value = d.address;
|
||||||
|
}}
|
||||||
|
setOutput({{ status: r.status, ok: r.ok, data: d }}, r.ok);
|
||||||
|
}} catch (e) {{
|
||||||
|
setOutput({{ error: String(e) }}, false);
|
||||||
|
}} finally {{
|
||||||
|
btn.disabled = false;
|
||||||
|
document.getElementById("scan-icon").outerHTML = '<span id="scan-icon">⟳</span>';
|
||||||
}}
|
}}
|
||||||
output.textContent = JSON.stringify({{ status: response.status, ok: response.ok, data }}, null, 2);
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
async function runGet(path) {{
|
async function runGet(path) {{
|
||||||
const response = await fetch(`${{path}}?${{commonParams().toString()}}`);
|
setOutput({{ message: "Loading…" }}, true);
|
||||||
await showResponse(response);
|
const r = await fetch(path + "?" + commonParams().toString());
|
||||||
}}
|
await showResponse(r);
|
||||||
|
|
||||||
async function scanAddress() {{
|
|
||||||
const btn = document.getElementById("scan-btn");
|
|
||||||
const output = document.getElementById("output");
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = "Scanning…";
|
|
||||||
output.textContent = "Scanning for printer (up to 8 s)…";
|
|
||||||
try {{
|
|
||||||
const response = await fetch("scan");
|
|
||||||
const data = await response.json();
|
|
||||||
if (response.ok && data.address) {{
|
|
||||||
document.getElementById("address").value = data.address;
|
|
||||||
output.textContent = JSON.stringify({{ status: response.status, ok: true, data }}, null, 2);
|
|
||||||
}} else {{
|
|
||||||
output.textContent = JSON.stringify({{ status: response.status, ok: false, data }}, null, 2);
|
|
||||||
}}
|
|
||||||
}} catch (e) {{
|
|
||||||
output.textContent = "Scan failed: " + e;
|
|
||||||
}} finally {{
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = "🔌 Scan";
|
|
||||||
}}
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// ── Print text ───────────────────────────────────────
|
||||||
async function printText() {{
|
async function printText() {{
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.set("text", document.getElementById("text").value);
|
form.set("text", document.getElementById("text").value);
|
||||||
@@ -390,18 +562,18 @@ def _ui_html() -> str:
|
|||||||
form.set("address", document.getElementById("address").value.trim());
|
form.set("address", document.getElementById("address").value.trim());
|
||||||
form.set("classic", String(document.getElementById("transport").value === "classic"));
|
form.set("classic", String(document.getElementById("transport").value === "classic"));
|
||||||
form.set("channel", document.getElementById("channel").value);
|
form.set("channel", document.getElementById("channel").value);
|
||||||
const response = await fetch("print/text", {{ method: "POST", body: form }});
|
setOutput({{ message: "Sending to printer…" }}, true);
|
||||||
await showResponse(response);
|
const r = await fetch("print/text", {{ method: "POST", body: form }});
|
||||||
|
await showResponse(r);
|
||||||
|
if (r.ok) refreshStatus();
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// ── Print image ──────────────────────────────────────
|
||||||
async function printImage() {{
|
async function printImage() {{
|
||||||
const fileInput = document.getElementById("image_file");
|
const fi = document.getElementById("image_file");
|
||||||
if (!fileInput.files.length) {{
|
if (!fi.files.length) {{ setOutput({{ error: "Select an image file first." }}, false); return; }}
|
||||||
document.getElementById("output").textContent = "Select an image file first.";
|
|
||||||
return;
|
|
||||||
}}
|
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.set("file", fileInput.files[0]);
|
form.set("file", fi.files[0]);
|
||||||
form.set("density", document.getElementById("image_density").value);
|
form.set("density", document.getElementById("image_density").value);
|
||||||
form.set("copies", document.getElementById("image_copies").value);
|
form.set("copies", document.getElementById("image_copies").value);
|
||||||
form.set("label_length", document.getElementById("image_label_length").value);
|
form.set("label_length", document.getElementById("image_label_length").value);
|
||||||
@@ -410,10 +582,12 @@ def _ui_html() -> str:
|
|||||||
form.set("address", document.getElementById("address").value.trim());
|
form.set("address", document.getElementById("address").value.trim());
|
||||||
form.set("classic", String(document.getElementById("transport").value === "classic"));
|
form.set("classic", String(document.getElementById("transport").value === "classic"));
|
||||||
form.set("channel", document.getElementById("channel").value);
|
form.set("channel", document.getElementById("channel").value);
|
||||||
const response = await fetch("print/image", {{ method: "POST", body: form }});
|
setOutput({{ message: "Sending to printer…" }}, true);
|
||||||
await showResponse(response);
|
const r = await fetch("print/image", {{ method: "POST", body: form }});
|
||||||
|
await showResponse(r);
|
||||||
|
if (r.ok) refreshStatus();
|
||||||
}}
|
}}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "fichero-web",
|
"name": "fichero-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.9",
|
"version": "0.1.13",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|||||||
Reference in New Issue
Block a user