Add pinned repositories

This commit is contained in:
2026-03-23 15:22:53 +01:00
parent 754540d578
commit a029738ec8
6 changed files with 109 additions and 4 deletions

View File

@@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
frontend_url_path="bahmcloud-store",
webcomponent_name="bahmcloud-store-panel",
# IMPORTANT: bump v to avoid caching old JS
module_url="/api/bahmcloud_store_static/panel.js?v=110",
module_url="/api/bahmcloud_store_static/panel.js?v=111",
sidebar_title="Bahmcloud Store",
sidebar_icon="mdi:store",
require_admin=True,

View File

@@ -118,7 +118,7 @@ class BCSCore:
self._installed_cache: dict[str, Any] = {}
# Persistent settings (UI toggles etc.)
self.settings: dict[str, Any] = {"hacs_enabled": False}
self.settings: dict[str, Any] = {"hacs_enabled": False, "favorite_repo_ids": []}
# Cached HACS metadata (display names/descriptions). Loaded from storage.
self._hacs_meta_fetched_at: int = 0
@@ -348,24 +348,52 @@ class BCSCore:
def get_settings_public(self) -> dict[str, Any]:
"""Return UI-relevant settings (no I/O)."""
favorite_repo_ids = self.settings.get("favorite_repo_ids") or []
if not isinstance(favorite_repo_ids, list):
favorite_repo_ids = []
return {
"hacs_enabled": bool(self.settings.get("hacs_enabled", False)),
"favorite_repo_ids": [str(x) for x in favorite_repo_ids if str(x).strip()],
}
async def set_settings(self, updates: dict[str, Any]) -> dict[str, Any]:
"""Persist settings and apply them."""
safe_updates: dict[str, Any] = {}
reload_required = False
if "hacs_enabled" in (updates or {}):
safe_updates["hacs_enabled"] = bool(updates.get("hacs_enabled"))
reload_required = True
if "favorite_repo_ids" in (updates or {}):
raw = updates.get("favorite_repo_ids") or []
if not isinstance(raw, list):
raw = []
favorite_ids: list[str] = []
seen: set[str] = set()
for item in raw:
rid = str(item or "").strip()
if not rid or rid in seen:
continue
seen.add(rid)
favorite_ids.append(rid)
safe_updates["favorite_repo_ids"] = favorite_ids
merged = await self.storage.set_settings(safe_updates)
if isinstance(merged, dict):
self.settings.update(merged)
# Reload repo list after changing settings.
await self.full_refresh(source="settings")
if reload_required:
await self.full_refresh(source="settings")
else:
self.signal_updated()
return self.get_settings_public()
def is_favorite_repo(self, repo_id: str) -> bool:
favorite_repo_ids = self.settings.get("favorite_repo_ids") or []
if not isinstance(favorite_repo_ids, list):
return False
target = str(repo_id or "").strip()
return bool(target) and target in [str(x).strip() for x in favorite_repo_ids]
async def refresh(self) -> None:
index_repos, refresh_seconds = await self._load_index_repos()
self.refresh_seconds = refresh_seconds
@@ -1151,6 +1179,7 @@ class BCSCore:
"installed_version": installed_version,
"installed_manifest_version": installed_manifest_version,
"installed_domains": installed_domains,
"favorite": self.is_favorite_repo(r.id),
}
)
return out

View File

