From 6f0f588b033c64d2dc2697c7bd92ee7156552f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Bachmann?= Date: Thu, 15 Jan 2026 10:33:11 +0000 Subject: [PATCH] =?UTF-8?q?custom=5Fcomponents/bahmcloud=5Fstore/panel/pan?= =?UTF-8?q?el.js=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bahmcloud_store/panel/panel.js | 602 ++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 custom_components/bahmcloud_store/panel/panel.js diff --git a/custom_components/bahmcloud_store/panel/panel.js b/custom_components/bahmcloud_store/panel/panel.js new file mode 100644 index 0000000..092e27b --- /dev/null +++ b/custom_components/bahmcloud_store/panel/panel.js @@ -0,0 +1,602 @@ +class BahmcloudStorePanel extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + + this._hass = null; + + this._view = "store"; + this._data = null; + this._loading = true; + this._error = null; + + this._customAddUrl = ""; + this._customAddName = ""; + + this._search = ""; + this._category = "all"; + + this._detailRepoId = null; + this._detailRepo = null; + + this._readmeLoading = false; + this._readmeText = null; + this._readmeHtml = null; + this._readmeError = null; + } + + set hass(hass) { + this._hass = hass; + if (!this._rendered) { + this._rendered = true; + this._render(); + this._load(); + } + } + + async _load() { + if (!this._hass) return; + + this._loading = true; + this._error = null; + this._update(); + + try { + const data = await this._hass.callApi("get", "bcs"); + this._data = data; + } catch (e) { + this._error = e?.message ? String(e.message) : String(e); + } finally { + this._loading = false; + this._update(); + } + } + + _isDesktop() { + return window.matchMedia && window.matchMedia("(min-width: 1024px)").matches; + } + + _toggleMenu() { + if (this._isDesktop()) return; + try { + this.dispatchEvent(new Event("hass-toggle-menu", { bubbles: true, composed: true })); + return; + } catch (_) {} + this._error = "Unable to open the sidebar on this client. Use the back button."; + this._update(); + } + + _goBack() { + if (this._view === "detail") { + this._view = "store"; + this._detailRepoId = null; + this._detailRepo = null; + this._readmeText = null; + this._readmeHtml = null; + this._readmeError = null; + this._update(); + return; + } + try { + history.back(); + } catch (_) { + window.location.href = "/"; + } + } + + _openRepoDetail(repoId) { + const repos = Array.isArray(this._data?.repos) ? this._data.repos : []; + const repo = repos.find((r) => r.id === repoId); + if (!repo) return; + + this._view = "detail"; + this._detailRepoId = repoId; + this._detailRepo = repo; + + this._readmeText = null; + this._readmeHtml = null; + this._readmeError = null; + + this._update(); + this._loadReadme(repoId); + } + + async _loadReadme(repoId) { + if (!this._hass) return; + this._readmeLoading = true; + this._readmeError = null; + this._update(); + + try { + const resp = await this._hass.callApi("get", `bcs/readme?repo_id=${encodeURIComponent(repoId)}`); + + if (resp?.ok && typeof resp.readme === "string" && resp.readme.trim()) { + this._readmeText = resp.readme; + this._readmeHtml = typeof resp.html === "string" && resp.html.trim() ? resp.html : null; + } else { + this._readmeText = null; + this._readmeHtml = null; + this._readmeError = resp?.message || "README not found."; + } + } catch (e) { + this._readmeText = null; + this._readmeHtml = null; + this._readmeError = e?.message ? String(e.message) : String(e); + } finally { + this._readmeLoading = false; + this._update(); + } + } + + _render() { + const root = this.shadowRoot; + + root.innerHTML = ` + + +
+
+ +
+
+
Bahmcloud Store
+
BCS — loading…
+
+
+
+ +
+
+ +
+
+
Store
+
Manage repositories
+
Settings / About
+
+ +
+
+
+ `; + + root.getElementById("refreshBtn").addEventListener("click", () => this._load()); + root.getElementById("menuBtn").addEventListener("click", () => this._toggleMenu()); + root.getElementById("backBtn").addEventListener("click", () => this._goBack()); + + for (const tab of root.querySelectorAll(".tab")) { + tab.addEventListener("click", () => { + this._view = tab.getAttribute("data-view"); + this._update(); + }); + } + + // Prevent HA global shortcuts while typing inside panel inputs + const stopIfFormField = (e) => { + const t = e.composedPath ? e.composedPath()[0] : e.target; + if (!t) return; + const tag = (t.tagName || "").toLowerCase(); + if (tag === "input" || tag === "textarea" || tag === "select" || t.isContentEditable) { + e.stopPropagation(); + } + }; + root.addEventListener("keydown", stopIfFormField, true); + root.addEventListener("keyup", stopIfFormField, true); + root.addEventListener("keypress", stopIfFormField, true); + } + + _captureFocusState() { + const root = this.shadowRoot; + const ae = root.activeElement; + if (!ae || !ae.id) return null; + + const supported = new Set(["searchInput", "categorySelect"]); + if (!supported.has(ae.id)) return null; + + return { + id: ae.id, + value: ae.value, + selectionStart: typeof ae.selectionStart === "number" ? ae.selectionStart : null, + selectionEnd: typeof ae.selectionEnd === "number" ? ae.selectionEnd : null, + }; + } + + _restoreFocusState(state) { + if (!state) return; + const root = this.shadowRoot; + const el = root.getElementById(state.id); + if (!el) return; + + try { + el.focus({ preventScroll: true }); + if (state.selectionStart !== null && state.selectionEnd !== null && typeof el.setSelectionRange === "function") { + el.setSelectionRange(state.selectionStart, state.selectionEnd); + } + } catch (_) {} + } + + _update() { + const root = this.shadowRoot; + const focusState = this._captureFocusState(); + + const content = root.getElementById("content"); + const err = root.getElementById("error"); + const subtitle = root.getElementById("subtitle"); + + const v = this._data?.version ? String(this._data.version) : null; + subtitle.textContent = v ? `BCS ${v}` : "BCS — loading…"; + + for (const tab of root.querySelectorAll(".tab")) { + tab.classList.toggle("active", tab.getAttribute("data-view") === this._view); + } + + err.textContent = this._error ? `Error: ${this._error}` : ""; + + if (this._loading) { + content.innerHTML = `
Loading…
`; + this._restoreFocusState(focusState); + return; + } + + if (!this._data) { + content.innerHTML = `
No data.
`; + this._restoreFocusState(focusState); + return; + } + + if (this._view === "store") { + content.innerHTML = this._renderStore(); + this._wireStore(); + this._restoreFocusState(focusState); + return; + } + + if (this._view === "manage") { + content.innerHTML = `
Manage view is unchanged in this patch.
`; + this._restoreFocusState(focusState); + return; + } + + if (this._view === "about") { + content.innerHTML = this._renderAbout(); + this._restoreFocusState(focusState); + return; + } + + content.innerHTML = this._renderDetail(); + this._wireDetail(); + this._restoreFocusState(focusState); + } + + _renderStore() { + const repos = Array.isArray(this._data.repos) ? this._data.repos : []; + const categories = Array.from(new Set(repos.map(r => (r.category || "").toLowerCase()).filter(Boolean))).sort(); + + const filtered = repos + .filter((r) => { + const q = (this._search || "").trim().toLowerCase(); + if (!q) return true; + const hay = `${r.name || ""} ${r.description || ""} ${r.url || ""} ${r.owner || ""}`.toLowerCase(); + return hay.includes(q); + }) + .filter((r) => { + if (this._category === "all") return true; + return (r.category || "").toLowerCase() === this._category; + }); + + const options = [ + ``, + ...categories.map((c) => ``), + ].join(""); + + const rows = filtered.map((r) => { + const badge = r.source === "custom" + ? `Custom` + : `Index`; + + const desc = r.description || "No description available."; + const creator = r.owner ? `Creator: ${r.owner}` : "Creator: -"; + const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown"; + const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null; + + const lineBits = [creator, latest, metaSrc].filter(Boolean); + + return ` +
+
+
+
${this._esc(r.name)}
+
${this._esc(desc)}
+
${this._esc(lineBits.join(" · "))}
+
+ ${badge} +
+
+ `; + }).join(""); + + return ` +
+ + +
+ ${rows || `
No repositories configured.
`} + `; + } + + _wireStore() { + const root = this.shadowRoot; + const search = root.getElementById("searchInput"); + const cat = root.getElementById("categorySelect"); + + if (search) { + search.addEventListener("input", (e) => { + this._search = e.target.value; + this._update(); + }); + } + + if (cat) { + cat.addEventListener("change", (e) => { + this._category = String(e.target.value || "all"); + this._update(); + }); + } + + for (const card of root.querySelectorAll("[data-repo]")) { + card.addEventListener("click", () => { + const id = card.getAttribute("data-repo"); + if (id) this._openRepoDetail(id); + }); + } + } + + _renderDetail() { + const r = this._detailRepo; + if (!r) return `
Repository not found.
`; + + const badge = r.source === "custom" + ? `Custom` + : `Index`; + + const desc = r.description || "No description available."; + const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown"; + + const infoBits = [ + r.owner ? `Creator: ${r.owner}` : "Creator: -", + latest, + r.provider ? `Provider: ${r.provider}` : null, + r.meta_source ? `Meta: ${r.meta_source}` : null, + ].filter(Boolean); + + const readmeBlock = this._readmeLoading + ? `
Loading README…
` + : this._readmeText + ? ` +
+
README
+
+ +
+ Show raw Markdown +
+
${this._esc(this._readmeText)}
+
+
+
+ ` + : ` +
+
README
+
${this._esc(this._readmeError || "README not found.")}
+
+ `; + + return ` +
+
+
+
+
+
${this._esc(r.name)}
+
${this._esc(desc)}
+
${this._esc(infoBits.join(" · "))}
+ +
+ ${badge} +
+
+ + ${readmeBlock} +
+ +
+
+
Installation & Updates
+
Buttons will be enabled in a later version.
+
+
+
+ `; + } + + _wireDetail() { + const root = this.shadowRoot; + const pretty = root.getElementById("readmePretty"); + if (!pretty) return; + + if (this._readmeHtml) { + pretty.innerHTML = this._readmeHtml; + } else if (this._readmeText) { + // If backend could not render HTML, show a friendly fallback text + pretty.innerHTML = `
Markdown rendering is not available. Please use "Show raw Markdown".
`; + } + } + + _renderAbout() { + return ` +
+
Settings / About
+
BCS version: ${this._esc(this._data.version || "-")}
+
+ `; + } + + _esc(s) { + return String(s ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + } +} + +customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);