custom_components/bahmcloud_store/panel/panel.js aktualisiert

This commit is contained in:
2026-01-15 06:12:34 +00:00
parent 3aee3886b1
commit 124693e545

View File

@@ -1,24 +1,472 @@
class BahmcloudStorePanel extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this._hass = null;
this._view = "store"; // store | manage | about
this._data = null;
this._loading = true;
this._error = null;
this._customAddUrl = "";
this._customAddName = "";
}
set hass(hass) {
if (this._rendered) return;
this._rendered = true;
this._hass = hass;
if (!this._rendered) {
this._rendered = true;
this._render();
this._load();
}
}
const root = this.attachShadow({ mode: "open" });
const iframe = document.createElement("iframe");
async _load() {
if (!this._hass) return;
iframe.src = "/api/bahmcloud_store_static/index.html";
iframe.style.width = "100%";
iframe.style.height = "100%";
iframe.style.border = "0";
iframe.style.display = "block";
this._loading = true;
this._error = null;
this._update();
const style = document.createElement("style");
style.textContent = `
:host { display: block; height: 100vh; }
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();
}
}
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();
}
}
_render() {
const root = this.shadowRoot;
root.innerHTML = `
<style>
:host {
display: block;
--bcs-accent: #1E88E5; /* Bahmcloud Blue (v1 default) */
}
.wrap {
padding: 16px;
max-width: 1100px;
margin: 0 auto;
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
color: var(--primary-text-color);
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.brand {
display: flex;
align-items: baseline;
gap: 10px;
}
.title {
font-size: 22px;
font-weight: 800;
letter-spacing: 0.2px;
}
.subtitle {
color: var(--secondary-text-color);
font-size: 13px;
margin-top: 2px;
}
.tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.tab {
border: 1px solid var(--divider-color);
background: var(--card-background-color);
color: var(--primary-text-color);
padding: 8px 12px;
border-radius: 999px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
}
.tab.active {
border-color: var(--bcs-accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bcs-accent) 20%, transparent);
}
.actions {
display: flex;
gap: 8px;
align-items: center;
}
button {
padding: 9px 12px;
border-radius: 12px;
border: 1px solid var(--divider-color);
background: var(--card-background-color);
color: var(--primary-text-color);
cursor: pointer;
font-weight: 700;
}
button.primary {
border-color: var(--bcs-accent);
background: color-mix(in srgb, var(--bcs-accent) 16%, var(--card-background-color));
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.card {
border: 1px solid var(--divider-color);
background: var(--card-background-color);
border-radius: 16px;
padding: 12px;
margin: 10px 0;
}
.row {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: flex-start;
}
.muted {
color: var(--secondary-text-color);
font-size: 13px;
margin-top: 4px;
}
.badge {
border: 1px solid var(--divider-color);
border-radius: 999px;
padding: 2px 10px;
font-size: 12px;
font-weight: 700;
height: fit-content;
}
.badge.custom {
border-color: var(--bcs-accent);
color: var(--bcs-accent);
}
.error {
color: #b00020;
white-space: pre-wrap;
margin-top: 10px;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.field {
display: grid;
gap: 6px;
}
input {
padding: 10px 12px;
border-radius: 12px;
border: 1px solid var(--divider-color);
background: var(--card-background-color);
color: var(--primary-text-color);
outline: none;
}
input:focus {
border-color: var(--bcs-accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bcs-accent) 20%, transparent);
}
.small {
font-size: 12px;
}
a {
color: var(--bcs-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
<div class="wrap">
<div class="topbar">
<div class="brand">
<div>
<div class="title">Bahmcloud Store</div>
<div class="subtitle">BCS 0.2.0 — foundation build</div>
</div>
</div>
<div class="actions">
<button id="refreshBtn">Refresh</button>
</div>
</div>
<div class="tabs">
<div class="tab" data-view="store">Store</div>
<div class="tab" data-view="manage">Manage repositories</div>
<div class="tab" data-view="about">Settings / About</div>
</div>
<div id="content"></div>
<div id="error" class="error"></div>
</div>
`;
root.appendChild(style);
root.appendChild(iframe);
root.getElementById("refreshBtn").addEventListener("click", () => this._load());
for (const tab of root.querySelectorAll(".tab")) {
tab.addEventListener("click", () => {
this._view = tab.getAttribute("data-view");
this._update();
});
}
}
_update() {
const root = this.shadowRoot;
const content = root.getElementById("content");
const err = root.getElementById("error");
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 = `<div class="card">Loading…</div>`;
return;
}
if (!this._data) {
content.innerHTML = `<div class="card">No data.</div>`;
return;
}
if (this._view === "store") {
content.innerHTML = this._renderStore();
return;
}
if (this._view === "manage") {
content.innerHTML = this._renderManage();
this._wireManage();
return;
}
content.innerHTML = this._renderAbout();
}
_renderStore() {
const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
const rows = repos.map((r) => {
const badge = r.source === "custom"
? `<span class="badge custom">Custom</span>`
: `<span class="badge">Index</span>`;
const owner = r.owner ? `Owner: ${this._esc(r.owner)}` : "Owner: -";
const provider = r.provider ? `Provider: ${this._esc(r.provider)}` : "Provider: -";
return `
<div class="card">
<div class="row">
<div>
<div><strong>${this._esc(r.name)}</strong></div>
<div class="muted">${this._esc(r.description || "Description will be loaded in a later version.")}</div>
<div class="muted small">${owner} · ${provider}</div>
<div class="muted small"><a href="${this._esc(r.url)}" target="_blank" rel="noreferrer">Open repository</a></div>
</div>
${badge}
</div>
</div>
`;
}).join("");
return `
<div class="card">
<div><strong>Store</strong></div>
<div class="muted small">Index URL: ${this._esc(this._data.store_url || "-")}</div>
<div class="muted small">Refresh seconds: ${this._esc(String(this._data.refresh_seconds || "-"))}</div>
</div>
${rows || `<div class="card">No repositories configured.</div>`}
`;
}
_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 `
<div class="card">
<div class="row">
<div>
<div><strong>${this._esc(r.name)}</strong></div>
<div class="muted">${this._esc(r.url)}</div>
</div>
<div class="actions">
<button class="primary" data-remove="${this._esc(r.id)}">Remove</button>
</div>
</div>
</div>
`;
}).join("");
return `
<div class="card">
<div><strong>Manage repositories</strong></div>
<div class="muted">Add public repositories from any git provider.</div>
<div class="grid" style="margin-top: 12px;">
<div class="field">
<label class="muted small">Repository URL</label>
<input id="addUrl" placeholder="https://github.com/user/repo" value="${this._esc(this._customAddUrl)}" />
</div>
<div class="field">
<label class="muted small">Display name (optional)</label>
<input id="addName" placeholder="My Integration" value="${this._esc(this._customAddName)}" />
</div>
<div class="actions">
<button id="addBtn" class="primary">Add repository</button>
</div>
</div>
</div>
${list || `<div class="card">No custom repositories added yet.</div>`}
`;
}
_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 `
<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>
<div class="card">
<div><strong>Roadmap</strong></div>
<div class="muted">
Next versions will add: repo metadata (bcs.yaml / hacs.*), README view, install/uninstall, update entities.
</div>
</div>
`;
}
_esc(s) {
return String(s ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
}