feat(ui): split setup and running views
Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled
Some checks failed
Desktop Release / Bootstrap updater signing key (push) Has been cancelled
Desktop Release / Windows x64 NSIS (push) Has been cancelled
Desktop Release / Linux x64 DEB + AppImage (push) Has been cancelled
Desktop Release / Publish public desktop release (push) Has been cancelled
This commit is contained in:
parent
cc7c5df011
commit
481b1c181c
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fieldlens/print",
|
||||
"version": "0.1.15",
|
||||
"version": "0.1.16",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fieldlens-print"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
description = "FieldLens print-stream barcode capture companion"
|
||||
authors = ["Arrowstack"]
|
||||
edition = "2021"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "FieldLens Print",
|
||||
"version": "0.1.15",
|
||||
"version": "0.1.16",
|
||||
"identifier": "dev.arrowstack.fieldlens.print",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
376
src/main.ts
376
src/main.ts
@ -85,7 +85,7 @@ let config: AppConfig
|
||||
let jobs: Job[] = []
|
||||
let status: Status
|
||||
let loginTimer: number | undefined
|
||||
let currentVersion = '0.1.15'
|
||||
let currentVersion = '0.1.16'
|
||||
let bundleType: BundleType | null = null
|
||||
let availableUpdate: Update | null = null
|
||||
let availableManualUpdate: ManualUpdate | null = null
|
||||
@ -93,6 +93,7 @@ let updateBusy = false
|
||||
let captureError: string | null = null
|
||||
let systemPrinters: SystemPrinter[] = []
|
||||
let virtualPrinterStatus: VirtualPrinterStatus | null = null
|
||||
let activeView: 'setup' | 'running' = 'setup'
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="shell">
|
||||
@ -104,155 +105,194 @@ app.innerHTML = `
|
||||
<p class="subtitle">Capture and inspect print jobs before the printer driver renders them.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="status-pill" class="status"><span class="dot"></span><span>Loading</span></div>
|
||||
|
||||
<div class="topbar-actions">
|
||||
<nav class="view-tabs" aria-label="FieldLens Print view">
|
||||
<button class="view-tab active" id="setup-tab" type="button">Setup</button>
|
||||
<button class="view-tab" id="running-tab" type="button">Running</button>
|
||||
</nav>
|
||||
<div id="status-pill" class="status"><span class="dot"></span><span>Loading</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="grid">
|
||||
<section class="card route-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<span class="eyebrow">PRINTER ROUTING</span>
|
||||
<h2>Destination</h2>
|
||||
</div>
|
||||
<button class="icon-btn" id="refresh-printers" title="Refresh printers" aria-label="Refresh printers">↻</button>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<label class="field-label">Physical printer
|
||||
<select id="forwardPrinter">
|
||||
<option value="">Choose a printer…</option>
|
||||
</select>
|
||||
<span id="forward-printer-help" class="help">Choose the printer that should receive production jobs.</span>
|
||||
</label>
|
||||
|
||||
<div class="route-tiles">
|
||||
<div class="route-tile">
|
||||
<div class="route-icon live-route">P</div>
|
||||
<main>
|
||||
<section id="setup-view" class="app-view">
|
||||
<div class="grid">
|
||||
<section class="card route-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<strong>FieldLens Print</strong>
|
||||
<span>Capture + physical print</span>
|
||||
<span class="eyebrow">PRINTER ROUTING</span>
|
||||
<h2>Destination</h2>
|
||||
</div>
|
||||
<button class="icon-btn" id="refresh-printers" title="Refresh printers" aria-label="Refresh printers">↻</button>
|
||||
</div>
|
||||
<div class="route-tile">
|
||||
<div class="route-icon test-route">T</div>
|
||||
<div>
|
||||
<strong>FieldLens Test Capture</strong>
|
||||
<span>Full pipeline, no paper</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="printer-state">
|
||||
<span class="state-dot"></span>
|
||||
<div>
|
||||
<strong>System printer</strong>
|
||||
<span id="virtual-printer-state">Checking FieldLens queues…</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<label class="field-label">Physical printer
|
||||
<select id="forwardPrinter">
|
||||
<option value="">Choose a printer…</option>
|
||||
</select>
|
||||
<span id="forward-printer-help" class="help">Choose the printer that should receive production jobs.</span>
|
||||
</label>
|
||||
|
||||
<div class="actions printer-actions">
|
||||
<button class="btn primary" id="install-printer">Create / repair printers</button>
|
||||
<button class="btn ghost" id="self-test">Run capture self-test</button>
|
||||
<button class="btn ghost danger" id="remove-printer">Remove</button>
|
||||
</div>
|
||||
|
||||
<details class="advanced">
|
||||
<summary>Advanced capture settings</summary>
|
||||
<div class="advanced-body">
|
||||
<div class="fields">
|
||||
<label>Capture mode
|
||||
<select id="mode">
|
||||
<option value="raw-proxy">RAW 9100 proxy</option>
|
||||
<option value="spool-folder">Spool folder</option>
|
||||
<option value="both">Both routes</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Listen address
|
||||
<input id="listenAddr" placeholder="127.0.0.1:9100" />
|
||||
</label>
|
||||
<label>RAW/TCP destination
|
||||
<input id="forwardAddr" placeholder="10.0.0.50:9100" />
|
||||
</label>
|
||||
<label>Spool capture folder
|
||||
<input id="spoolDir" />
|
||||
</label>
|
||||
<div class="route-tiles">
|
||||
<div class="route-tile">
|
||||
<div class="route-icon live-route">P</div>
|
||||
<div>
|
||||
<strong>FieldLens Print</strong>
|
||||
<span>Capture + physical print</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="route-tile">
|
||||
<div class="route-icon test-route">T</div>
|
||||
<div>
|
||||
<strong>FieldLens Test Capture</strong>
|
||||
<span>Full pipeline, no paper</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions compact-actions">
|
||||
<button class="btn ghost" id="save">Save settings</button>
|
||||
<button class="btn ghost" id="start">Start capture</button>
|
||||
<button class="btn ghost danger" id="stop">Stop capture</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card activity-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<span class="eyebrow">ACTIVITY</span>
|
||||
<h2>FieldLens</h2>
|
||||
</div>
|
||||
<span id="job-hint" class="muted">Waiting for capture</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="metrics">
|
||||
<div class="metric"><b id="job-count">0</b><span>Jobs</span></div>
|
||||
<div class="metric"><b id="code-count">0</b><span>Codes</span></div>
|
||||
<div class="metric"><b id="pending-count">0</b><span>Pending</span></div>
|
||||
</div>
|
||||
|
||||
<div class="account-row">
|
||||
<div>
|
||||
<strong>FieldLens account</strong>
|
||||
<span id="login-state" class="muted">Not signed in.</span>
|
||||
<div id="login-code" class="login-code"></div>
|
||||
</div>
|
||||
<div class="inline-actions">
|
||||
<button class="btn ghost" id="sync">Sync</button>
|
||||
<button class="btn blue" id="login">Sign in</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="advanced">
|
||||
<summary>Connection & updates</summary>
|
||||
<div class="advanced-body">
|
||||
<div class="fields">
|
||||
<label class="full">FieldLens API<input id="apiBaseUrl" /></label>
|
||||
<label class="full">Keycloak issuer<input id="issuer" /></label>
|
||||
<label>Desktop client<input id="clientId" /></label>
|
||||
<label>Station ID<input id="stationId" placeholder="optional" /></label>
|
||||
</div>
|
||||
|
||||
<div class="update-box">
|
||||
<div class="printer-state">
|
||||
<span class="state-dot"></span>
|
||||
<div>
|
||||
<strong>App updates</strong>
|
||||
<span id="version-label" class="muted">Version …</span>
|
||||
<small id="update-state" class="muted">Checking for updates shortly after startup.</small>
|
||||
<strong>System printer</strong>
|
||||
<span id="virtual-printer-state">Checking FieldLens queues…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions printer-actions">
|
||||
<button class="btn primary" id="install-printer">Create / repair printers</button>
|
||||
<button class="btn ghost" id="self-test">Run capture self-test</button>
|
||||
<button class="btn ghost danger" id="remove-printer">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card settings-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<span class="eyebrow">SETTINGS</span>
|
||||
<h2>FieldLens Print</h2>
|
||||
</div>
|
||||
<span id="settings-state" class="muted">Local configuration</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="account-row settings-account">
|
||||
<div>
|
||||
<strong>FieldLens account</strong>
|
||||
<span id="login-state" class="muted">Not signed in.</span>
|
||||
<div id="login-code" class="login-code"></div>
|
||||
</div>
|
||||
<div class="inline-actions">
|
||||
<button class="btn ghost" id="check-update">Check</button>
|
||||
<button class="btn blue" id="install-update" hidden>Install</button>
|
||||
<button class="btn ghost" id="sync">Sync</button>
|
||||
<button class="btn blue" id="login">Sign in</button>
|
||||
</div>
|
||||
<div id="update-progress" class="update-progress" hidden><span></span></div>
|
||||
</div>
|
||||
|
||||
<details class="advanced" open>
|
||||
<summary>Capture settings</summary>
|
||||
<div class="advanced-body">
|
||||
<div class="fields">
|
||||
<label>Capture mode
|
||||
<select id="mode">
|
||||
<option value="raw-proxy">RAW 9100 proxy</option>
|
||||
<option value="spool-folder">Spool folder</option>
|
||||
<option value="both">Both routes</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Listen address
|
||||
<input id="listenAddr" placeholder="127.0.0.1:9100" />
|
||||
</label>
|
||||
<label>RAW/TCP destination
|
||||
<input id="forwardAddr" placeholder="10.0.0.50:9100" />
|
||||
</label>
|
||||
<label>Spool capture folder
|
||||
<input id="spoolDir" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="actions compact-actions">
|
||||
<button class="btn ghost" id="save">Save settings</button>
|
||||
<button class="btn blue" id="start">Start capture</button>
|
||||
<button class="btn ghost danger" id="stop">Stop capture</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="advanced">
|
||||
<summary>Connection & updates</summary>
|
||||
<div class="advanced-body">
|
||||
<div class="fields">
|
||||
<label class="full">FieldLens API<input id="apiBaseUrl" /></label>
|
||||
<label class="full">Keycloak issuer<input id="issuer" /></label>
|
||||
<label>Desktop client<input id="clientId" /></label>
|
||||
<label>Station ID<input id="stationId" placeholder="optional" /></label>
|
||||
</div>
|
||||
|
||||
<div class="update-box">
|
||||
<div>
|
||||
<strong>App updates</strong>
|
||||
<span id="version-label" class="muted">Version …</span>
|
||||
<small id="update-state" class="muted">Checking for updates shortly after startup.</small>
|
||||
</div>
|
||||
<div class="inline-actions">
|
||||
<button class="btn ghost" id="check-update">Check</button>
|
||||
<button class="btn blue" id="install-update" hidden>Install</button>
|
||||
</div>
|
||||
<div id="update-progress" class="update-progress" hidden><span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card jobs-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<span class="eyebrow">RECENT ACTIVITY</span>
|
||||
<h2>Print jobs</h2>
|
||||
</div>
|
||||
<span class="muted">Newest first</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="jobs" class="jobs"></div>
|
||||
<section id="running-view" class="app-view" hidden>
|
||||
<div class="running-grid">
|
||||
<section class="card running-summary-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<span class="eyebrow">RUNNING</span>
|
||||
<h2>Capture activity</h2>
|
||||
</div>
|
||||
<span id="job-hint" class="muted">Waiting for capture</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="metrics">
|
||||
<div class="metric"><b id="job-count">0</b><span>Jobs</span></div>
|
||||
<div class="metric"><b id="code-count">0</b><span>Codes</span></div>
|
||||
<div class="metric"><b id="pending-count">0</b><span>Pending</span></div>
|
||||
</div>
|
||||
|
||||
<div class="running-route">
|
||||
<div>
|
||||
<span class="eyebrow">PHYSICAL DESTINATION</span>
|
||||
<strong id="running-printer">No printer selected</strong>
|
||||
</div>
|
||||
<div class="inline-actions">
|
||||
<button class="btn ghost" id="running-self-test">Test capture</button>
|
||||
<button class="btn ghost danger" id="running-stop">Stop capture</button>
|
||||
</div>
|
||||
</div>
|
||||
<span id="running-action-state" class="muted running-action-state"></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card jobs-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<span class="eyebrow">RECENT ACTIVITY</span>
|
||||
<h2>Print jobs</h2>
|
||||
</div>
|
||||
<span class="muted">Newest first</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="jobs" class="jobs"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@ -263,6 +303,14 @@ function el<T extends HTMLElement>(id: string) {
|
||||
return document.getElementById(id) as T
|
||||
}
|
||||
|
||||
function setView(view: 'setup' | 'running') {
|
||||
activeView = view
|
||||
el('setup-view').hidden = view !== 'setup'
|
||||
el('running-view').hidden = view !== 'running'
|
||||
el('setup-tab').classList.toggle('active', view === 'setup')
|
||||
el('running-tab').classList.toggle('active', view === 'running')
|
||||
}
|
||||
|
||||
function readForm(): AppConfig {
|
||||
return {
|
||||
...config,
|
||||
@ -304,6 +352,7 @@ function renderPrinters() {
|
||||
if (options.some((printer) => printer.name === current)) select.value = current
|
||||
|
||||
const selected = options.find((printer) => printer.name === select.value)
|
||||
el('running-printer').textContent = selected?.name || config?.forwardPrinter || 'No physical printer selected'
|
||||
const help = el('forward-printer-help')
|
||||
if (selected?.address) {
|
||||
help.textContent = `Detected RAW network endpoint: ${selected.address}`
|
||||
@ -459,22 +508,38 @@ el('install-printer').addEventListener('click', async () => {
|
||||
}
|
||||
|
||||
button.disabled = true
|
||||
state.textContent = 'Creating FieldLens Print… administrator approval may appear.'
|
||||
let stoppedForRepair = false
|
||||
|
||||
try {
|
||||
state.textContent = 'Stopping capture before printer repair…'
|
||||
await invoke('stop_capture')
|
||||
stoppedForRepair = true
|
||||
await refreshStatus()
|
||||
|
||||
state.textContent = 'Repairing printers and migrating captured jobs… administrator approval may appear.'
|
||||
virtualPrinterStatus = await invoke<VirtualPrinterStatus>('install_virtual_printer', { targetPrinter })
|
||||
config = await invoke<AppConfig>('save_config', { config: readForm() })
|
||||
await restartCapture()
|
||||
|
||||
state.textContent = 'Restarting capture and processing migrated backlog…'
|
||||
const restarted = await ensureCaptureStarted()
|
||||
if (!restarted) {
|
||||
throw new Error(captureError || 'capture did not restart')
|
||||
}
|
||||
stoppedForRepair = false
|
||||
|
||||
await refreshPrinters()
|
||||
setView('running')
|
||||
} catch (error) {
|
||||
state.textContent = `Printer setup failed: ${String(error)}`
|
||||
if (stoppedForRepair) {
|
||||
await ensureCaptureStarted().catch(() => undefined)
|
||||
}
|
||||
} finally {
|
||||
button.disabled = false
|
||||
}
|
||||
})
|
||||
|
||||
el('self-test').addEventListener('click', async () => {
|
||||
const button = el<HTMLButtonElement>('self-test')
|
||||
const state = el('virtual-printer-state')
|
||||
async function submitCaptureSelfTest(button: HTMLButtonElement, state: HTMLElement) {
|
||||
button.disabled = true
|
||||
state.textContent = 'Submitting a no-paper capture self-test…'
|
||||
try {
|
||||
@ -486,6 +551,20 @@ el('self-test').addEventListener('click', async () => {
|
||||
} finally {
|
||||
button.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
el('self-test').addEventListener('click', () => {
|
||||
void submitCaptureSelfTest(
|
||||
el<HTMLButtonElement>('self-test'),
|
||||
el('virtual-printer-state'),
|
||||
)
|
||||
})
|
||||
|
||||
el('running-self-test').addEventListener('click', () => {
|
||||
void submitCaptureSelfTest(
|
||||
el<HTMLButtonElement>('running-self-test'),
|
||||
el('running-action-state'),
|
||||
)
|
||||
})
|
||||
|
||||
el('remove-printer').addEventListener('click', async () => {
|
||||
@ -512,7 +591,8 @@ el('save').addEventListener('click', async () => {
|
||||
|
||||
el('start').addEventListener('click', async () => {
|
||||
config = await invoke<AppConfig>('save_config', { config: readForm() })
|
||||
await restartCapture()
|
||||
const started = await restartCapture()
|
||||
if (started) setView('running')
|
||||
})
|
||||
|
||||
el('stop').addEventListener('click', async () => {
|
||||
@ -521,6 +601,18 @@ el('stop').addEventListener('click', async () => {
|
||||
await refreshStatus()
|
||||
})
|
||||
|
||||
el('setup-tab').addEventListener('click', () => setView('setup'))
|
||||
el('running-tab').addEventListener('click', () => setView('running'))
|
||||
|
||||
el('running-stop').addEventListener('click', async () => {
|
||||
const state = el('running-action-state')
|
||||
state.textContent = 'Stopping capture…'
|
||||
captureError = null
|
||||
await invoke('stop_capture')
|
||||
await refreshStatus()
|
||||
state.textContent = 'Capture stopped. Open Setup to restart or change configuration.'
|
||||
})
|
||||
|
||||
el('sync').addEventListener('click', async () => {
|
||||
const result = await invoke<{ synced: number; pending: number }>('sync_pending')
|
||||
el('login-state').textContent = `Synced ${result.synced}. ${result.pending} pending.`
|
||||
@ -710,6 +802,14 @@ writeForm(config)
|
||||
await refreshPrinters()
|
||||
await ensureCaptureStarted()
|
||||
|
||||
const readyForRunning = Boolean(
|
||||
virtualPrinterStatus?.installed
|
||||
&& config.forwardPrinter
|
||||
&& status?.running
|
||||
&& !captureError
|
||||
)
|
||||
setView(readyForRunning ? 'running' : 'setup')
|
||||
|
||||
currentVersion = await getVersion()
|
||||
bundleType = await getBundleType().catch(() => null)
|
||||
el('version-label').textContent = `Version ${currentVersion}${bundleType ? ` · ${bundleType}` : ''}`
|
||||
|
||||
@ -543,3 +543,101 @@ select:focus {
|
||||
color: #9fcef5;
|
||||
border-color: #294865;
|
||||
}
|
||||
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.view-tabs {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid #202a36;
|
||||
border-radius: 10px;
|
||||
background: #0d131a;
|
||||
}
|
||||
|
||||
.view-tab {
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
padding: 7px 10px;
|
||||
background: transparent;
|
||||
color: #718094;
|
||||
font-size: 10.5px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.view-tab:hover {
|
||||
color: #dce4ed;
|
||||
background: #151d27;
|
||||
}
|
||||
|
||||
.view-tab.active {
|
||||
background: #1a2b24;
|
||||
color: #b9f3d5;
|
||||
}
|
||||
|
||||
.app-view[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-card .settings-account {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.running-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.running-summary-card .card-body {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.running-route {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
border: 1px solid #222c38;
|
||||
border-radius: 11px;
|
||||
padding: 12px;
|
||||
background: #0d131a;
|
||||
}
|
||||
|
||||
.running-route > div:first-child {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.running-route strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.running-action-state {
|
||||
min-height: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.topbar-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.running-route {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user