Printio/templates/index.html
2025-10-27 22:24:25 -07:00

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>