diff --git a/custom_components/bahmcloud_store/panel/app.js b/custom_components/bahmcloud_store/panel/app.js index 75147d7..412bf0e 100644 --- a/custom_components/bahmcloud_store/panel/app.js +++ b/custom_components/bahmcloud_store/panel/app.js @@ -26,6 +26,11 @@ class BahmcloudStorePanel extends HTMLElement { this._readmeError = null; this._refreshing = false; + + this._installingRepoId = null; + this._updatingRepoId = null; + this._restartRequired = false; + this._lastActionMsg = null; } set hass(hass) { @@ -47,6 +52,12 @@ class BahmcloudStorePanel extends HTMLElement { try { const data = await this._hass.callApi("get", "bcs"); this._data = data; + + // keep detail repo fresh (installed state, versions, etc.) + if (this._view === "detail" && this._detailRepoId && Array.isArray(data?.repos)) { + const fresh = data.repos.find((r) => this._safeId(r?.id) === this._detailRepoId); + if (fresh) this._detailRepo = fresh; + } } catch (e) { this._error = e?.message ? String(e.message) : String(e); } finally { @@ -83,6 +94,68 @@ class BahmcloudStorePanel extends HTMLElement { await this._load(); } + async _installRepo(repoId) { + if (!this._hass) return; + if (!repoId) return; + if (this._installingRepoId || this._updatingRepoId) return; + + this._installingRepoId = repoId; + this._error = null; + this._lastActionMsg = null; + this._update(); + + try { + const resp = await this._hass.callApi("post", `bcs/install?repo_id=${encodeURIComponent(repoId)}`, {}); + if (!resp?.ok) { + this._error = this._safeText(resp?.message) || "Install failed."; + } else { + this._restartRequired = !!resp.restart_required; + this._lastActionMsg = "Installation finished. Restart required."; + } + } catch (e) { + this._error = e?.message ? String(e.message) : String(e); + } finally { + this._installingRepoId = null; + await this._load(); + } + } + + async _updateRepo(repoId) { + if (!this._hass) return; + if (!repoId) return; + if (this._installingRepoId || this._updatingRepoId) return; + + this._updatingRepoId = repoId; + this._error = null; + this._lastActionMsg = null; + this._update(); + + try { + const resp = await this._hass.callApi("post", `bcs/update?repo_id=${encodeURIComponent(repoId)}`, {}); + if (!resp?.ok) { + this._error = this._safeText(resp?.message) || "Update failed."; + } else { + this._restartRequired = !!resp.restart_required; + this._lastActionMsg = "Update finished. Restart required."; + } + } catch (e) { + this._error = e?.message ? String(e.message) : String(e); + } finally { + this._updatingRepoId = null; + await this._load(); + } + } + + async _restartHA() { + if (!this._hass) return; + try { + await this._hass.callApi("post", "bcs/restart", {}); + } catch (e) { + this._error = e?.message ? String(e.message) : String(e); + this._update(); + } + } + _isDesktop() { return window.matchMedia && window.matchMedia("(min-width: 1024px)").matches; } @@ -218,385 +291,355 @@ class BahmcloudStorePanel extends HTMLElement { .iconbtn{ width:40px; height:40px; border-radius:14px; + display:flex; align-items:center; justify-content:center; border:1px solid var(--divider-color); - background: color-mix(in srgb, var(--card-background-color) 82%, transparent); - color:inherit; display:inline-flex; align-items:center; justify-content:center; - cursor:pointer; user-select:none; font-weight:900; font-size:18px; line-height:1; + background: var(--card-background-color); + cursor:pointer; user-select:none; } - .iconbtn:hover{ box-shadow: 0 10px 30px rgba(0,0,0,0.10); transform: translateY(-1px); } - .iconbtn:active{ transform: translateY(0px); box-shadow:none; } - @media (min-width: 1024px) { .iconbtn.menu { display:none; } } - - .brandtitle{ display:flex; flex-direction:column; line-height:1.2; } - .brandtitle .t{ font-size:16px; font-weight:900; letter-spacing: .2px; } - .brandtitle .s{ font-size:12px; color:var(--secondary-text-color); margin-top:2px; } + .iconbtn:hover{ filter:brightness(0.98); } .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); + max-width:1200px; margin:0 auto; padding:16px; } - .tabs{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px; } + .tabs{ + display:flex; gap:10px; flex-wrap:wrap; + margin:8px 0 16px; + } .tab{ + padding:10px 14px; border-radius:999px; 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:800; font-size:13px; + cursor:pointer; user-select:none; + background: var(--card-background-color); } - .tab.active{ - border-color:var(--bcs-accent); - box-shadow:0 0 0 2px color-mix(in srgb, var(--bcs-accent) 20%, transparent); + .tab.active{ border-color: var(--bcs-accent); box-shadow: 0 0 0 2px rgba(30,136,229,.15); } + + .grid{ + display:grid; gap:12px; + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + @media (min-width: 900px){ + .grid{ grid-template-columns: repeat(2, minmax(0, 1fr)); } } - button{ - padding:10px 12px; border-radius:14px; - border:1px solid var(--divider-color); - background:var(--card-background-color); - color:var(--primary-text-color); - cursor:pointer; font-weight:900; + .grid2{ + display:grid; gap:12px; + grid-template-columns: 1fr; } - button.primary{ - border-color:var(--bcs-accent); - background: color-mix(in srgb, var(--bcs-accent) 16%, var(--card-background-color)); + @media (min-width: 1024px){ + .grid2{ grid-template-columns: 1.2fr .8fr; } } - button:hover{ box-shadow: 0 10px 30px rgba(0,0,0,0.10); transform: translateY(-1px); } - button:active{ transform: translateY(0px); box-shadow:none; } - button:disabled{ opacity: 0.55; cursor: not-allowed; } .card{ + padding:14px 14px; + border-radius:18px; + background: var(--card-background-color); border:1px solid var(--divider-color); - background:var(--card-background-color); - border-radius:18px; padding:12px; margin:10px 0; + box-shadow: 0 1px 0 rgba(0,0,0,.04); } - .card.clickable{ - cursor:pointer; - transition: transform 120ms ease, box-shadow 120ms ease; - } - .card.clickable:hover{ - transform: translateY(-1px); - box-shadow: 0 12px 34px rgba(0,0,0,0.10); - } - - .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; } - .small{ font-size:12px; } - + .row{ display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } + .muted{ color: var(--secondary-text-color); } + .small{ font-size: 12px; } .badge{ + padding:6px 10px; + border-radius:999px; border:1px solid var(--divider-color); - border-radius:999px; padding:2px 10px; - font-size:12px; font-weight:900; height:fit-content; - } - .badge.custom{ border-color:var(--bcs-accent); color:var(--bcs-accent); } - - .error{ color:#b00020; white-space:pre-wrap; margin-top:10px; } - - .grid2{ display:grid; grid-template-columns: 1fr; gap: 12px; } - @media (min-width: 900px){ .grid2{ grid-template-columns: 1.2fr 0.8fr; } } - - .filters{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:12px; } - .chips{ display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px; } - - .chip{ - display:inline-flex; align-items:center; gap:8px; - 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); + background: rgba(30,136,229,.06); + color: var(--primary-text-color); + font-size: 12px; + white-space:nowrap; } + .filters{ display:flex; gap:10px; flex-wrap:wrap; margin-bottom:12px; } input, select{ - padding:10px 12px; border-radius:14px; + padding:10px 12px; + border-radius:14px; border:1px solid var(--divider-color); - background:var(--card-background-color); - color:var(--primary-text-color); + background: var(--card-background-color); + color: var(--primary-text-color); outline:none; } input:focus, select:focus{ - border-color:var(--bcs-accent); - box-shadow:0 0 0 2px color-mix(in srgb, var(--bcs-accent) 20%, transparent); + border-color: var(--bcs-accent); + box-shadow: 0 0 0 2px rgba(30,136,229,.15); } - a{ color:var(--bcs-accent); text-decoration:none; } - a:hover{ text-decoration:underline; } + button{ + padding:10px 12px; + border-radius:14px; + border:1px solid var(--divider-color); + background: var(--card-background-color); + color: var(--primary-text-color); + cursor:pointer; + } + button.primary{ + border-color: rgba(30,136,229,.35); + background: rgba(30,136,229,.08); + } + button:disabled{ + opacity: .55; + cursor: not-allowed; + } + + .err{ + margin:12px 0; + padding:12px 14px; + border-radius:14px; + border:1px solid rgba(255, 82, 82, .35); + background: rgba(255, 82, 82, .08); + } .fabs{ position: fixed; - right: 18px; - bottom: 18px; - display: grid; - gap: 10px; - z-index: 100; + right: 16px; + bottom: 16px; + display:flex; + flex-direction:column; + gap:10px; + z-index: 60; } .fab{ - width: 56px; - height: 56px; - border-radius: 18px; - border: 1px solid var(--divider-color); + width:54px; height:54px; + border-radius:18px; + border:1px solid var(--divider-color); background: var(--card-background-color); - box-shadow: 0 12px 30px rgba(0,0,0,0.14); - display: inline-flex; - align-items: center; - justify-content: center; + display:flex; align-items:center; justify-content:center; + cursor:pointer; + box-shadow: 0 8px 18px rgba(0,0,0,.12); + user-select:none; font-size: 18px; - font-weight: 900; - cursor: pointer; - user-select: none; } .fab.primary{ - border-color: var(--bcs-accent); - background: color-mix(in srgb, var(--bcs-accent) 18%, var(--card-background-color)); + border-color: rgba(30,136,229,.35); + background: rgba(30,136,229,.10); } .fab[disabled]{ opacity: .55; cursor: not-allowed; } pre.readme{ - white-space: pre-wrap; - word-break: break-word; - margin: 0; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - font-size: 12.5px; - line-height: 1.5; + padding: 12px; + border-radius: 14px; + border: 1px solid var(--divider-color); + background: rgba(0,0,0,.04); + overflow:auto; + font-size: 12px; + line-height: 1.4; } - details{ margin-top: 10px; } - summary{ cursor:pointer; color: var(--bcs-accent); font-weight: 900; } - .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; } - .md pre { overflow:auto; padding:12px; border-radius:14px; border:1px solid var(--divider-color); } - .md code { padding:2px 6px; border-radius:8px; border:1px solid var(--divider-color); } - .md blockquote { border-left:4px solid var(--bcs-accent); padding:8px 12px; border-radius:12px; - background: color-mix(in srgb, var(--bcs-accent) 8%, var(--card-background-color)); } - .md table{ width:100%; border-collapse: collapse; } - .md th,.md td{ border:1px solid var(--divider-color); padding:8px; text-align:left; } - .md img{ max-width:100%; height:auto; border-radius:12px; } + .md :is(h1,h2,h3){ margin-top: 12px; } + .md code{ + padding: 2px 5px; + border-radius: 8px; + border: 1px solid var(--divider-color); + background: rgba(0,0,0,.04); + } + .md pre{ + padding: 12px; + border-radius: 14px; + border: 1px solid var(--divider-color); + background: rgba(0,0,0,.04); + overflow:auto; + } + .md table{ + width:100%; + border-collapse: collapse; + overflow:auto; + display:block; + } + .md th, .md td{ + border: 1px solid var(--divider-color); + padding: 8px; + text-align:left; + }