custom_components/bahmcloud_store/panel/panel.js aktualisiert
This commit is contained in:
@@ -15,14 +15,14 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
|
||||
this._search = "";
|
||||
this._category = "all";
|
||||
this._provider = "all"; // NEW: provider filter (all|github|gitea|gitlab|other|custom)
|
||||
this._provider = "all"; // all|github|gitea|gitlab|other|custom
|
||||
|
||||
this._detailRepoId = null;
|
||||
this._detailRepo = null;
|
||||
|
||||
this._readmeLoading = false;
|
||||
this._readmeText = null;
|
||||
this._readmeHtml = null; // backend may provide; client renderer does not rely on it
|
||||
this._readmeHtml = null;
|
||||
this._readmeError = null;
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
|
||||
_openRepoDetail(repoId) {
|
||||
const repos = Array.isArray(this._data?.repos) ? this._data.repos : [];
|
||||
const repo = repos.find((r) => r.id === repoId);
|
||||
const repo = repos.find((r) => this._safeId(r?.id) === repoId);
|
||||
if (!repo) return;
|
||||
|
||||
this._view = "detail";
|
||||
@@ -160,7 +160,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
} else {
|
||||
this._readmeText = null;
|
||||
this._readmeHtml = null;
|
||||
this._readmeError = resp?.message || "README not found.";
|
||||
this._readmeError = this._safeText(resp?.message) || "README not found.";
|
||||
}
|
||||
} catch (e) {
|
||||
this._readmeText = null;
|
||||
@@ -340,7 +340,6 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
details{ margin-top: 10px; }
|
||||
summary{ cursor:pointer; color: var(--bcs-accent); font-weight: 900; }
|
||||
|
||||
/* Pretty Markdown container */
|
||||
.md { line-height: 1.65; font-size: 14px; }
|
||||
.md :is(h1,h2,h3){ margin: 18px 0 10px; }
|
||||
.md :is(p,ul,ol,pre,blockquote,table){ margin: 10px 0; }
|
||||
@@ -392,7 +391,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent HA global shortcuts while typing inside the panel
|
||||
// prevent HA global shortcuts while typing
|
||||
const stopIfFormField = (e) => {
|
||||
const t = e.composedPath ? e.composedPath()[0] : e.target;
|
||||
if (!t) return;
|
||||
@@ -401,7 +400,6 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
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);
|
||||
@@ -450,7 +448,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
const subtitle = root.getElementById("subtitle");
|
||||
const fabs = root.getElementById("fabs");
|
||||
|
||||
const v = this._data?.version ? String(this._data.version) : null;
|
||||
const v = this._safeText(this._data?.version);
|
||||
subtitle.textContent = v ? `BCS ${v}` : "BCS — loading…";
|
||||
|
||||
for (const tab of root.querySelectorAll(".tab")) {
|
||||
@@ -502,26 +500,23 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
_renderStore() {
|
||||
const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
|
||||
const categories = this._computeCategories(repos);
|
||||
|
||||
// Provider stats (for overview)
|
||||
const stats = this._computeProviderStats(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();
|
||||
const hay = `${this._safeText(r?.name)} ${this._safeText(r?.description)} ${this._safeText(r?.url)} ${this._safeText(r?.owner)}`.toLowerCase();
|
||||
return hay.includes(q);
|
||||
})
|
||||
.filter((r) => {
|
||||
if (this._category === "all") return true;
|
||||
return (r.category || "").toLowerCase() === this._category;
|
||||
return this._safeLower(r?.category) === this._category;
|
||||
})
|
||||
.filter((r) => {
|
||||
if (this._provider === "all") return true;
|
||||
if (this._provider === "custom") return r.source === "custom";
|
||||
const pv = (r.provider || "other").toLowerCase();
|
||||
return pv === this._provider;
|
||||
if (this._provider === "custom") return r?.source === "custom";
|
||||
return this._safeLower(r?.provider) === this._provider;
|
||||
});
|
||||
|
||||
const options = [
|
||||
@@ -538,24 +533,27 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
|
||||
const rows = filtered
|
||||
.map((r) => {
|
||||
const id = this._safeId(r?.id);
|
||||
const name = this._safeText(r?.name) || "Unnamed repository";
|
||||
const desc = this._safeText(r?.description) || "No description available.";
|
||||
|
||||
const badge =
|
||||
r.source === "custom"
|
||||
r?.source === "custom"
|
||||
? `<span class="badge custom">Custom</span>`
|
||||
: `<span class="badge">Index</span>`;
|
||||
|
||||
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 creator = this._safeText(r?.owner) ? `Creator: ${this._safeText(r?.owner)}` : "Creator: -";
|
||||
const latest = this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: unknown";
|
||||
const cat = this._safeText(r?.category) ? `Category: ${this._safeText(r?.category)}` : null;
|
||||
const metaSrc = this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null;
|
||||
|
||||
const lineBits = [creator, latest, cat, metaSrc].filter(Boolean);
|
||||
|
||||
return `
|
||||
<div class="card clickable" data-repo="${this._esc(r.id)}">
|
||||
<div class="card clickable" data-repo="${this._esc(id)}">
|
||||
<div class="row">
|
||||
<div>
|
||||
<div><strong>${this._esc(r.name)}</strong></div>
|
||||
<div><strong>${this._esc(name)}</strong></div>
|
||||
<div class="muted">${this._esc(desc)}</div>
|
||||
<div class="muted small">${this._esc(lineBits.join(" · "))}</div>
|
||||
</div>
|
||||
@@ -581,6 +579,45 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
`;
|
||||
}
|
||||
|
||||
_wireStore() {
|
||||
const root = this.shadowRoot;
|
||||
const search = root.getElementById("searchInput");
|
||||
const cat = root.getElementById("categorySelect");
|
||||
const chips = root.getElementById("providerChips");
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
if (chips) {
|
||||
for (const c of chips.querySelectorAll("[data-prov]")) {
|
||||
c.addEventListener("click", () => {
|
||||
const key = c.getAttribute("data-prov");
|
||||
if (!key) return;
|
||||
this._provider = key;
|
||||
this._update();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const card of root.querySelectorAll("[data-repo]")) {
|
||||
card.addEventListener("click", () => {
|
||||
const id = card.getAttribute("data-repo");
|
||||
if (id) this._openRepoDetail(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_renderProviderChips(stats) {
|
||||
const order = ["all", "github", "gitea", "gitlab", "other", "custom"];
|
||||
const labels = {
|
||||
@@ -615,9 +652,10 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
|
||||
for (const r of repos) {
|
||||
s.total += 1;
|
||||
if (r.source === "custom") s.custom += 1;
|
||||
|
||||
const p = (r.provider || "other").toLowerCase();
|
||||
if (r?.source === "custom") s.custom += 1;
|
||||
|
||||
const p = this._safeLower(r?.provider) || "other";
|
||||
if (p === "github") s.github += 1;
|
||||
else if (p === "gitea") s.gitea += 1;
|
||||
else if (p === "gitlab") s.gitlab += 1;
|
||||
@@ -626,63 +664,29 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
return s;
|
||||
}
|
||||
|
||||
_wireStore() {
|
||||
const root = this.shadowRoot;
|
||||
const search = root.getElementById("searchInput");
|
||||
const cat = root.getElementById("categorySelect");
|
||||
const chips = root.getElementById("providerChips");
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
if (chips) {
|
||||
for (const c of chips.querySelectorAll("[data-prov]")) {
|
||||
c.addEventListener("click", () => {
|
||||
const key = c.getAttribute("data-prov");
|
||||
if (!key) return;
|
||||
this._provider = key; // set filter
|
||||
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 `<div class="card">Repository not found.</div>`;
|
||||
|
||||
const badge =
|
||||
r.source === "custom" ? `<span class="badge custom">Custom</span>` : `<span class="badge">Index</span>`;
|
||||
const name = this._safeText(r?.name) || "Unnamed repository";
|
||||
const desc = this._safeText(r?.description) || "No description available.";
|
||||
const url = this._safeText(r?.url) || "#";
|
||||
|
||||
const desc = r.description || "No description available.";
|
||||
const latest = r.latest_version ? `Latest: ${r.latest_version}` : "Latest: unknown";
|
||||
const badge =
|
||||
r?.source === "custom"
|
||||
? `<span class="badge custom">Custom</span>`
|
||||
: `<span class="badge">Index</span>`;
|
||||
|
||||
const latest = this._safeText(r?.latest_version) ? `Latest: ${this._safeText(r?.latest_version)}` : "Latest: unknown";
|
||||
|
||||
const infoBits = [
|
||||
r.owner ? `Creator: ${r.owner}` : "Creator: -",
|
||||
this._safeText(r?.owner) ? `Creator: ${this._safeText(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,
|
||||
this._safeText(r?.provider) ? `Provider: ${this._safeText(r?.provider)}` : null,
|
||||
this._safeText(r?.category) ? `Category: ${this._safeText(r?.category)}` : null,
|
||||
this._safeText(r?.meta_author) ? `Author: ${this._safeText(r?.meta_author)}` : null,
|
||||
this._safeText(r?.meta_maintainer) ? `Maintainer: ${this._safeText(r?.meta_maintainer)}` : null,
|
||||
this._safeText(r?.meta_source) ? `Meta: ${this._safeText(r?.meta_source)}` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
const readmeBlock = this._readmeLoading
|
||||
@@ -718,11 +722,11 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div>
|
||||
<div><strong style="font-size:16px;">${this._esc(r.name)}</strong></div>
|
||||
<div><strong style="font-size:16px;">${this._esc(name)}</strong></div>
|
||||
<div class="muted">${this._esc(desc)}</div>
|
||||
<div class="muted small" style="margin-top:8px;">${this._esc(infoBits.join(" · "))}</div>
|
||||
<div class="muted small" style="margin-top:8px;">
|
||||
<a href="${this._esc(r.url)}" target="_blank" rel="noreferrer">Open repository</a>
|
||||
<a href="${this._esc(url)}" target="_blank" rel="noreferrer">Open repository</a>
|
||||
</div>
|
||||
</div>
|
||||
${badge}
|
||||
@@ -754,57 +758,19 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
if (!mount) return;
|
||||
|
||||
if (this._readmeText) {
|
||||
const html = this._mdToHtml(this._readmeText);
|
||||
|
||||
if (html) {
|
||||
mount.innerHTML = html;
|
||||
this._postprocessRenderedMarkdown(mount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Client renderer may be unavailable; prefer server-provided HTML
|
||||
if (this._readmeHtml) {
|
||||
mount.innerHTML = this._readmeHtml;
|
||||
this._postprocessRenderedMarkdown(mount);
|
||||
return;
|
||||
}
|
||||
|
||||
mount.innerHTML = `<div class="muted">Markdown renderer not available on this client. Use “Show raw Markdown”.</div>`;
|
||||
mount.innerHTML = `<div class="muted">Rendered HTML not available. Use “Show raw Markdown”.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
mount.innerHTML = "";
|
||||
}
|
||||
|
||||
_mdToHtml(markdown) {
|
||||
const md = String(markdown || "");
|
||||
if (!md.trim()) return "";
|
||||
|
||||
const markedObj = window.marked;
|
||||
const domPurify = window.DOMPurify;
|
||||
|
||||
let html = "";
|
||||
|
||||
try {
|
||||
if (markedObj && typeof markedObj.parse === "function") {
|
||||
html = markedObj.parse(md, { breaks: true, gfm: true });
|
||||
} else if (typeof markedObj === "function") {
|
||||
html = markedObj(md);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
if (domPurify && typeof domPurify.sanitize === "function") {
|
||||
html = domPurify.sanitize(html);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
_postprocessRenderedMarkdown(container) {
|
||||
if (!container) return;
|
||||
try {
|
||||
@@ -836,18 +802,18 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
const r = this._detailRepo;
|
||||
if (!r) return;
|
||||
|
||||
const url = this._safeText(r?.url);
|
||||
|
||||
const open = root.getElementById("fabOpen");
|
||||
const reload = root.getElementById("fabReload");
|
||||
const info = root.getElementById("fabInfo");
|
||||
|
||||
if (open) open.addEventListener("click", () => window.open(r.url, "_blank", "noreferrer"));
|
||||
|
||||
if (open) open.addEventListener("click", () => url && window.open(url, "_blank", "noreferrer"));
|
||||
if (reload) {
|
||||
reload.addEventListener("click", () => {
|
||||
if (this._detailRepoId) this._loadReadme(this._detailRepoId);
|
||||
});
|
||||
}
|
||||
|
||||
if (info) {
|
||||
info.addEventListener("click", () => {
|
||||
this._view = "about";
|
||||
@@ -858,24 +824,27 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
|
||||
_renderManage() {
|
||||
const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
|
||||
const custom = repos.filter((r) => r.source === "custom");
|
||||
const custom = repos.filter((r) => r?.source === "custom");
|
||||
|
||||
const list = custom
|
||||
.map(
|
||||
(r) => `
|
||||
.map((r) => {
|
||||
const id = this._safeId(r?.id);
|
||||
const name = this._safeText(r?.name) || "Unnamed repository";
|
||||
const url = this._safeText(r?.url) || "";
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div>
|
||||
<div><strong>${this._esc(r.name)}</strong></div>
|
||||
<div class="muted">${this._esc(r.url)}</div>
|
||||
<div><strong>${this._esc(name)}</strong></div>
|
||||
<div class="muted">${this._esc(url)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="primary" data-remove="${this._esc(r.id)}">Remove</button>
|
||||
<button class="primary" data-remove="${this._esc(id)}">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return `
|
||||
@@ -924,13 +893,14 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
}
|
||||
|
||||
_renderAbout() {
|
||||
const v = this._safeText(this._data?.version) || "-";
|
||||
return `
|
||||
<div class="card">
|
||||
<div><strong>Settings / About</strong></div>
|
||||
<div class="muted">Language: English (v1). i18n will be added later.</div>
|
||||
<div class="muted">Theme: follows Home Assistant light/dark automatically.</div>
|
||||
<div class="muted">Accent: Bahmcloud Blue.</div>
|
||||
<div class="muted small" style="margin-top: 10px;">BCS version: ${this._esc(this._data.version || "-")}</div>
|
||||
<div class="muted small" style="margin-top: 10px;">BCS version: ${this._esc(v)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -938,12 +908,31 @@ class BahmcloudStorePanel extends HTMLElement {
|
||||
_computeCategories(repos) {
|
||||
const set = new Set();
|
||||
for (const r of repos) {
|
||||
const c = (r.category || "").trim().toLowerCase();
|
||||
const c = this._safeLower(r?.category);
|
||||
if (c) set.add(c);
|
||||
}
|
||||
return Array.from(set).sort();
|
||||
}
|
||||
|
||||
// --- HARDENING HELPERS (fixes [object Object]) ---
|
||||
_safeText(v) {
|
||||
if (v === null || v === undefined) return "";
|
||||
const t = typeof v;
|
||||
if (t === "string") return v;
|
||||
if (t === "number" || t === "boolean") return String(v);
|
||||
return ""; // objects/arrays/functions => empty (prevents [object Object])
|
||||
}
|
||||
|
||||
_safeLower(v) {
|
||||
const s = this._safeText(v);
|
||||
return s ? s.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
_safeId(v) {
|
||||
const s = this._safeText(v);
|
||||
return s || "";
|
||||
}
|
||||
|
||||
_esc(s) {
|
||||
return String(s ?? "")
|
||||
.replaceAll("&", "&")
|
||||
|
||||
Reference in New Issue
Block a user