@@ -23,6 +23,7 @@ class BahmcloudStorePanel extends HTMLElement {
// HACS toggle (settings)
this._hacsEnabled = false;
this._favoriteRepoIds = [];
this._detailRepoId = null;
this._detailRepo = null;
@@ -121,9 +122,15 @@ class BahmcloudStorePanel extends HTMLElement {
// Persistent settings (e.g. HACS toggle)
this._hacsEnabled = !!data?.settings?.hacs_enabled;
this._favoriteRepoIds = Array.isArray(data?.settings?.favorite_repo_ids)
? data.settings.favorite_repo_ids.map((x) => String(x))
: [];
// Sync settings from backend (e.g. HACS toggle)
this._hacsEnabled = !!data?.settings?.hacs_enabled;
this._favoriteRepoIds = Array.isArray(data?.settings?.favorite_repo_ids)
? data.settings.favorite_repo_ids.map((x) => String(x))
: [];
if (this._view === "detail" && this._detailRepoId && Array.isArray(data?.repos)) {
const fresh = data.repos.find((r) => this._safeId(r?.id) === this._detailRepoId);
@@ -143,6 +150,9 @@ class BahmcloudStorePanel extends HTMLElement {
const resp = await this._hass.callApi("post", "bcs/settings", updates || {});
if (resp?.ok) {
this._hacsEnabled = !!resp?.settings?.hacs_enabled;
this._favoriteRepoIds = Array.isArray(resp?.settings?.favorite_repo_ids)
? resp.settings.favorite_repo_ids.map((x) => String(x))
: [];
}
} catch (e) {
// Do not fail UI for settings.
@@ -966,6 +976,37 @@ class BahmcloudStorePanel extends HTMLElement {
return v === true;
}
_isFavoriteRepo(repoId) {
const id = this._safeId(repoId);
return !!id && Array.isArray(this._favoriteRepoIds) && this._favoriteRepoIds.includes(id);
}
async _toggleFavorite(repoId) {
if (!this._hass || !repoId) return;
const id = this._safeId(repoId);
const current = Array.isArray(this._favoriteRepoIds) ? this._favoriteRepoIds.slice() : [];
const next = current.includes(id)
? current.filter((x) => x !== id)
: current.concat([id]);
this._favoriteRepoIds = next;
if (Array.isArray(this._data?.repos)) {
this._data.repos = this._data.repos.map((r) => {
if (this._safeId(r?.id) !== id) return r;
return { ...r, favorite: next.includes(id) };
});
}
if (this._detailRepo && this._safeId(this._detailRepo?.id) === id) {
this._detailRepo = { ...this._detailRepo, favorite: next.includes(id) };
}
this._update();
await this._setSettings({ favorite_repo_ids: next });
await this._load();
}
_renderStore() {
const repos = Array.isArray(this._data.repos) ? this._data.repos : [];
@@ -990,11 +1031,13 @@ class BahmcloudStorePanel extends HTMLElement {
const installed = this._asBoolStrict(r?.installed);
const installedVersion = this._safeText(r?.installed_version);
const updateAvailable = installed && !!latest && (!installedVersion || latest !== installedVersion);
const favorite = this._asBoolStrict(r?.favorite) || this._isFavoriteRepo(r?.id);
if (this._filter === "installed" && !installed) return false;
if (this._filter === "not_installed" && installed) return false;
if (this._filter === "updates" && !updateAvailable) return false;
if (this._filter === "custom" && r?.source !== "custom") return false;
if (this._filter === "favorites" && !favorite) return false;
return true;
})
@@ -1006,16 +1049,22 @@ class BahmcloudStorePanel extends HTMLElement {
const ainstalled = this._asBoolStrict(a?.installed);
const ainstalledVersion = this._safeText(a?.installed_version);
const aupdate = ainstalled && !!alatest && (!ainstalledVersion || alatest !== ainstalledVersion);
const afavorite = this._asBoolStrict(a?.favorite) || this._isFavoriteRepo(a?.id);
const blatest = this._safeText(b?.latest_version);
const binstalled = this._asBoolStrict(b?.installed);
const binstalledVersion = this._safeText(b?.installed_version);
const bupdate = binstalled && !!blatest && (!binstalledVersion || blatest !== binstalledVersion);
const bfavorite = this._asBoolStrict(b?.favorite) || this._isFavoriteRepo(b?.id);
if (this._sort === "updates_first") {
if (aupdate !== bupdate) return aupdate ? -1 : 1;
return an.localeCompare(bn);
}
if (this._sort === "favorites_first") {
if (afavorite !== bfavorite) return afavorite ? -1 : 1;
return an.localeCompare(bn);
}
if (this._sort === "installed_first") {
if (ainstalled !== binstalled) return ainstalled ? -1 : 1;
return an.localeCompare(bn);
@@ -1038,6 +1087,7 @@ class BahmcloudStorePanel extends HTMLElement {
const installed = this._asBoolStrict(r?.installed);
const installedVersion = this._safeText(r?.installed_version);
const updateAvailable = installed && !!latest && (!installedVersion || latest !== installedVersion);
const favorite = this._asBoolStrict(r?.favorite) || this._isFavoriteRepo(r?.id);
const badges = [];
// Source badges
@@ -1045,6 +1095,7 @@ class BahmcloudStorePanel extends HTMLElement {
else if (r?.source === "hacs") badges.push("HACS");
else if (r?.source === "custom") badges.push("Custom");
if (favorite) badges.push("Pinned");
if (installed) badges.push("Installed");
if (updateAvailable) badges.push("Update");
@@ -1088,6 +1139,7 @@ class BahmcloudStorePanel extends HTMLElement {
</select>
<select id="filter">
<option value="all" ${this._filter === "all" ? "selected" : ""}>All</option>
<option value="favorites" ${this._filter === "favorites" ? "selected" : ""}>Pinned</option>
<option value="installed" ${this._filter === "installed" ? "selected" : ""}>Installed</option>
<option value="not_installed" ${this._filter === "not_installed" ? "selected" : ""}>Not installed</option>
<option value="updates" ${this._filter === "updates" ? "selected" : ""}>Updates available</option>
@@ -1095,6 +1147,7 @@ class BahmcloudStorePanel extends HTMLElement {
</select>
<select id="sort">
<option value="az" ${this._sort === "az" ? "selected" : ""}>AZ</option>
<option value="favorites_first" ${this._sort === "favorites_first" ? "selected" : ""}>Pinned first</option>
<option value="updates_first" ${this._sort === "updates_first" ? "selected" : ""}>Updates first</option>
<option value="installed_first" ${this._sort === "installed_first" ? "selected" : ""}>Installed first</option>
</select>
@@ -1260,6 +1313,7 @@ class BahmcloudStorePanel extends HTMLElement {
const installedVersion = this._safeText(r?.installed_version);
const installedDomains = Array.isArray(r?.installed_domains) ? r.installed_domains : [];
const latestVersion = this._safeText(r?.latest_version);
const favorite = this._asBoolStrict(r?.favorite) || this._isFavoriteRepo(repoId);
const busyInstall = this._installingRepoId === repoId;
const busyUpdate = this._updatingRepoId === repoId;
@@ -1323,6 +1377,7 @@ class BahmcloudStorePanel extends HTMLElement {
const updateBtn = `<button class="primary" id="btnUpdate" ${!updateAvailable || busy ? "disabled" : ""}>${busyUpdate ? "Updating…" : updateAvailable ? "Update" : "Up to date"}</button>`;
const uninstallBtn = `<button class="primary" id="btnUninstall" ${!installed || busy ? "disabled" : ""}>${busyUninstall ? "Uninstalling…" : "Uninstall"}</button>`;
const restoreBtn = `<button class="primary" id="btnRestore" ${!installed || busy ? "disabled" : ""}>Restore</button>`;
const favoriteBtn = `<button id="btnFavorite">${favorite ? "Unpin" : "Pin"}</button>`;
const restartHint = this._restartRequired
? `
@@ -1375,6 +1430,7 @@ class BahmcloudStorePanel extends HTMLElement {
${releaseNotesBlock}
<div class="row" style="margin-top:14px; gap:10px; flex-wrap:wrap;">
${favoriteBtn}
${installBtn}
${updateBtn}
${uninstallBtn}
@@ -1399,6 +1455,13 @@ class BahmcloudStorePanel extends HTMLElement {
const btnRestart = root.getElementById("btnRestart");
const btnReadmeToggle = root.getElementById("btnReadmeToggle");
const selVersion = root.getElementById("selVersion");
const btnFavorite = root.getElementById("btnFavorite");
if (btnFavorite) {
btnFavorite.addEventListener("click", () => {
if (this._detailRepoId) this._toggleFavorite(this._detailRepoId);
});
}
if (btnInstall) {
btnInstall.addEventListener("click", () => {

View File

@@ -275,6 +275,9 @@ class BCSSettingsView(HomeAssistantView):
updates: dict[str, Any] = {}
if "hacs_enabled" in data:
updates["hacs_enabled"] = bool(data.get("hacs_enabled"))
if "favorite_repo_ids" in data:
raw = data.get("favorite_repo_ids") or []
updates["favorite_repo_ids"] = raw if isinstance(raw, list) else []
try:
settings = await self.core.set_settings(updates)
@@ -560,6 +563,7 @@ class BCSRepoDetailView(HomeAssistantView):
"installed_version": inst.get("installed_version"),
"installed_manifest_version": inst.get("installed_manifest_version"),
"installed_domains": domains,
"favorite": self.core.is_favorite_repo(repo.id),
}
}, status=200)
except Exception as e: