Dashboard Studio 1.14.1: Android app, phone menu button, install fix
- Android app (android/, built with build.sh): signs in to Home Assistant once, opens Dashboard Studio full screen, renews the ingress session; downloadable from Install as an app - Phone dashboards: a round menu button brings back the top bar with Edit, Kiosk and Install as an app - Install as an app explains what works per device (Android app, https for browser install, iPhone limits) - Fixed: browser install failed because the manifest was fetched without the Home Assistant sign-in Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+56
-20
@@ -246,21 +246,50 @@
|
||||
let installPrompt = null;
|
||||
window.addEventListener("beforeinstallprompt", (e) => { e.preventDefault(); installPrompt = e; });
|
||||
const standalone = () => matchMedia("(display-mode: standalone)").matches || navigator.standalone === true;
|
||||
// The Android app (android/ in the repository) shows this page in its own window and adds window.DSApp.
|
||||
const IN_APP = !!window.DSApp;
|
||||
const APK_ID = "nl.melvanveen.dashboardstudio";
|
||||
async function installDialog() {
|
||||
let inFrame = false;
|
||||
try { inFrame = window.top !== window; } catch { inFrame = true; }
|
||||
const ios = /iphone|ipad|ipod/i.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
const how = inFrame
|
||||
? tr("Home Assistant’s page can’t be installed itself. Open Dashboard Studio in its own tab first, then choose Install as an app again there.")
|
||||
: ios ? tr("In Safari, tap the Share button, then Add to Home Screen.")
|
||||
: installPrompt ? tr("Your browser can install Dashboard Studio as an app with its own icon. It opens full screen, without the browser’s bars.")
|
||||
: tr("In your browser’s menu, choose Install app or Add to Home screen.");
|
||||
const choice = await openModal(`<h3>${tr("Install as an app")}</h3><p>${esc(how)}</p>
|
||||
<p class="muted small">${tr("It opens on the dashboard chosen for that device. Mark buttons as a shortcut to use them from the app icon.")}</p>
|
||||
<div class="modal-actions"><button class="btn" data-x="cancel">${tr("Close")}</button>${inFrame ? `<button class="btn primary" data-x="tab">${tr("Open in its own tab")}</button>` : !ios && installPrompt && !standalone() ? `<button class="btn primary" data-x="install">${tr("Install")}</button>` : ""}</div>`,
|
||||
(e, close) => { const x = e.target.closest("[data-x]")?.dataset.x; if (x) close(x); });
|
||||
if (choice === "tab") window.open(new URL(`?ds=${current.id}`, BASE).href, "_blank", "noopener");
|
||||
const ua = navigator.userAgent;
|
||||
const ios = /iphone|ipad|ipod/i.test(ua) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
const android = /android/i.test(ua);
|
||||
const link = new URL(`?ds=${encodeURIComponent(current.id)}`, BASE).href;
|
||||
const browserInstall = !inFrame && window.isSecureContext && installPrompt && !standalone();
|
||||
const p = (t, cls = "") => `<p${cls ? ` class="${cls}"` : ""}>${esc(t)}</p>`;
|
||||
let body, actions = `<button class="btn" data-x="cancel">${tr("Close")}</button>`;
|
||||
if (IN_APP) {
|
||||
body = p(tr("This is the Dashboard Studio app, connected to {0}.", location.host));
|
||||
actions += `<button class="btn" data-x="reset">${tr("Change address")}</button>`;
|
||||
} else if (android) {
|
||||
// A home screen app from the browser needs https, and Home Assistant's own app can't make one at all: the Android app always works.
|
||||
body = p(tr("Get the Dashboard Studio app for Android. It opens full screen with its own icon and keeps you signed in."))
|
||||
+ `<ol class="steps"><li>${esc(tr("Download the app and open the file to install it. Android asks once to allow apps from this source."))}</li>
|
||||
<li>${esc(tr("Come back here and tap Open in the app. Or open the app and enter the address of Home Assistant."))}</li></ol>`
|
||||
+ (browserInstall ? p(tr("Or install it from the browser, without the app."), "muted small") : "");
|
||||
actions += (browserInstall ? `<button class="btn" data-x="install">${tr("Install from the browser")}</button>` : "")
|
||||
+ `<a class="btn" href="${new URL("android.apk", BASE).href}" download="Dashboard Studio.apk">${tr("Download the app")}</a>`
|
||||
+ `<a class="btn primary" target="_top" href="intent://open?url=${encodeURIComponent(link)}#Intent;scheme=dashboardstudio;package=${APK_ID};end">${tr("Open in the app")}</a>`;
|
||||
} else if (ios) {
|
||||
// Home screen web apps on iPhone get their own storage, without Home Assistant's sign-in: they can't open an app inside Home Assistant.
|
||||
body = p(tr("On iPhone and iPad, apps added to the home screen can’t use Home Assistant’s sign-in, so Dashboard Studio can’t run on its own there. Open it from the sidebar in the Home Assistant app instead."));
|
||||
} else if (inFrame) {
|
||||
body = p(tr("Home Assistant’s page can’t be installed itself. Open Dashboard Studio in its own tab first, then choose Install as an app again there."));
|
||||
actions += `<button class="btn primary" data-x="tab">${tr("Open in its own tab")}</button>`;
|
||||
} else if (!window.isSecureContext) {
|
||||
body = p(tr("Browsers only install apps from a secure address (https), and Home Assistant is open over http here. Open Home Assistant through its https address, for example Home Assistant Cloud, and try again."));
|
||||
} else {
|
||||
body = p(browserInstall ? tr("Your browser can install Dashboard Studio as an app with its own icon. It opens full screen, without the browser’s bars.") : tr("In your browser’s menu, choose Install app or Add to Home screen."));
|
||||
if (browserInstall) actions += `<button class="btn primary" data-x="install">${tr("Install")}</button>`;
|
||||
}
|
||||
const choice = await openModal(`<h3>${tr("Install as an app")}</h3>${body}
|
||||
${IN_APP || ios ? "" : p(tr("It opens on the dashboard chosen for that device. Mark buttons as a shortcut to use them from the app icon."), "muted small")}
|
||||
<div class="modal-actions">${actions}</div>`,
|
||||
(e, close) => { const x = e.target.closest("[data-x], a.btn"); if (x) close(x.dataset.x || "link"); });
|
||||
if (choice === "tab") window.open(link, "_blank", "noopener");
|
||||
if (choice === "install" && installPrompt) { installPrompt.prompt(); installPrompt = null; }
|
||||
if (choice === "reset") window.DSApp.reset();
|
||||
}
|
||||
|
||||
/** Opened on its own (as an app, or in a tab), nobody renews Home Assistant's ingress session, so
|
||||
@@ -770,7 +799,9 @@
|
||||
const bg = t.preset !== "System" && t.bg ? t.bg : "var(--canvas)";
|
||||
if (t.bgImage) s.setProperty("--d-bg", `center / cover no-repeat url("${t.bgImage.replace(/"/g, "")}"), ${bg}`);
|
||||
s.setProperty("--d-radius", `${t.radius ?? 20}px`);
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", /^#[0-9a-f]{3,8}$/i.test(t.bg || "") && t.preset !== "System" ? t.bg : prefersDark() ? "#000000" : "#f2f2f7");
|
||||
const bar = /^#[0-9a-f]{3,8}$/i.test(t.bg || "") && t.preset !== "System" ? t.bg : prefersDark() ? "#000000" : "#f2f2f7";
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", bar);
|
||||
window.DSApp?.setBarColor(bar);
|
||||
s.setProperty("--d-gap", `${Number(current.grid.gap) || 14}px`);
|
||||
stageEl.classList.toggle("theme-outline", !!t.outline && t.preset !== "System");
|
||||
s.setProperty("--d-blur", `${t.blur || 0}px`);
|
||||
@@ -2450,6 +2481,7 @@
|
||||
|
||||
async function requestWakeLock() {
|
||||
try { wakeLock = await navigator.wakeLock?.request("screen"); } catch { wakeLock = null; }
|
||||
window.DSApp?.keepAwake(true); // the Android app has no wake lock: it keeps the screen on itself
|
||||
}
|
||||
|
||||
const cornerName = () => (current.kiosk.exitCorner || "top-right").replace("-", " ");
|
||||
@@ -2474,6 +2506,7 @@
|
||||
closeDoorbell();
|
||||
clearTimeout(idleTimer); clearTimeout(saverTimer);
|
||||
wakeLock?.release?.(); wakeLock = null;
|
||||
window.DSApp?.keepAwake(false);
|
||||
if (document.fullscreenElement) document.exitFullscreen?.();
|
||||
history.replaceState(null, "", location.pathname);
|
||||
renderGrid();
|
||||
@@ -2880,18 +2913,18 @@
|
||||
canvasEl.addEventListener("pointerdown", (e) => {
|
||||
if (!document.body.classList.contains("phone-ui") || e.target.closest(".w")) return;
|
||||
holdTimer2 = setTimeout(() => {
|
||||
document.body.classList.add("peek");
|
||||
clearTimeout(peekTimer);
|
||||
peekTimer = setTimeout(() => document.body.classList.remove("peek"), 10000);
|
||||
peek(true);
|
||||
navigator.vibrate?.(10);
|
||||
}, 600);
|
||||
});
|
||||
for (const ev of ["pointerup", "pointercancel", "pointermove"]) canvasEl.addEventListener(ev, (e) => { if (ev !== "pointermove" || e.movementX || e.movementY) clearTimeout(holdTimer2); });
|
||||
function phoneHint() {
|
||||
if (!document.body.classList.contains("phone-ui") || lsGet("ds.peekHint")) return;
|
||||
lsSet("ds.peekHint", "1");
|
||||
toast(tr("Touch and hold an empty spot to show the menu."));
|
||||
// The round button in the corner does the same, for dashboards without an empty spot.
|
||||
function peek(on) {
|
||||
document.body.classList.toggle("peek", on);
|
||||
clearTimeout(peekTimer);
|
||||
if (on) peekTimer = setTimeout(() => document.body.classList.remove("peek"), 15000);
|
||||
}
|
||||
$("#phone-menu").onclick = () => peek(!document.body.classList.contains("peek"));
|
||||
|
||||
// ------------------------------------------------------------ chrome wiring
|
||||
function setCurrent(id) {
|
||||
@@ -2954,6 +2987,8 @@
|
||||
|
||||
$("#btn-edit").onclick = () => setEdit(!editMode);
|
||||
$("#btn-kiosk").onclick = () => enterKiosk();
|
||||
$("#btn-app").onclick = () => installDialog();
|
||||
$("#btn-app").hidden = IN_APP || standalone();
|
||||
$("#btn-undo").onclick = undo;
|
||||
$("#btn-redo").onclick = redo;
|
||||
$("#dash-select").onchange = (e) => setCurrent(e.target.value);
|
||||
@@ -3200,6 +3235,8 @@
|
||||
$("#btn-redo").innerHTML = Icons.svg("redo");
|
||||
$("#btn-menu").innerHTML = Icons.svg("more");
|
||||
$("#btn-kiosk").insertAdjacentHTML("afterbegin", Icons.svg("kiosk"));
|
||||
$("#btn-app").innerHTML = Icons.svg("phone");
|
||||
$("#phone-menu").innerHTML = Icons.svg("more");
|
||||
document.querySelectorAll("#menu [data-icon]").forEach((b) => b.insertAdjacentHTML("afterbegin", Icons.svg(b.dataset.icon)));
|
||||
}
|
||||
|
||||
@@ -3272,7 +3309,6 @@
|
||||
const exists = (id) => id && store.dashboards.some((d) => d.id === id);
|
||||
setCurrent([hashId, dev?.dashboard, forUser, lsGet("ds.current")].find(exists));
|
||||
if (location.hash.includes("kiosk") || shared.get("ds_kiosk") === "1" || (dev?.kiosk && !hashId)) enterKiosk({ fullscreen: false });
|
||||
setTimeout(phoneHint, 1500);
|
||||
const action = shared.get("ds_action");
|
||||
if (action) { history.replaceState(null, "", location.pathname + location.hash); runShortcut(action); }
|
||||
// After an update, point at what changed (once per device).
|
||||
|
||||
Reference in New Issue
Block a user