UI redesign v2: modern ant/mono design + app version (v2.0.0)
Test / Tests (Python 3.12) (push) Canceled after 0s

This commit is contained in:
sirdave73
2026-09-04 07:22:43 -04:00
parent 7ea2b2baad
commit fa558ccfb3
2 changed files with 378 additions and 163 deletions
+5 -2
View File
@@ -48,6 +48,9 @@ for _d in (UPLOAD_DIR, OUTPUT_DIR, WORK_DIR):
MAX_UPLOAD_MB = 400 MAX_UPLOAD_MB = 400
# Semantic version exposed by /health so the UI can show "vX.Y.Z" in the footer.
APP_VERSION = "2.0.0"
# req 3 (health): supported formats advertised to clients. Additive key — the # req 3 (health): supported formats advertised to clients. Additive key — the
# existing /health contract asserts only status + max_upload_mb. # existing /health contract asserts only status + max_upload_mb.
SUPPORTED_FORMATS = ["epub", "html", "htm", "md", "markdown", "txt", "log", SUPPORTED_FORMATS = ["epub", "html", "htm", "md", "markdown", "txt", "log",
@@ -623,10 +626,10 @@ def health():
if not _cjk_fonts_present(): if not _cjk_fonts_present():
problems.append("CJK fonts missing (fc-list found none)") problems.append("CJK fonts missing (fc-list found none)")
body = {"status": "ok", "max_upload_mb": MAX_UPLOAD_MB, body = {"status": "ok", "max_upload_mb": MAX_UPLOAD_MB,
"supported_formats": SUPPORTED_FORMATS} "supported_formats": SUPPORTED_FORMATS, "version": APP_VERSION}
if problems: if problems:
body = {"status": "degraded", "max_upload_mb": MAX_UPLOAD_MB, body = {"status": "degraded", "max_upload_mb": MAX_UPLOAD_MB,
"supported_formats": SUPPORTED_FORMATS, "problems": problems} "supported_formats": SUPPORTED_FORMATS, "version": APP_VERSION, "problems": problems}
return jsonify(body), 503 return jsonify(body), 503
return jsonify(body), 200 return jsonify(body), 200
+349 -137
View File
@@ -1,143 +1,350 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Bindery — EPUB to PDF</title> <title>Bindery &mdash; EPUB to PDF</title>
<style> <style>
:root{ :root {
--ink-0:#131a17; --ink-1:#1a221e; --ink-2:#212a25; --line:#2c362f; /* Mono palette with an indigo/violet accent */
--paper:#efe9dc; --muted:#8b938a; --gilt:#c9a24b; --gilt-hi:#e2c06e; --bg: #0b0d17;
--ok:#86a878; --err:#d07f6b; --sheet:#f4efe2; --sheet-line:#d8d1c0; --bg-elev: #131522;
--serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif; --bg-card: rgba(22, 24, 40, 0.72);
--sans:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; --line: rgba(255, 255, 255, 0.08);
--mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace; --line-strong: rgba(255, 255, 255, 0.14);
--text: #f2f3f7;
--muted: #9aa0b4;
--muted-2: #6b7188;
--accent: #6366f1;
--accent-2: #8b5cf6;
--accent-soft: rgba(139, 92, 246, 0.16);
--ok: #34d399;
--err: #f87171;
--radius: 18px;
--radius-sm: 11px;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
--shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.6);
--shadow-soft: 0 8px 24px -12px rgba(0, 0, 0, 0.5);
} }
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%} * { box-sizing: border-box; margin: 0; padding: 0; }
body{ html, body { height: 100%; }
font-family:var(--sans); color:var(--paper); background:var(--ink-0); body {
background-image:radial-gradient(120% 80% at 50% -10%, #1d2721 0%, var(--ink-0) 60%); font-family: var(--sans);
min-height:100vh; display:flex; flex-direction:column; align-items:center; color: var(--text);
-webkit-font-smoothing:antialiased; background: var(--bg);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
-webkit-font-smoothing: antialiased;
background-image:
radial-gradient(120% 90% at 50% -10%, rgba(99, 102, 241, 0.18), transparent 55%),
radial-gradient(120% 80% at 80% 0%, rgba(139, 92, 246, 0.12), transparent 50%),
var(--bg);
position: relative;
overflow-x: hidden;
} }
.mono{font-family:var(--mono)}
/* ---- masthead ---- */ /* ambient grid backdrop */
.masthead{width:100%;max-width:720px;padding:44px 20px 8px;text-align:center} body::before {
.wordmark{ content: "";
font-family:var(--serif); font-size:2.5rem; font-weight:600; letter-spacing:.5px; position: fixed;
color:var(--paper); display:inline-flex; align-items:baseline; gap:.4rem; inset: 0;
background-image: linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: radial-gradient(100% 70% at 50% 0%, #000 0%, transparent 75%);
pointer-events: none;
z-index: 0;
} }
.wordmark .fleuron{color:var(--gilt); font-size:1.5rem; transform:translateY(-2px)}
.tagline{color:var(--muted); margin-top:10px; font-size:.95rem; max-width:34ch; margin-left:auto; margin-right:auto; line-height:1.5}
.rule{width:720px;max-width:calc(100% - 40px);height:1px;background:linear-gradient(90deg,transparent,var(--line),transparent);margin:22px auto 0}
main{width:100%;max-width:720px;padding:26px 20px 48px;flex:1} .wrap {
position: relative;
/* ---- drop zone (shelf of spines) ---- */ z-index: 1;
.drop{ width: 100%;
border:1px dashed #3a463d; border-radius:14px; background:linear-gradient(180deg,var(--ink-1),var(--ink-0)); max-width: 780px;
padding:34px 24px 40px; text-align:center; cursor:pointer; outline:none; padding: 0 20px;
transition:border-color .18s, background .18s, transform .18s; display: flex;
flex-direction: column;
flex: 1;
} }
.drop:hover{border-color:#4b5a50}
.drop:focus-visible{border-color:var(--gilt); box-shadow:0 0 0 3px rgba(201,162,75,.25)}
.drop.over{border-color:var(--gilt); background:linear-gradient(180deg,var(--ink-2),var(--ink-1))}
.drop input{display:none}
.shelf{position:relative;display:flex;align-items:flex-end;justify-content:center;gap:7px;min-height:188px;padding:0 6px 14px} /* ---- logo + nav ---- */
.shelf::after{ .topbar {
content:"";position:absolute;left:-16px;right:-16px;bottom:2px;height:11px;border-radius:2px; display: flex;
background:linear-gradient(180deg,#2b2118,#181209);box-shadow:0 10px 18px rgba(0,0,0,.5); align-items: center;
justify-content: space-between;
padding: 26px 0 8px;
} }
.spine{ .wordmark {
position:relative;width:var(--w);height:var(--h);background:var(--c);border-radius:2px 2px 1px 1px; display: inline-flex;
box-shadow:inset 3px 0 4px rgba(255,255,255,.07),inset -5px 0 8px rgba(0,0,0,.4); align-items: center;
transition:transform .22s cubic-bezier(.2,.8,.3,1); gap: 9px;
font-weight: 700;
font-size: 1.25rem;
letter-spacing: -0.01em;
} }
.spine::before{content:"";position:absolute;top:15px;left:50%;transform:translateX(-50%);width:66%;height:3px;background:rgba(232,206,142,.5);border-radius:1px} .wordmark .glyph {
.spine::after{content:"";position:absolute;bottom:13px;left:50%;transform:translateX(-50%);width:54%;height:2px;background:rgba(232,206,142,.32);border-radius:1px} display: grid;
.drop:hover .spine,.drop.over .spine{transform:translateY(-7px)} place-items: center;
.drop:hover .spine:nth-child(2n),.drop.over .spine:nth-child(2n){transform:translateY(-11px)} width: 32px; height: 32px;
border-radius: 9px;
.drop-copy{margin-top:26px} background: linear-gradient(135deg, var(--accent), var(--accent-2));
.drop-copy h1{font-family:var(--serif);font-weight:600;font-size:1.5rem;letter-spacing:.2px} color: white;
.drop-copy p{color:var(--muted);margin-top:10px;font-size:.9rem} font-size: 1rem;
.drop-copy .browse{color:var(--gilt);text-decoration:underline;text-underline-offset:3px;text-decoration-color:rgba(201,162,75,.4)} box-shadow: 0 6px 16px -4px rgba(99, 102, 241, 0.6);
.drop-copy code{font-family:var(--mono);color:var(--paper);background:var(--ink-2);padding:1px 6px;border-radius:4px;font-size:.82em}
.drop-err{margin-top:14px;color:var(--err);font-size:.9rem;min-height:1.1em;opacity:0;transition:opacity .2s}
.drop-err.show{opacity:1}
/* ---- status / press state ---- */
.status{border:1px solid var(--line);border-radius:14px;background:linear-gradient(180deg,var(--ink-1),var(--ink-0));padding:26px 24px}
.status-top{display:flex;justify-content:space-between;align-items:flex-end;gap:20px}
.book-meta{min-width:0;flex:1}
.eyebrow{font-family:var(--mono);font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--gilt)}
.bookname{display:block;font-family:var(--mono);font-weight:600;font-size:1.02rem;margin-top:8px;word-break:break-all;line-height:1.35}
.chapter{display:block;font-family:var(--mono);font-size:.85rem;color:var(--muted);margin-top:6px;min-height:1.1em}
.book-block{
position:relative;flex:0 0 auto;width:112px;height:148px;border-radius:3px;overflow:hidden;
background:linear-gradient(180deg,#241b11,#171109);border:1px solid var(--line);
box-shadow:inset 0 0 0 3px #0c0906,inset 9px 0 0 rgba(0,0,0,.35);
} }
.pages{ .pill {
position:absolute;left:0;right:0;bottom:0;height:0%; display: inline-flex;
background:repeating-linear-gradient(180deg,var(--sheet) 0 2px,var(--sheet-line) 2px 3px); align-items: center;
box-shadow:0 -1px 0 rgba(0,0,0,.45);transition:height .25s ease; gap: 7px;
font-size: 0.78rem;
color: var(--muted);
padding: 6px 12px;
border-radius: 999px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.03);
font-family: var(--mono);
} }
.current{position:absolute;left:-2px;right:-2px;bottom:0%;height:2px;background:var(--gilt);box-shadow:0 0 7px rgba(201,162,75,.65);transition:bottom .25s ease} .pill .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted-2); }
.pill .dot.online { background: var(--ok); box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.18); }
.pill .dot.offline { background: var(--err); box-shadow: 0 0 0 4px rgba(248, 113, 113, 0.18); }
.status-line{margin-top:20px;padding-top:16px;border-top:1px solid var(--line);color:var(--muted);font-size:.85rem;min-height:1.2em} /* ---- hero ---- */
.masthead { padding: 46px 0 34px; text-align: center; }
.actions{margin-top:18px;display:flex;align-items:center;gap:12px;flex-wrap:wrap} .headline {
.btn{display:inline-block;padding:11px 22px;border-radius:8px;border:1px solid transparent;font-size:.92rem;font-weight:600;cursor:pointer;text-decoration:none;font-family:var(--sans)} font-size: clamp(2rem, 5vw, 3rem);
.btn:focus-visible{outline:2px solid var(--gilt);outline-offset:2px} font-weight: 700;
.btn-primary{background:var(--gilt);color:#1a1408} letter-spacing: -0.025em;
.btn-primary:hover{background:var(--gilt-hi)} line-height: 1.08;
.btn-ghost{background:transparent;color:var(--muted);border-color:var(--line)} background: linear-gradient(180deg, #ffffff 30%, #b7bce0 100%);
.btn-ghost:hover{color:var(--paper);border-color:#4b5a50} -webkit-background-clip: text;
.err{color:var(--err);font-size:.9rem} background-clip: text;
-webkit-text-fill-color: transparent;
footer{width:100%;max-width:720px;padding:16px 20px 26px;display:flex;justify-content:space-between;align-items:center;color:#5f665e;font-size:.78rem} max-width: 22ch;
footer .dot{color:var(--ok)} margin: 0 auto;
@media (max-width:560px){
.wordmark{font-size:2rem}
.status-top{flex-direction:column;align-items:stretch}
.book-block{margin:0 auto}
.shelf{min-height:150px;gap:5px}
.spine{--h:calc(var(--h) * .82)}
} }
@media (prefers-reduced-motion:reduce){ .subline {
*{transition:none !important} color: var(--muted);
font-size: 1.02rem;
line-height: 1.55;
max-width: 42ch;
margin: 16px auto 0;
}
.trust {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 18px;
color: var(--muted-2);
font-size: 0.82rem;
}
.trust svg { color: var(--accent-2); }
/* ---- drop zone ---- */
.drop {
position: relative;
border: 1.5px dashed var(--line-strong);
border-radius: var(--radius);
background: var(--bg-card);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
padding: 44px 26px 40px;
text-align: center;
cursor: pointer;
outline: none;
transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s, background 0.2s;
}
.drop::before {
content: "";
position: absolute;
inset: 0;
border-radius: var(--radius);
padding: 1.5px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.drop:hover { border-color: transparent; }
.drop:hover::before { opacity: 1; }
.drop:focus-visible { box-shadow: 0 0 0 4px var(--accent-soft); }
.drop.over { border-color: transparent; transform: translateY(-2px); background: rgba(139, 92, 246, 0.08); }
.drop.over::before { opacity: 1; }
.drop input { display: none; }
.drop-visual {
display: grid;
place-items: center;
width: 64px; height: 64px;
margin: 0 auto 20px;
border-radius: 16px;
background: var(--accent-soft);
color: var(--accent-2);
}
.drop h1 { font-size: 1.32rem; font-weight: 650; letter-spacing: -0.01em; }
.drop-note { color: var(--muted); margin-top: 11px; font-size: 0.92rem; line-height: 1.5; }
.drop-note .browse { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
.drop-note code {
font-family: var(--mono);
color: var(--text);
background: rgba(255, 255, 255, 0.06);
padding: 2px 7px;
border-radius: 6px;
font-size: 0.8em;
}
.drop-err {
margin-top: 16px;
color: var(--err);
font-size: 0.88rem;
min-height: 1.1em;
opacity: 0;
transition: opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.drop-err.show { opacity: 1; }
/* ---- status card ---- */
.status {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--bg-card);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
padding: 28px 26px;
box-shadow: var(--shadow);
}
.status-top { display: flex; align-items: center; gap: 22px; }
.book-meta { flex: 1; min-width: 0; }
.eyebrow { font-family: var(--mono); font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent-2); }
.bookname { display: block; font-family: var(--mono); font-weight: 600; font-size: 1rem; margin-top: 8px; word-break: break-all; line-height: 1.35; }
.chapter { display: block; font-family: var(--mono); font-size: 0.82rem; color: var(--muted); margin-top: 6px; min-height: 1.1em; }
.book-block {
position: relative; flex: 0 0 auto; width: 96px; height: 128px;
border-radius: 8px;
background: linear-gradient(135deg, #1c1f33, #141627);
border: 1px solid var(--line-strong);
box-shadow: var(--shadow-soft), inset 0 0 0 1px rgba(255,255,255,0.03);
overflow: hidden;
}
.book-block::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 6px;
background: linear-gradient(180deg, var(--accent), var(--accent-2));
}
.book-glow {
position: absolute; inset: 0;
background: radial-gradient(120% 80% at 50% 0%, var(--accent-soft), transparent 70%);
opacity: 0.5;
}
.progress-track {
position: absolute; left: 0; right: 0; bottom: 0; height: 4px;
background: rgba(255, 255, 255, 0.08);
}
.progress-fill {
height: 100%; width: 0%;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
box-shadow: 0 0 10px rgba(139, 92, 246, 0.7);
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.status-line { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--muted); font-size: 0.85rem; min-height: 1.2em; }
.actions { margin-top: 18px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: var(--radius-sm);
border: 1px solid transparent;
font-size: 0.94rem;
font-weight: 600;
cursor: pointer;
text-decoration: none;
font-family: var(--sans);
min-height: 44px;
transition: transform 0.15s, background 0.15s, border-color 0.15s, opacity 0.15s;
}
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:active { transform: translateY(1px); }
.btn-primary {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: white;
box-shadow: 0 8px 20px -8px rgba(99, 102, 241, 0.7);
}
.btn-primary:hover { filter: brightness(1.08); }
.btn-ghost {
background: transparent;
color: var(--muted);
border-color: var(--line-strong);
}
.btn-ghost:hover { color: var(--text); border-color: #5b617a; background: rgba(255,255,255,0.03); }
.err { color: var(--err); font-size: 0.9rem; }
/* ---- footer ---- */
footer {
width: 100%;
max-width: 780px;
padding: 22px 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
color: var(--muted-2);
font-size: 0.78rem;
}
footer .mono { font-family: var(--mono); color: var(--muted); }
footer .version { color: var(--muted); }
@media (max-width: 560px) {
.topbar { padding: 20px 0 4px; }
.masthead { padding: 34px 0 26px; }
.status-top { flex-direction: column; align-items: stretch; }
.book-block { margin: 0 auto; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
} }
</style> </style>
</head> </head>
<body> <body>
<header class="masthead"> <div class="wrap">
<div class="wordmark"><span class="fleuron">&#10086;</span>Bindery</div> <div class="topbar">
<p class="tagline">A local typesetter. Drop a document, take a properly set PDF. Nothing leaves your machine.</p> <div class="wordmark">
</header> <span class="glyph">&#10086;</span> Bindery
<div class="rule"></div> </div>
<span class="pill" id="health-pill">
<span class="dot" id="health-dot"></span>
<span id="health-state">checking&hellip;</span>
</span>
</div>
<main> <header class="masthead">
<h1 class="headline">Turn any document into a typeset PDF</h1>
<p class="subline">Drop an EPUB, Markdown, DOCX, or 20+ other formats &mdash; get a clean, self-hosted PDF back in seconds. Your files never leave this machine.</p>
<span class="trust">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Local &amp; private &middot; nothing leaves your machine
</span>
</header>
<main>
<section id="drop" class="drop" role="button" tabindex="0" aria-label="Upload a document"> <section id="drop" class="drop" role="button" tabindex="0" aria-label="Upload a document">
<div class="shelf" aria-hidden="true"> <div class="drop-visual" aria-hidden="true">
<span class="spine" style="--c:#6e3b34;--h:150px;--w:24px"></span> <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="m7 8 5-5 5 5"/><path d="M5 21h14"/></svg>
<span class="spine" style="--c:#33513f;--h:164px;--w:20px"></span>
<span class="spine" style="--c:#33415c;--h:138px;--w:26px"></span>
<span class="spine" style="--c:#5a5934;--h:158px;--w:18px"></span>
<span class="spine" style="--c:#54394b;--h:146px;--w:22px"></span>
<span class="spine" style="--c:#315550;--h:168px;--w:24px"></span>
<span class="spine" style="--c:#7a4a38;--h:134px;--w:20px"></span>
<span class="spine" style="--c:#46525e;--h:156px;--w:26px"></span>
</div> </div>
<div class="drop-copy">
<h1>Drop a file to set it in type</h1> <h1>Drop a file to set it in type</h1>
<p>or <span class="browse">browse your files</span> &middot; up to <span id="maxmb">400</span>&nbsp;MB &middot; <code>.epub .docx .odt .rtf .tex .org .adoc .rst .html .md .txt .csv .json .xml .svg .png .jpg .webp &hellip;</code></p> <p class="drop-note">or <span class="browse">browse your files</span> &middot; up to <span id="maxmb">400</span>&nbsp;MB &middot; <code>.epub .docx .odt .md .html .txt .csv .svg .png .pdf</code></p>
</div>
<p id="droperr" class="drop-err" role="alert"></p> <p id="droperr" class="drop-err" role="alert"></p>
<input type="file" id="file" accept=".epub,.html,.htm,.txt,.log,.md,.markdown,.csv,.tsv,.json,.xml,.svg,.png,.jpg,.jpeg,.webp,.tiff,.bmp,.pdf,.docx,.odt,.rtf,.tex,.latex,.org,.adoc,.rst"> <input type="file" id="file" accept=".epub,.html,.htm,.txt,.log,.md,.markdown,.csv,.tsv,.json,.xml,.svg,.png,.jpg,.jpeg,.webp,.tiff,.bmp,.pdf,.docx,.odt,.rtf,.tex,.latex,.org,.adoc,.rst">
</section> </section>
@@ -150,8 +357,8 @@
<span id="chapter" class="chapter"></span> <span id="chapter" class="chapter"></span>
</div> </div>
<div class="book-block" aria-hidden="true"> <div class="book-block" aria-hidden="true">
<div class="pages" id="pages"></div> <div class="book-glow"></div>
<div class="current" id="current"></div> <div class="progress-track"><div class="progress-fill" id="pages"></div></div>
</div> </div>
</div> </div>
<div class="status-line mono" id="msg"></div> <div class="status-line mono" id="msg"></div>
@@ -161,12 +368,13 @@
<span class="err" id="errtext" hidden></span> <span class="err" id="errtext" hidden></span>
</div> </div>
</section> </section>
</main> </main>
</div>
<footer> <footer>
<span>Bindery &middot; EPUB &rarr; PDF &middot; WeasyPress engine</span> <span>Bindery &middot; EPUB &rarr; PDF &middot; WeasyPress engine</span>
<span class="mono" id="foot">press: &hellip;</span> <span class="version">v<span id="foot">&mdash;</span></span>
</footer> </footer>
<script> <script>
(function () { (function () {
@@ -177,22 +385,30 @@
var chapter = document.getElementById('chapter'); var chapter = document.getElementById('chapter');
var stateEl = document.getElementById('state'); var stateEl = document.getElementById('state');
var pages = document.getElementById('pages'); var pages = document.getElementById('pages');
var current = document.getElementById('current'); var chapterEl = document.getElementById('chapter');
var msg = document.getElementById('msg'); var msg = document.getElementById('msg');
var errtext = document.getElementById('errtext'); var errtext = document.getElementById('errtext');
var download = document.getElementById('download'); var download = document.getElementById('download');
var again = document.getElementById('again'); var again = document.getElementById('again');
var droperr = document.getElementById('droperr'); var droperr = document.getElementById('droperr');
var foot = document.getElementById('foot'); var foot = document.getElementById('foot');
var healthDot = document.getElementById('health-dot');
var healthState = document.getElementById('health-state');
var timer = null, errTimer = null; var timer = null, errTimer = null;
fetch('/health').then(function (r) { return r.json(); }) fetch('/health').then(function (r) { return r.json(); })
.then(function (d) { .then(function (d) {
foot.innerHTML = 'press: <span class="dot">ok</span>'; healthDot.classList.add(d.status === 'ok' ? 'online' : 'offline');
healthState.textContent = d.status === 'ok' ? 'Ready' : 'Degraded';
var maxmb = document.getElementById('maxmb'); var maxmb = document.getElementById('maxmb');
if (maxmb && d.max_upload_mb != null) maxmb.textContent = d.max_upload_mb; if (maxmb && d.max_upload_mb != null) maxmb.textContent = d.max_upload_mb;
if (d.version) foot.textContent = 'v' + d.version;
}) })
.catch(function () { foot.textContent = 'press: down'; }); .catch(function () {
healthDot.classList.add('offline');
healthState.textContent = 'Offline';
foot.textContent = 'v';
});
function openPicker() { fileInput.click(); } function openPicker() { fileInput.click(); }
drop.addEventListener('click', openPicker); drop.addEventListener('click', openPicker);
@@ -225,9 +441,8 @@
function resetUI() { function resetUI() {
if (timer) clearInterval(timer); if (timer) clearInterval(timer);
fileInput.value = ''; fileInput.value = '';
pages.style.height = '0%'; pages.style.width = '0%';
current.style.bottom = '0%'; chapterEl.textContent = '';
chapter.textContent = '';
stateEl.textContent = 'Setting in type'; stateEl.textContent = 'Setting in type';
msg.textContent = ''; msg.textContent = '';
errtext.hidden = true; errtext.hidden = true;
@@ -238,16 +453,15 @@
function start(f) { function start(f) {
var OKEXT = /\.(epub|html?|txt|log|md|markdown|csv|tsv|json|xml|svg|png|jpe?g|webp)$/i; var OKEXT = /\.(epub|html?|txt|log|md|markdown|csv|tsv|json|xml|svg|png|jpe?g|webp)$/i;
if (!OKEXT.test(f.name)) { flashError('Unsupported file type — accepted: .epub .html .md .txt .log .csv .tsv .json .xml .svg .png .jpg .webp'); return; } if (!OKEXT.test(f.name)) { flashError('Unsupported file type &mdash; accepted: .epub .html .md .txt .log .csv .tsv .json .xml .svg .png .jpg .webp'); return; }
var form = new FormData(); var form = new FormData();
form.append('file', f); form.append('file', f);
showStatus(); showStatus();
bookname.textContent = f.name; bookname.textContent = f.name;
stateEl.textContent = 'Sending to the press'; stateEl.textContent = 'Sending to the press';
chapter.textContent = ''; chapterEl.textContent = '';
msg.textContent = 'Uploading…'; msg.textContent = 'Uploading…';
pages.style.height = '0%'; pages.style.width = '0%';
current.style.bottom = '0%';
fetch('/api/convert', { method: 'POST', body: form }) fetch('/api/convert', { method: 'POST', body: form })
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); }) .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); })
@@ -271,17 +485,15 @@
var j = res.data; var j = res.data;
if (j.status === 'error') return fail(j.error || 'Conversion failed'); if (j.status === 'error') return fail(j.error || 'Conversion failed');
var p = j.progress || 0; var p = j.progress || 0;
pages.style.height = p + '%'; pages.style.width = p + '%';
current.style.bottom = p + '%';
var m = (j.message || '').match(/\((\d+)\/(\d+)\)/); var m = (j.message || '').match(/\((\d+)\/(\d+)\)/);
chapter.textContent = m ? ('ch ' + m[1] + ' / ' + m[2]) : ''; chapterEl.textContent = m ? ('step ' + m[1] + ' / ' + m[2]) : '';
stateEl.textContent = /merging/i.test(j.message || '') ? 'Collating pages' : 'Setting in type'; stateEl.textContent = /merging/i.test(j.message || '') ? 'Collating pages' : 'Setting in type';
msg.textContent = j.message || 'Working…'; msg.textContent = j.message || 'Working…';
if (j.status === 'done') { if (j.status === 'done') {
clearInterval(timer); clearInterval(timer);
pages.style.height = '100%'; pages.style.width = '100%';
current.style.bottom = '100%'; stateEl.textContent = 'Set &amp; bound';
stateEl.textContent = 'Set & bound';
msg.textContent = 'Done — ready to download.'; msg.textContent = 'Done — ready to download.';
download.href = '/download/' + encodeURIComponent(j.filename); download.href = '/download/' + encodeURIComponent(j.filename);
download.hidden = false; download.hidden = false;