diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/hassio/src/addon-store/hassio-addon-repository.ts index 080d4a3d77..60a104bd57 100644 --- a/hassio/src/addon-store/hassio-addon-repository.ts +++ b/hassio/src/addon-store/hassio-addon-repository.ts @@ -41,13 +41,19 @@ class HassioAddonRepositoryEl extends LitElement { protected render(): TemplateResult { const repo = this.repo; - const addons = this._getAddons(this.addons, this.filter); + let _addons = this.addons; + if (!this.hass.userData?.showAdvanced) { + _addons = _addons.filter((addon) => { + return !addon.advanced; + }); + } + const addons = this._getAddons(_addons, this.filter); if (this.filter && addons.length < 1) { return html`

- No results found in "${repo.name}" + No results found in "${repo.name}."

`; @@ -57,66 +63,55 @@ class HassioAddonRepositoryEl extends LitElement {

${repo.name}

-

- Maintained by ${repo.maintainer}
- - ${repo.url} - -

${addons.map( (addon) => html` - ${addon.advanced && !this.hass.userData?.showAdvanced - ? "" - : html` - -
- -
-
- `} + +
+ +
+
` )}
diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index 1e9985c284..8bdbe6e398 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -12,15 +12,17 @@ import { HassioAddonRepository, reloadHassioAddons, } from "../../../src/data/hassio/addon"; +import "../../../src/components/ha-icon"; import "../../../src/layouts/loading-screen"; import "../../../src/layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../../src/types"; -import "../components/hassio-search-input"; +import "../../../src/common/search/search-input"; import "./hassio-addon-repository"; -import "./hassio-repositories-editor"; import { supervisorTabs } from "../hassio-panel"; +import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; + const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { if (a.slug === "local") { return -1; @@ -76,7 +78,7 @@ class HassioAddonStore extends LitElement { .hass=${this.hass} .repo=${repo} .addons=${addons} - .filter=${this._filter} + .filter=${this._filter!} > `); } @@ -92,28 +94,52 @@ class HassioAddonStore extends LitElement { .tabs=${supervisorTabs} > Add-on store - - + > + + + + Repositories + + + Reload + + + ${repos.length === 0 ? html`` : html` - - - + ${repos} `} + ${!this.hass.userData?.showAdvanced + ? html` +
+ Missing add-ons? Enable advanced mode on + + your profile page + + . +
+ ` + : ""} `; } @@ -130,6 +156,13 @@ class HassioAddonStore extends LitElement { } } + private async _manageRepositories() { + showRepositoriesDialog(this, { + repos: this._repos!, + loadData: () => this._loadData(), + }); + } + private async _loadData() { try { const addonsInfo = await fetchHassioAddonsInfo(this.hass); @@ -150,6 +183,25 @@ class HassioAddonStore extends LitElement { hassio-addon-repository { margin-top: 24px; } + .search { + padding: 0 16px; + background: var(--sidebar-background-color); + border-bottom: 1px solid var(--divider-color); + } + .search search-input { + position: relative; + top: 2px; + } + .advanced { + padding: 12px; + display: flex; + flex-wrap: wrap; + color: var(--primary-text-color); + } + .advanced a { + margin-left: 0.5em; + color: var(--primary-color); + } `; } } diff --git a/hassio/src/addon-store/hassio-repositories-editor.ts b/hassio/src/addon-store/hassio-repositories-editor.ts deleted file mode 100644 index 48e151dc34..0000000000 --- a/hassio/src/addon-store/hassio-repositories-editor.ts +++ /dev/null @@ -1,152 +0,0 @@ -import "@polymer/paper-card/paper-card"; -import "@polymer/paper-input/paper-input"; -import { - css, - CSSResultArray, - customElement, - html, - LitElement, - property, - PropertyValues, - TemplateResult, -} from "lit-element"; -import { repeat } from "lit-html/directives/repeat"; -import memoizeOne from "memoize-one"; -import "../../../src/components/buttons/ha-call-api-button"; -import { HassioAddonRepository } from "../../../src/data/hassio/addon"; -import { PolymerChangedEvent } from "../../../src/polymer-types"; -import { HomeAssistant } from "../../../src/types"; -import "../components/hassio-card-content"; -import { hassioStyle } from "../resources/hassio-style"; -import "../../../src/components/ha-icon"; - -@customElement("hassio-repositories-editor") -class HassioRepositoriesEditor extends LitElement { - @property() public hass!: HomeAssistant; - - @property() public repos!: HassioAddonRepository[]; - - @property() private _repoUrl = ""; - - private _sortedRepos = memoizeOne((repos: HassioAddonRepository[]) => - repos - .filter((repo) => repo.slug !== "core" && repo.slug !== "local") - .sort((a, b) => (a.name < b.name ? -1 : 1)) - ); - - protected render(): TemplateResult { - const repos = this._sortedRepos(this.repos); - return html` -
-

- Repositories -

-

- Configure which add-on repositories to fetch data from: -

-
- ${// Use repeat so that the fade-out from call-service-api-button - // stays with the correct repo after we add/delete one. - repeat( - repos, - (repo) => repo.slug, - (repo) => html` - -
- -
-
- - Remove - -
-
- ` - )} - - -
- - -
-
- - Add - -
-
-
-
- `; - } - - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - - if (changedProps.has("repos")) { - this._repoUrl = ""; - } - } - - private _urlChanged(ev: PolymerChangedEvent) { - this._repoUrl = ev.detail.value; - } - - private computeRemoveRepoData(repoList, url) { - const list = repoList - .filter((repo) => repo.url !== url) - .map((repo) => repo.source); - return { addons_repositories: list }; - } - - private computeAddRepoData(repoList, url) { - const list = repoList ? repoList.map((repo) => repo.source) : []; - list.push(url); - return { addons_repositories: list }; - } - - static get styles(): CSSResultArray { - return [ - hassioStyle, - css` - .add { - padding: 12px 16px; - } - ha-icon { - color: var(--secondary-text-color); - margin-right: 16px; - display: inline-block; - } - paper-input { - width: calc(100% - 49px); - display: inline-block; - margin-top: -4px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-repositories-editor": HassioRepositoriesEditor; - } -} diff --git a/hassio/src/components/hassio-search-input.ts b/hassio/src/components/hassio-search-input.ts deleted file mode 100644 index cdf69b3607..0000000000 --- a/hassio/src/components/hassio-search-input.ts +++ /dev/null @@ -1,81 +0,0 @@ -import "@material/mwc-button"; -import "../../../src/components/ha-icon-button"; -import "../../../src/components/ha-icon"; -import "@polymer/paper-input/paper-input"; -import { - css, - CSSResult, - customElement, - LitElement, - property, -} from "lit-element"; -import { html, TemplateResult } from "lit-html"; -import { fireEvent } from "../../../src/common/dom/fire_event"; - -@customElement("hassio-search-input") -class HassioSearchInput extends LitElement { - @property() private filter?: string; - - protected render(): TemplateResult { - return html` -
- - - ${this.filter && - html` - - `} - -
- `; - } - - private async _filterChanged(value: string) { - fireEvent(this, "value-changed", { value: String(value) }); - } - - private async _filterInputChanged(e) { - this._filterChanged(e.target.value); - } - - private async _clearSearch() { - this._filterChanged(""); - } - - static get styles(): CSSResult { - return css` - paper-input { - flex: 1 1 auto; - margin: 0 16px; - } - .search-container { - display: inline-flex; - width: 100%; - align-items: center; - } - .prefix { - margin: 8px; - } - ha-icon { - color: var(--primary-text-color); - } - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-search-input": HassioSearchInput; - } -} diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts new file mode 100644 index 0000000000..fe76c3d6fb --- /dev/null +++ b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts @@ -0,0 +1,232 @@ +import "@material/mwc-button/mwc-button"; +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-spinner/paper-spinner"; +import type { PaperInputElement } from "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + query, + TemplateResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import "../../../../src/components/ha-dialog"; +import "../../../../src/components/ha-icon"; + +import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; +import type { HomeAssistant } from "../../../../src/types"; +import { + HassioAddonRepository, + fetchHassioAddonsInfo, +} from "../../../../src/data/hassio/addon"; + +import { setSupervisorOption } from "../../../../src/data/hassio/supervisor"; +import { HassioRepositoryDialogParams } from "./show-dialog-repositories"; + +@customElement("dialog-hassio-repositories") +class HassioRepositoriesDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) private _repos: HassioAddonRepository[] = []; + + @property({ attribute: false }) + private _dialogParams?: HassioRepositoryDialogParams; + + @query("#repository_input") private _optionInput?: PaperInputElement; + + @property() private _opened = false; + + @property() private _prosessing = false; + + @property() private _error?: string; + + public async showDialog(_dialogParams: any): Promise { + this._dialogParams = _dialogParams; + this._repos = _dialogParams.repos; + this._opened = true; + await this.updateComplete; + } + + public closeDialog(): void { + this._opened = false; + this._error = ""; + } + + private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) => + repos + .filter((repo) => repo.slug !== "core" && repo.slug !== "local") + .sort((a, b) => (a.name < b.name ? -1 : 1)) + ); + + protected render(): TemplateResult { + const repositories = this._filteredRepositories(this._repos); + return html` + + ${this._error ? html`
${this._error}
` : ""} +
+ ${repositories.length + ? repositories.map((repo) => { + return html` + + +
${repo.name}
+
${repo.maintainer}
+
${repo.url}
+
+ +
+ `; + }) + : html` + + No repositories + + `} +
+ + + ${this._prosessing + ? html`` + : "Add"} + +
+
+ + Close + +
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + haStyleDialog, + css` + ha-dialog.button-left { + --justify-action-buttons: flex-start; + } + paper-icon-item { + cursor: pointer; + } + .form { + color: var(--primary-text-color); + } + .option { + border: 1px solid var(--divider-color); + border-radius: 4px; + margin-top: 4px; + } + mwc-button { + margin-left: 8px; + } + ha-paper-dropdown-menu { + display: block; + } + `, + ]; + } + + public focus() { + this.updateComplete.then(() => + (this.shadowRoot?.querySelector( + "[dialogInitialFocus]" + ) as HTMLElement)?.focus() + ); + } + + private _handleKeyAdd(ev: KeyboardEvent) { + ev.stopPropagation(); + if (ev.keyCode !== 13) { + return; + } + this._addRepository(); + } + + private async _addRepository() { + const input = this._optionInput; + if (!input || !input.value) { + return; + } + this._prosessing = true; + const repositories = this._filteredRepositories(this._repos); + const newRepositories = repositories.map((repo) => { + return repo.source; + }); + newRepositories.push(input.value); + + try { + await setSupervisorOption(this.hass, { + addons_repositories: newRepositories, + }); + + const addonsInfo = await fetchHassioAddonsInfo(this.hass); + this._repos = addonsInfo.repositories; + + await this._dialogParams!.loadData(); + + input.value = ""; + } catch (err) { + this._error = err.message; + } + this._prosessing = false; + } + + private async _removeRepository(ev: Event) { + const slug = (ev.target as any).slug; + const repositories = this._filteredRepositories(this._repos); + const repository = repositories.find((repo) => { + return repo.slug === slug; + }); + if (!repository) { + return; + } + const newRepositories = repositories + .map((repo) => { + return repo.source; + }) + .filter((repo) => { + return repo !== repository.source; + }); + + try { + await setSupervisorOption(this.hass, { + addons_repositories: newRepositories, + }); + + const addonsInfo = await fetchHassioAddonsInfo(this.hass); + this._repos = addonsInfo.repositories; + + await this._dialogParams!.loadData(); + } catch (err) { + this._error = err.message; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-hassio-repositories": HassioRepositoriesDialog; + } +} diff --git a/hassio/src/dialogs/repositories/show-dialog-repositories.ts b/hassio/src/dialogs/repositories/show-dialog-repositories.ts new file mode 100644 index 0000000000..8485cebd78 --- /dev/null +++ b/hassio/src/dialogs/repositories/show-dialog-repositories.ts @@ -0,0 +1,22 @@ +import { fireEvent } from "../../../../src/common/dom/fire_event"; +import "./dialog-hassio-repositories"; +import { HassioAddonRepository } from "../../../../src/data/hassio/addon"; + +export interface HassioRepositoryDialogParams { + repos: HassioAddonRepository[]; + loadData: () => Promise; +} + +export const showRepositoriesDialog = ( + element: HTMLElement, + dialogParams: HassioRepositoryDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-hassio-repositories", + dialogImport: () => + import( + /* webpackChunkName: "dialog-hassio-repositories" */ "./dialog-hassio-repositories" + ), + dialogParams, + }); +}; diff --git a/src/common/search/search-input.ts b/src/common/search/search-input.ts index 8b5cfa208f..65cd7673f7 100644 --- a/src/common/search/search-input.ts +++ b/src/common/search/search-input.ts @@ -77,6 +77,10 @@ class SearchInput extends LitElement { static get styles(): CSSResult { return css` + ha-icon, + ha-icon-button { + color: var(--primary-text-color); + } ha-icon { margin: 8px; } diff --git a/src/data/hassio/supervisor.ts b/src/data/hassio/supervisor.ts index 8788b88f14..65cda7e8b1 100644 --- a/src/data/hassio/supervisor.ts +++ b/src/data/hassio/supervisor.ts @@ -16,7 +16,8 @@ export interface CreateSessionResponse { } export interface SupervisorOptions { - channel: "beta" | "dev" | "stable"; + channel?: "beta" | "dev" | "stable"; + addons_repositories?: string[]; } export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {