From 225442f549bddd0e8fc9be1730b8bc5d37c75684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Bachmann?= Date: Thu, 15 Jan 2026 10:13:49 +0000 Subject: [PATCH] =?UTF-8?q?custom=5Fcomponents/bahmcloud=5Fstore/panel/pan?= =?UTF-8?q?el.js=20gel=C3=B6scht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bahmcloud_store/panel/panel.js | 876 ------------------ 1 file changed, 876 deletions(-) delete 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 deleted file mode 100644 index 1c7cc29..0000000 --- a/custom_components/bahmcloud_store/panel/panel.js +++ /dev/null @@ -1,876 +0,0 @@ -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._readmeError = null; - - this._mdMountToken = 0; - } - - 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 { - const ev = new Event("hass-toggle-menu", { bubbles: true, composed: true }); - this.dispatchEvent(ev); - return; - } catch (_) {} - - try { - const host = this.shadowRoot?.host; - if (host) { - const ev2 = new Event("hass-toggle-menu", { bubbles: true, composed: true }); - host.dispatchEvent(ev2); - 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._readmeError = null; - this._update(); - return; - } - - try { - history.back(); - } catch (_) { - window.location.href = "/"; - } - } - - async _addCustomRepo() { - if (!this._hass) return; - - const url = (this._customAddUrl || "").trim(); - const name = (this._customAddName || "").trim() || null; - - if (!url) { - this._error = "Please enter a repository URL."; - this._update(); - return; - } - - this._error = null; - this._update(); - - try { - await this._hass.callApi("post", "bcs", { op: "add_custom_repo", url, name }); - this._customAddUrl = ""; - this._customAddName = ""; - await this._load(); - this._view = "manage"; - this._update(); - } catch (e) { - this._error = e?.message ? String(e.message) : String(e); - this._update(); - } - } - - async _removeCustomRepo(id) { - if (!this._hass) return; - - try { - await this._hass.callApi("delete", `bcs/custom_repo?id=${encodeURIComponent(id)}`); - await this._load(); - this._view = "manage"; - this._update(); - } catch (e) { - this._error = e?.message ? String(e.message) : String(e); - this._update(); - } - } - - _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._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; - } else { - this._readmeText = null; - this._readmeError = resp?.message || "README not found."; - } - } catch (e) { - this._readmeText = 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(); - const isEditable = - tag === "input" || - tag === "textarea" || - tag === "select" || - t.isContentEditable; - - if (isEditable) 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", "addUrl", "addName"]); - 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 fabs = root.getElementById("fabs"); - - 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}` : ""; - - fabs.innerHTML = this._view === "detail" ? this._renderFabs() : ""; - this._wireFabs(); - - 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 = this._renderManage(); - this._wireManage(); - 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 = this._computeCategories(repos); - - 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 cat = r.category ? `Category: ${r.category}` : null; - const metaSrc = r.meta_source ? `Meta: ${r.meta_source}` : null; - - const lineBits = [creator, latest, cat, 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.category ? `Category: ${r.category}` : null, - r.meta_author ? `Author: ${r.meta_author}` : null, - r.meta_maintainer ? `Maintainer: ${r.meta_maintainer}` : 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
-
- Installation & updates will be implemented via the official Home Assistant APIs. - In v0.4.0, the buttons are UI-only (coming soon). -
-
- Updates remain manual (like HACS). -
-
-
-
- `; - } - - async _mountPrettyMarkdown(container, markdown) { - const token = ++this._mdMountToken; - container.innerHTML = ""; - - const stillValid = () => - token === this._mdMountToken && - this._view === "detail" && - typeof this._readmeText === "string" && - this._readmeText.trim().length > 0; - - // 1) BEST: Use Lovelace markdown card (hui-markdown-card) - try { - await customElements.whenDefined("hui-markdown-card"); - if (!stillValid()) return; - - const wrap = document.createElement("div"); - wrap.className = "mdwrap"; - container.appendChild(wrap); - - const card = document.createElement("hui-markdown-card"); - wrap.appendChild(card); - - // Lovelace cards use setConfig + hass - if (typeof card.setConfig === "function") { - card.setConfig({ - type: "markdown", - content: markdown, - }); - } else { - // fallback property name (just in case) - card.config = { type: "markdown", content: markdown }; - } - - // Provide hass after config - try { card.hass = this._hass; } catch (_) {} - - // Nudge a render pass - await new Promise((resolve) => requestAnimationFrame(resolve)); - if (!stillValid()) return; - try { card.hass = this._hass; } catch (_) {} - - return; - } catch (_) { - // Continue to next fallback - } - - // 2) Fallback: ha-markdown - try { - await customElements.whenDefined("ha-markdown"); - if (!stillValid()) return; - - const el = document.createElement("ha-markdown"); - container.appendChild(el); - try { el.hass = this._hass; } catch (_) {} - try { el.content = markdown; } catch (_) { - try { el.markdown = markdown; } catch (_) {} - } - return; - } catch (_) {} - - // 3) Last fallback: plain text - const pre = document.createElement("pre"); - pre.className = "readme"; - pre.textContent = markdown; - container.appendChild(pre); - } - - _wireDetail() { - const root = this.shadowRoot; - const container = root.getElementById("readmeContainer"); - if (!container) return; - - container.innerHTML = ""; - if (!this._readmeText) return; - - this._mountPrettyMarkdown(container, this._readmeText); - } - - _renderFabs() { - const r = this._detailRepo; - if (!r) return ""; - - return ` -
-
-
-
-
-
- `; - } - - _wireFabs() { - const root = this.shadowRoot; - const r = this._detailRepo; - if (!r) return; - - const open = root.getElementById("fabOpen"); - const reload = root.getElementById("fabReload"); - - if (open) { - open.addEventListener("click", () => { - window.open(r.url, "_blank", "noreferrer"); - }); - } - - if (reload) { - reload.addEventListener("click", () => { - if (this._detailRepoId) this._loadReadme(this._detailRepoId); - }); - } - } - - _renderManage() { - const repos = Array.isArray(this._data.repos) ? this._data.repos : []; - const custom = repos.filter((r) => r.source === "custom"); - - const list = custom.map((r) => { - return ` -
-
-
-
${this._esc(r.name)}
-
${this._esc(r.url)}
-
-
- -
-
-
- `; - }).join(""); - - return ` -
-
Manage repositories
-
Add public repositories from any git provider.
- -
-
-
Repository URL
- -
- -
-
Display name (optional)
- -
- -
- -
-
-
- - ${list || `
No custom repositories added yet.
`} - `; - } - - _wireManage() { - const root = this.shadowRoot; - - const addUrl = root.getElementById("addUrl"); - const addName = root.getElementById("addName"); - const addBtn = root.getElementById("addBtn"); - - if (addUrl) addUrl.addEventListener("input", (e) => (this._customAddUrl = e.target.value)); - if (addName) addName.addEventListener("input", (e) => (this._customAddName = e.target.value)); - if (addBtn) addBtn.addEventListener("click", () => this._addCustomRepo()); - - for (const btn of root.querySelectorAll("[data-remove]")) { - btn.addEventListener("click", () => { - const id = btn.getAttribute("data-remove"); - if (id) this._removeCustomRepo(id); - }); - } - } - - _renderAbout() { - return ` -
-
Settings / About
-
Language: English (v1). i18n will be added later.
-
Theme: follows Home Assistant light/dark automatically.
-
Accent: Bahmcloud Blue.
-
BCS version: ${this._esc(this._data.version || "-")}
-
- `; - } - - _computeCategories(repos) { - const set = new Set(); - for (const r of repos) { - const c = (r.category || "").trim().toLowerCase(); - if (c) set.add(c); - } - return Array.from(set).sort(); - } - - _esc(s) { - return String(s ?? "") - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); - } -} - -customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);