Havre: update pill (NEW VERSION · RELOAD) #215
No files matched your search
@@ -23,6 +23,7 @@
|
||||
<link rel="alternate" type="text/markdown" href="skill.md" title="Skill">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script type="module" src="app.js"></script>
|
||||
<script src="update.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#chat">skip to the chat</a>
|
||||
|
||||
@@ -551,6 +551,13 @@ p.note { padding: 8px 10px; border: 1px solid var(--ink); background: var(--hi);
|
||||
.lefthead [role=tab] { font-size: 19px; }
|
||||
.lefthead .sep::before { font-size: 19px; }
|
||||
}
|
||||
/* a new version is out: a bar at the top to reload (update.js) */
|
||||
#upd {
|
||||
position: fixed; top: calc(10px + env(safe-area-inset-top)); left: 50%; transform: translateX(-50%); z-index: 30;
|
||||
padding: 9px 16px; border: 1px solid var(--paper); border-radius: 0; background: var(--ink); color: var(--paper);
|
||||
font: 700 13px/1 var(--mono); letter-spacing: .12em; white-space: nowrap; cursor: pointer;
|
||||
}
|
||||
#upd:hover, #upd:focus-visible { background: var(--hi); color: var(--ink); border-color: var(--ink); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { scroll-behavior: auto !important; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// update.js: an installed Havre has no address bar to refresh from, so the page notices when a new version has been
|
||||
// published and offers a reload, like gandora's pill. It fingerprints the files the app is made of when it opens,
|
||||
// checks again whenever it comes back to the foreground (and every ten minutes), and shows the pill when they differ.
|
||||
// Checking fetches with cache: "reload", which also refreshes the browser's copies, so the reload gets the new files.
|
||||
(() => {
|
||||
const FILES = ["./", "app.js", "style.css", "workflows.js", "abilities.js", "card.js", "venice.js", "memory.js",
|
||||
"threads.js", "media.js", "backup.js", "theme.js"];
|
||||
let base = null, shown = false, busy = false;
|
||||
async function print() {
|
||||
const parts = await Promise.all(FILES.map((f) => fetch(f, { cache: "reload" }).then((r) => (r.ok ? r.text() : "")).catch(() => null)));
|
||||
if (parts.some((p) => p === null)) return null; // offline: no verdict
|
||||
const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(parts.join("\u0000")));
|
||||
return Array.from(new Uint8Array(d), (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
function show() {
|
||||
if (shown) return;
|
||||
shown = true;
|
||||
const b = document.createElement("button");
|
||||
b.id = "upd"; b.type = "button"; b.textContent = "NEW VERSION · RELOAD";
|
||||
b.addEventListener("click", () => location.reload());
|
||||
document.body.append(b);
|
||||
}
|
||||
async function check() {
|
||||
if (busy || shown || !crypto.subtle) return;
|
||||
busy = true;
|
||||
try {
|
||||
const now = await print();
|
||||
if (!now) return;
|
||||
if (!base) base = now;
|
||||
else if (now !== base) show();
|
||||
} finally { busy = false; }
|
||||
}
|
||||
if (location.protocol !== "https:") return;
|
||||
setTimeout(check, 3000);
|
||||
setInterval(check, 10 * 60 * 1000);
|
||||
document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") check(); });
|
||||
})();
|
||||
Reference in new issue
Block a user