.
This commit is contained in:
@@ -15,6 +15,7 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
|
|
||||||
this._search = "";
|
this._search = "";
|
||||||
this._category = "all";
|
this._category = "all";
|
||||||
|
this._provider = "all"; // NEW: provider filter (all|github|gitea|gitlab|other|custom)
|
||||||
|
|
||||||
this._detailRepoId = null;
|
this._detailRepoId = null;
|
||||||
this._detailRepo = null;
|
this._detailRepo = null;
|
||||||
@@ -268,12 +269,20 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
.grid2{ display:grid; grid-template-columns: 1fr; gap: 12px; }
|
.grid2{ display:grid; grid-template-columns: 1fr; gap: 12px; }
|
||||||
@media (min-width: 900px){ .grid2{ grid-template-columns: 1.2fr 0.8fr; } }
|
@media (min-width: 900px){ .grid2{ grid-template-columns: 1.2fr 0.8fr; } }
|
||||||
|
|
||||||
.filters{
|
.filters{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:12px; }
|
||||||
display:flex;
|
.chips{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px; }
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
.chip{
|
||||||
align-items: center;
|
display:inline-flex; align-items:center; gap:8px;
|
||||||
margin-bottom: 12px;
|
padding:6px 10px; border-radius:999px;
|
||||||
|
border:1px solid var(--divider-color);
|
||||||
|
background:var(--card-background-color);
|
||||||
|
cursor:pointer; user-select:none; font-weight:800; font-size:12px;
|
||||||
|
}
|
||||||
|
.chip strong{ font-size:12px; }
|
||||||
|
.chip.active{
|
||||||
|
border-color:var(--bcs-accent);
|
||||||
|
box-shadow:0 0 0 2px color-mix(in srgb, var(--bcs-accent) 18%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
input, select{
|
input, select{
|
||||||
@@ -494,6 +503,9 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
|
const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
|
||||||
const categories = this._computeCategories(repos);
|
const categories = this._computeCategories(repos);
|
||||||
|
|
||||||
|
// Provider stats (for overview)
|
||||||
|
const stats = this._computeProviderStats(repos);
|
||||||
|
|
||||||
const filtered = repos
|
const filtered = repos
|
||||||
.filter((r) => {
|
.filter((r) => {
|
||||||
const q = (this._search || "").trim().toLowerCase();
|
const q = (this._search || "").trim().toLowerCase();
|
||||||
@@ -504,6 +516,12 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
.filter((r) => {
|
.filter((r) => {
|
||||||
if (this._category === "all") return true;
|
if (this._category === "all") return true;
|
||||||
return (r.category || "").toLowerCase() === this._category;
|
return (r.category || "").toLowerCase() === 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;
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
@@ -516,6 +534,8 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
),
|
),
|
||||||
].join("");
|
].join("");
|
||||||
|
|
||||||
|
const providerChips = this._renderProviderChips(stats);
|
||||||
|
|
||||||
const rows = filtered
|
const rows = filtered
|
||||||
.map((r) => {
|
.map((r) => {
|
||||||
const badge =
|
const badge =
|
||||||
@@ -547,19 +567,70 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
<div class="card">
|
||||||
|
<div><strong>Providers</strong></div>
|
||||||
|
<div class="chips" id="providerChips">${providerChips}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<input id="searchInput" placeholder="Search repositories…" value="${this._esc(this._search)}" />
|
<input id="searchInput" placeholder="Search repositories…" value="${this._esc(this._search)}" />
|
||||||
<select id="categorySelect">${options}</select>
|
<select id="categorySelect">${options}</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${rows || `<div class="card">No repositories configured.</div>`}
|
${rows || `<div class="card">No repositories match this filter.</div>`}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderProviderChips(stats) {
|
||||||
|
const order = ["all", "github", "gitea", "gitlab", "other", "custom"];
|
||||||
|
const labels = {
|
||||||
|
all: "All",
|
||||||
|
github: "GitHub",
|
||||||
|
gitea: "Gitea",
|
||||||
|
gitlab: "GitLab",
|
||||||
|
other: "Other",
|
||||||
|
custom: "Custom",
|
||||||
|
};
|
||||||
|
const values = {
|
||||||
|
all: stats.total,
|
||||||
|
github: stats.github,
|
||||||
|
gitea: stats.gitea,
|
||||||
|
gitlab: stats.gitlab,
|
||||||
|
other: stats.other,
|
||||||
|
custom: stats.custom,
|
||||||
|
};
|
||||||
|
|
||||||
|
return order
|
||||||
|
.map((key) => {
|
||||||
|
const count = values[key] ?? 0;
|
||||||
|
const active = this._provider === key ? " active" : "";
|
||||||
|
return `<div class="chip${active}" data-prov="${key}"><strong>${labels[key]}</strong> ${count}</div>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeProviderStats(repos) {
|
||||||
|
const s = { total: 0, github: 0, gitea: 0, gitlab: 0, other: 0, custom: 0 };
|
||||||
|
if (!Array.isArray(repos)) return s;
|
||||||
|
|
||||||
|
for (const r of repos) {
|
||||||
|
s.total += 1;
|
||||||
|
if (r.source === "custom") s.custom += 1;
|
||||||
|
|
||||||
|
const p = (r.provider || "other").toLowerCase();
|
||||||
|
if (p === "github") s.github += 1;
|
||||||
|
else if (p === "gitea") s.gitea += 1;
|
||||||
|
else if (p === "gitlab") s.gitlab += 1;
|
||||||
|
else s.other += 1;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
_wireStore() {
|
_wireStore() {
|
||||||
const root = this.shadowRoot;
|
const root = this.shadowRoot;
|
||||||
const search = root.getElementById("searchInput");
|
const search = root.getElementById("searchInput");
|
||||||
const cat = root.getElementById("categorySelect");
|
const cat = root.getElementById("categorySelect");
|
||||||
|
const chips = root.getElementById("providerChips");
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
search.addEventListener("input", (e) => {
|
search.addEventListener("input", (e) => {
|
||||||
@@ -575,6 +646,17 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]")) {
|
for (const card of root.querySelectorAll("[data-repo]")) {
|
||||||
card.addEventListener("click", () => {
|
card.addEventListener("click", () => {
|
||||||
const id = card.getAttribute("data-repo");
|
const id = card.getAttribute("data-repo");
|
||||||
@@ -666,18 +748,43 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_wireDetail() {
|
||||||
|
const root = this.shadowRoot;
|
||||||
|
const mount = root.getElementById("readmePretty");
|
||||||
|
if (!mount) return;
|
||||||
|
|
||||||
|
if (this._readmeText) {
|
||||||
|
const html = this._mdToHtml(this._readmeText);
|
||||||
|
|
||||||
|
if (html) {
|
||||||
|
mount.innerHTML = html;
|
||||||
|
this._postprocessRenderedMarkdown(mount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
_mdToHtml(markdown) {
|
_mdToHtml(markdown) {
|
||||||
const md = String(markdown || "");
|
const md = String(markdown || "");
|
||||||
if (!md.trim()) return "";
|
if (!md.trim()) return "";
|
||||||
|
|
||||||
// Prefer HA's frontend libs if present
|
|
||||||
const markedObj = window.marked;
|
const markedObj = window.marked;
|
||||||
const domPurify = window.DOMPurify;
|
const domPurify = window.DOMPurify;
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// marked can be a function or an object with parse()
|
|
||||||
if (markedObj && typeof markedObj.parse === "function") {
|
if (markedObj && typeof markedObj.parse === "function") {
|
||||||
html = markedObj.parse(md, { breaks: true, gfm: true });
|
html = markedObj.parse(md, { breaks: true, gfm: true });
|
||||||
} else if (typeof markedObj === "function") {
|
} else if (typeof markedObj === "function") {
|
||||||
@@ -700,8 +807,6 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
|
|
||||||
_postprocessRenderedMarkdown(container) {
|
_postprocessRenderedMarkdown(container) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
// Make links open in new tab
|
|
||||||
try {
|
try {
|
||||||
const links = container.querySelectorAll("a[href]");
|
const links = container.querySelectorAll("a[href]");
|
||||||
links.forEach((a) => {
|
links.forEach((a) => {
|
||||||
@@ -711,36 +816,6 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
_wireDetail() {
|
|
||||||
const root = this.shadowRoot;
|
|
||||||
const mount = root.getElementById("readmePretty");
|
|
||||||
if (!mount) return;
|
|
||||||
|
|
||||||
if (this._readmeText) {
|
|
||||||
const html = this._mdToHtml(this._readmeText);
|
|
||||||
|
|
||||||
if (html) {
|
|
||||||
mount.innerHTML = html;
|
|
||||||
this._postprocessRenderedMarkdown(mount);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: if backend provided sanitized HTML, use it
|
|
||||||
if (this._readmeHtml) {
|
|
||||||
mount.innerHTML = this._readmeHtml;
|
|
||||||
this._postprocessRenderedMarkdown(mount);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort
|
|
||||||
mount.innerHTML = `<div class="muted">Markdown renderer not available on this client. Use “Show raw Markdown”.</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No readme text loaded
|
|
||||||
mount.innerHTML = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderFabs() {
|
_renderFabs() {
|
||||||
const r = this._detailRepo;
|
const r = this._detailRepo;
|
||||||
if (!r) return "";
|
if (!r) return "";
|
||||||
@@ -879,4 +954,4 @@ class BahmcloudStorePanel extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);
|
customElements.define("bahmcloud-store-panel", BahmcloudStorePanel);
|
||||||
Reference in New Issue
Block a user