370 lines
10 KiB
HTML
370 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Printio</title>
|
|
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico" />
|
|
<link rel="stylesheet" href="/static/css/styles.css" />
|
|
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css" />
|
|
<style>
|
|
:root{
|
|
--brand: #1e40af; /* blue-800 */
|
|
--brand-hover: #15327f;
|
|
--danger: #ef4444; /* red-500 */
|
|
--ok: #16a34a; /* green-600 */
|
|
--ok-hover: #118039;
|
|
--border: #d1d5db; /* gray-300 */
|
|
--text: #111827; /* gray-900 */
|
|
--muted: #4b5563; /* gray-600 */
|
|
--bg: #ffffff;
|
|
--bg-alt: #f9fafb; /* gray-50 */
|
|
--focus: #2563eb; /* blue-600 */
|
|
}
|
|
@media (prefers-color-scheme: dark){
|
|
:root{
|
|
--brand: #60a5fa;
|
|
--brand-hover: #3b82f6;
|
|
--danger: #f87171;
|
|
--ok: #34d399;
|
|
--ok-hover: #10b981;
|
|
--border: #374151;
|
|
--text: #e5e7eb;
|
|
--muted: #9ca3af;
|
|
--bg: #0b0f14;
|
|
--bg-alt: #111827;
|
|
--focus: #60a5fa;
|
|
}
|
|
}
|
|
|
|
*{ box-sizing: border-box; }
|
|
html, body{ height: 100%; }
|
|
body{
|
|
margin: 0;
|
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
line-height: 1.55;
|
|
}
|
|
|
|
#navbar{ position: sticky; top: 0; z-index: 20; }
|
|
|
|
h1, h2{ margin: 0 0 12px; }
|
|
h1{ font-size: clamp(1.5rem, 2vw, 2rem); }
|
|
h2{ font-size: clamp(1.1rem, 1.6vw, 1.4rem); color: var(--muted); }
|
|
|
|
.content-wrapper{
|
|
max-width: 1100px;
|
|
margin: 0 auto;
|
|
padding: 16px;
|
|
}
|
|
@media (min-width: 980px){
|
|
.content-wrapper{ padding: 24px; }
|
|
}
|
|
|
|
.card{
|
|
padding: 16px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
background: var(--bg-alt);
|
|
}
|
|
|
|
.header-img{
|
|
margin: 24px 0;
|
|
width: auto;
|
|
height: 100px;
|
|
}
|
|
|
|
/* Switch row */
|
|
.switch-row{
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
margin: 12px 0 6px;
|
|
}
|
|
.switch-label{ font-weight: 600; color: var(--text); }
|
|
.switch-input{ transform: scale(1.1); }
|
|
|
|
.error{
|
|
color: var(--danger);
|
|
text-align: center;
|
|
margin: 8px 0 0;
|
|
display: none;
|
|
}
|
|
|
|
/* Actions */
|
|
button{
|
|
appearance: none;
|
|
border: none;
|
|
border-radius: 8px;
|
|
padding: 8px 14px;
|
|
font-size: 0.98rem;
|
|
cursor: pointer;
|
|
color: #fff;
|
|
background: var(--brand);
|
|
}
|
|
button:hover{ background: var(--brand-hover); }
|
|
button:focus-visible{
|
|
outline: 3px solid var(--focus);
|
|
outline-offset: 2px;
|
|
}
|
|
.btn-danger{ background: var(--danger); }
|
|
.btn-ok{ background: var(--ok); }
|
|
.btn-ok:hover{ background: var(--ok-hover); }
|
|
|
|
.btn-ghost{
|
|
background: transparent;
|
|
color: var(--brand);
|
|
border: 1px solid var(--brand);
|
|
}
|
|
.btn-ghost:hover{
|
|
color: #fff;
|
|
background: var(--brand);
|
|
}
|
|
|
|
/* Table */
|
|
.table-wrap{
|
|
margin-top: 16px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
background: var(--bg);
|
|
}
|
|
.table-scroll{
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
}
|
|
table{
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
min-width: 720px;
|
|
}
|
|
thead th{
|
|
position: sticky;
|
|
top: 0;
|
|
background: var(--bg-alt);
|
|
color: var(--text);
|
|
border-bottom: 1px solid var(--border);
|
|
text-align: left;
|
|
padding: 10px;
|
|
font-weight: 600;
|
|
}
|
|
tbody td{
|
|
border-top: 1px solid var(--border);
|
|
padding: 10px;
|
|
vertical-align: middle;
|
|
}
|
|
tbody tr:hover{
|
|
background: rgba(0,0,0,0.03);
|
|
}
|
|
|
|
.actions{
|
|
display: inline-flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* Job history */
|
|
details{
|
|
margin-top: 16px;
|
|
}
|
|
summary{
|
|
list-style: none;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-weight: 600;
|
|
color: var(--brand);
|
|
}
|
|
summary::-webkit-details-marker{ display: none; }
|
|
|
|
.log{
|
|
margin-top: 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
background: var(--bg);
|
|
padding: 12px;
|
|
max-height: 360px;
|
|
overflow: auto;
|
|
}
|
|
.log pre{
|
|
margin: 0;
|
|
white-space: pre;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.sr-only{
|
|
position: absolute;
|
|
width: 1px; height: 1px;
|
|
padding: 0; margin: -1px;
|
|
overflow: hidden; clip: rect(0, 0, 1, 1);
|
|
white-space: nowrap; border: 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="navbar" role="navigation" aria-label="Primary"></div>
|
|
<script>
|
|
fetch('/static/html/nav.html')
|
|
.then(r => r.ok ? r.text() : Promise.reject(r.status))
|
|
.then(html => { document.getElementById('navbar').innerHTML = html; })
|
|
.catch(() => { document.getElementById('navbar').innerHTML = '<div class="card" role="alert">Navigation failed to load.</div>'; });
|
|
</script>
|
|
|
|
<main class="content-wrapper">
|
|
<section class="content card" aria-labelledby="active-printers-heading">
|
|
<img src="/static/images/printio_white_logo.png" alt="Printio logo" class="header-img" />
|
|
<h1 id="active-printers-heading">Active Printers</h1>
|
|
|
|
<div class="switch-row">
|
|
<label class="switch-label" for="printOnConnect">Print on Connect</label>
|
|
<input
|
|
type="checkbox"
|
|
id="printOnConnect"
|
|
name="printOnConnect"
|
|
class="switch-input"
|
|
aria-describedby="printOnConnectHelp printOnConnectError"
|
|
{{ settings.printOnConnect }}>
|
|
</div>
|
|
<p id="printOnConnectHelp" class="sr-only">If enabled, a test or queued job can print automatically upon printer connection.</p>
|
|
<div id="printOnConnectError" class="error" role="alert" aria-live="polite"></div>
|
|
|
|
<div class="table-wrap" role="region" aria-label="Printer list">
|
|
<div class="table-scroll">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Printer Name</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">Prints Left</th>
|
|
<th scope="col">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for printer in printers %}
|
|
<tr>
|
|
<td>{{ printer.name }}</td>
|
|
<td>{{ printer.state }}</td>
|
|
<td>{{ printer.prints_left_message }}</td>
|
|
<td>
|
|
<div class="actions">
|
|
<form action="{{ url_for('cancel_all_jobs', printer_name=printer.name) }}" method="post">
|
|
<button type="submit" class="btn-danger">
|
|
<i class="fa fa-ban" aria-hidden="true"></i> Kill Jobs
|
|
</button>
|
|
</form>
|
|
<form action="{{ url_for('test_print', printer_name=printer.name) }}" method="post">
|
|
<button type="submit" class="btn-ok">
|
|
<i class="fa fa-print" aria-hidden="true"></i> Test Print
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<details id="jobHistory">
|
|
<summary>
|
|
<i class="fa fa-history" aria-hidden="true"></i>
|
|
Job History
|
|
</summary>
|
|
<div class="log" id="messageLog" aria-live="off">
|
|
<pre id="logText">Loading…</pre>
|
|
</div>
|
|
</details>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
/**
|
|
* Initialize the "Print on Connect" checkbox behavior.
|
|
*/
|
|
function initPrintOnConnect() {
|
|
const checkbox = document.getElementById('printOnConnect');
|
|
const errorDiv = document.getElementById('printOnConnectError');
|
|
|
|
if (!checkbox) return;
|
|
|
|
checkbox.addEventListener('change', () => {
|
|
const value = checkbox.checked;
|
|
|
|
fetch('/printer-settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ 'print-on-connect': value })
|
|
})
|
|
.then((resp) => {
|
|
if (!resp.ok) throw new Error('Server rejected the setting');
|
|
return resp.json();
|
|
})
|
|
.then((json) => {
|
|
if (!json.reply) throw new Error(json.message || 'Server error');
|
|
// Clear any prior error
|
|
errorDiv.style.display = 'none';
|
|
errorDiv.textContent = '';
|
|
})
|
|
.catch((err) => {
|
|
console.error('Failed to save print-on-connect:', err);
|
|
// Revert checkbox
|
|
checkbox.checked = !value;
|
|
// Show error
|
|
errorDiv.textContent = 'Failed to save setting to server';
|
|
errorDiv.style.display = 'block';
|
|
setTimeout(() => {
|
|
errorDiv.style.display = 'none';
|
|
}, 4000);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load job history on demand.
|
|
*/
|
|
function loadPrinterJobs() {
|
|
const jobs = {{ job_history | tojson | safe }};
|
|
const pre = document.getElementById('logText');
|
|
if (!pre) return;
|
|
|
|
if (!Array.isArray(jobs) || jobs.length === 0) {
|
|
pre.textContent = 'No jobs found.';
|
|
return;
|
|
}
|
|
|
|
// Build a readable, tabular-ish log
|
|
const lines = jobs.map(j => {
|
|
const id = j.job_id ?? '';
|
|
const t = j.time ?? '';
|
|
const size = j.job_size ?? '';
|
|
const pn = j.printer_name ?? '';
|
|
const state = j.job_state ?? '';
|
|
const reason = j.job_reason ?? '';
|
|
return `Job ${id} | Time: ${t} | Size: ${size} | Printer: ${pn} | State: ${state} | Reason: ${reason}`;
|
|
});
|
|
|
|
pre.textContent = lines.join('\n');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initPrintOnConnect();
|
|
|
|
// Lazy-load job history when the <details> opens
|
|
const details = document.getElementById('jobHistory');
|
|
if (details) {
|
|
details.addEventListener('toggle', () => {
|
|
if (details.open) loadPrinterJobs();
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|