diff --git a/.gitignore b/.gitignore index a310dbff9..a86ee128c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ yarn-error.log .yarn/* !.yarn/releases !.yarn/releases/yarn-*.cjs + diff --git a/script/build b/script/build deleted file mode 100755 index 83d0bdb98..000000000 --- a/script/build +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Run the frontend development server - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -if [ ! -d "./node_modules" ]; then - echo "Directory /node_modules DOES NOT exists." - echo "Running yarn install" - yarn install -fi - -NODE_OPTIONS=--max_old_space_size=6144 ./node_modules/.bin/gulp build-hacs \ No newline at end of file diff --git a/src/components/dialogs/hacs-download-dialog.ts b/src/components/dialogs/hacs-download-dialog.ts index d7a0f3449..a44223abf 100644 --- a/src/components/dialogs/hacs-download-dialog.ts +++ b/src/components/dialogs/hacs-download-dialog.ts @@ -1,388 +1,389 @@ -import "@material/mwc-button/mwc-button"; -import "@material/mwc-linear-progress/mwc-linear-progress"; -import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../homeassistant-frontend/src/common/dom/fire_event"; -import { mainWindow } from "../../../homeassistant-frontend/src/common/dom/get_main_window"; -import { computeRTL } from "../../../homeassistant-frontend/src/common/util/compute_rtl"; -import "../../../homeassistant-frontend/src/components/ha-alert"; -import "../../../homeassistant-frontend/src/components/ha-button"; -import "../../../homeassistant-frontend/src/components/ha-circular-progress"; -import "../../../homeassistant-frontend/src/components/ha-dialog"; -import "../../../homeassistant-frontend/src/components/ha-expansion-panel"; -import "../../../homeassistant-frontend/src/components/ha-form/ha-form"; -import "../../../homeassistant-frontend/src/components/ha-list-item"; - -import { relativeTime } from "../../../homeassistant-frontend/src/common/datetime/relative_time"; -import { showConfirmationDialog } from "../../../homeassistant-frontend/src/dialogs/generic/show-dialog-box"; -import type { HomeAssistant } from "../../../homeassistant-frontend/src/types"; -import { HacsDispatchEvent } from "../../data/common"; -import { - fetchRepositoryInformation, - RepositoryBase, - repositoryDownloadVersion, - RepositoryInfo, - repositoryReleases, -} from "../../data/repository"; -import { websocketSubscription } from "../../data/websocket"; -import { HacsStyles } from "../../styles/hacs-common-style"; -import { generateFrontendResourceURL } from "../../tools/frontend-resource"; -import type { HacsDownloadDialogParams } from "./show-hacs-dialog"; - -@customElement("release-item") -export class ReleaseItem extends LitElement { - @property({ attribute: false }) public locale!: HomeAssistant["locale"]; - @property({ attribute: false }) public release!: { - tag: string; - published_at: string; - name: string; - prerelease: boolean; - }; - - protected render() { - return html` - - ${this.release.tag} - ${this.release.prerelease ? html`pre-release` : nothing} - - - ${relativeTime(new Date(this.release.published_at), this.locale)} - ${this.release.name && this.release.name !== this.release.tag - ? html` - ${this.release.name}` - : nothing} - - `; - } - - static get styles(): CSSResultGroup { - return css` - :host { - display: flex; - flex-direction: column; - } - .secondary { - font-size: 0.8em; - color: var(--secondary-text-color); - font-style: italic; - } - .pre-release { - background-color: var(--accent-color); - padding: 2px 4px; - font-size: 0.8em; - font-weight: 600; - border-radius: 12px; - margin: 0 2px; - color: var(--secondary-background-color); - } - `; - } -} -@customElement("hacs-download-dialog") -export class HacsDonwloadDialog extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _waiting = true; - - @state() private _installing = false; - - @state() private _error?: any; - - @state() private _releases?: { - tag: string; - name: string; - published_at: string; - prerelease: boolean; - }[]; - - @state() public _repository?: RepositoryInfo; - - @state() _dialogParams?: HacsDownloadDialogParams; - - @state() _selectedVersion?: string; - - public async showDialog(dialogParams: HacsDownloadDialogParams): Promise { - this._dialogParams = dialogParams; - this._waiting = false; - if (dialogParams.repository) { - this._repository = dialogParams.repository; - } else { - await this._fetchRepository(); - } - - if (this._repository && this._repository.version_or_commit !== "commit") { - this._selectedVersion = this._repository.available_version; - } - this._releases = undefined; - - websocketSubscription( - this.hass, - (data) => { - this._error = data; - this._installing = false; - }, - HacsDispatchEvent.ERROR, - ); - await this.updateComplete; - } - - public closeDialog(): void { - this._dialogParams = undefined; - this._repository = undefined; - this._error = undefined; - this._installing = false; - this._waiting = false; - this._releases = undefined; - this._selectedVersion = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - private _getInstallPath = memoizeOne((repository: RepositoryBase) => { - let path: string = repository.local_path; - if (["template", "theme", "python_script"].includes(repository.category)) { - path = `${path}/${repository.file_name}`; - } - return path; - }); - - private async _fetchRepository() { - try { - this._repository = await fetchRepositoryInformation( - this.hass, - this._dialogParams!.repositoryId, - ); - } catch (err: any) { - this._error = err; - } - } - - protected render() { - if (!this._dialogParams) { - return nothing; - } - if (!this._repository) { - return html` - -
- - ${this._error - ? html` - ${this._error.message || this._error} - ` - : nothing} -
-
- `; - } - - const installPath = this._getInstallPath(this._repository); - return html` - -
-

- ${this._dialogParams.hacs.localize( - this._repository.version_or_commit === "commit" - ? "dialog_download.will_download_commit" - : "dialog_download.will_download_version", - { - ref: html` - ${this._selectedVersion || this._repository.available_version} - `, - }, - )} -

-
- ${this._dialogParams.hacs.localize("dialog_download.note_downloaded", { - location: html`'${installPath}'`, - })} - ${this._repository.category === "plugin" && - this._dialogParams.hacs.info.lovelace_mode !== "storage" - ? html` -

${this._dialogParams.hacs.localize(`dialog_download.lovelace_instruction`)}

-
-                url: ${generateFrontendResourceURL({ repository: this._repository })}
-                type: module
-                
- ` - : nothing} - ${this._repository.category === "integration" - ? html`

${this._dialogParams.hacs.localize("dialog_download.restart")}

` - : nothing} -
- ${this._selectedVersion - ? html` -

${this._dialogParams!.hacs.localize("dialog_download.release_warning")}

- ${this._releases === undefined - ? this._dialogParams.hacs.localize("dialog_download.fetching_releases") - : this._releases.length === 0 - ? this._dialogParams.hacs.localize("dialog_download.no_releases") - : html` ({ - value: release.tag, - label: html` - ${release.tag} - `, - })), - }, - }, - }, - ]} - >`} -
` - : nothing} - ${this._error - ? html` - ${this._error.message || this._error} - ` - : nothing} - ${this._installing - ? html`` - : nothing} -
- - ${this._dialogParams.hacs.localize("common.cancel")} - - - ${this._dialogParams.hacs.localize("common.download")} - -
- `; - } - - private _computeLabel = (entry: any): string => - entry.name === "release" - ? this._dialogParams!.hacs.localize("dialog_download.release") - : entry.name; - - private async _installRepository(): Promise { - if (!this._repository) { - return; - } - - if (this._waiting) { - this._error = "Waiting to update repository information, try later."; - return; - } - - if (this._installing) { - this._error = "Already installing, please wait."; - return; - } - - this._installing = true; - this._error = undefined; - - try { - await repositoryDownloadVersion( - this.hass, - String(this._repository.id), - this._selectedVersion || this._repository.available_version, - ); - } catch (err: any) { - this._error = err || { - message: "Could not download repository, check core logs for more information.", - }; - this._installing = false; - return; - } - - this._dialogParams!.hacs.log.debug(this._repository.category, "_installRepository"); - this._dialogParams!.hacs.log.debug( - this._dialogParams!.hacs.info.lovelace_mode, - "_installRepository", - ); - this._installing = false; - - if (this._repository.category === "plugin") { - showConfirmationDialog(this, { - title: this._dialogParams!.hacs.localize!("common.reload"), - text: html`${this._dialogParams!.hacs.localize!("dialog.reload.description")}
${this - ._dialogParams!.hacs.localize!("dialog.reload.confirm")}`, - dismissText: this._dialogParams!.hacs.localize!("common.cancel"), - confirmText: this._dialogParams!.hacs.localize!("common.reload"), - confirm: () => { - // eslint-disable-next-line - mainWindow.location.href = mainWindow.location.href; - }, - }); - } - if (this._error === undefined) { - this.closeDialog(); - } - } - - async _fetchReleases() { - if (this._releases !== undefined) { - return; - } - try { - this._releases = await repositoryReleases(this.hass, this._repository!.id); - } catch (error) { - this._error = error; - } - } - - private _versionChanged(ev: CustomEvent) { - this._selectedVersion = ev.detail.value.release; - } - - static get styles(): CSSResultGroup { - return [ - HacsStyles, - css` - .note { - margin-top: 12px; - } - pre { - white-space: pre-line; - user-select: all; - padding: 8px; - } - mwc-linear-progress { - margin-bottom: -8px; - margin-top: 4px; - } - ha-expansion-panel { - background-color: var(--secondary-background-color); - padding: 8px; - } - .loading { - text-align: center; - padding: 16px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hacs-download-dialog": HacsDonwloadDialog; - "release-item": ReleaseItem; - } -} +import "@material/mwc-button/mwc-button"; +import "@material/mwc-linear-progress/mwc-linear-progress"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../homeassistant-frontend/src/common/dom/fire_event"; +import { mainWindow } from "../../../homeassistant-frontend/src/common/dom/get_main_window"; +import { computeRTL } from "../../../homeassistant-frontend/src/common/util/compute_rtl"; +import "../../../homeassistant-frontend/src/components/ha-alert"; +import "../../../homeassistant-frontend/src/components/ha-button"; +import "../../../homeassistant-frontend/src/components/ha-circular-progress"; +import "../../../homeassistant-frontend/src/components/ha-dialog"; +import "../../../homeassistant-frontend/src/components/ha-expansion-panel"; +import "../../../homeassistant-frontend/src/components/ha-form/ha-form"; +import "../../../homeassistant-frontend/src/components/ha-list-item"; + +import { relativeTime } from "../../../homeassistant-frontend/src/common/datetime/relative_time"; +import { showConfirmationDialog } from "../../../homeassistant-frontend/src/dialogs/generic/show-dialog-box"; +import type { HomeAssistant } from "../../../homeassistant-frontend/src/types"; +import { HacsDispatchEvent } from "../../data/common"; +import { + fetchRepositoryInformation, + RepositoryBase, + repositoryDownloadVersion, + RepositoryInfo, + repositoryReleases, +} from "../../data/repository"; +import { websocketSubscription } from "../../data/websocket"; +import { HacsStyles } from "../../styles/hacs-common-style"; +import { generateFrontendResourceURL } from "../../tools/frontend-resource"; +import type { HacsDownloadDialogParams } from "./show-hacs-dialog"; + +@customElement("release-item") +export class ReleaseItem extends LitElement { + @property({ attribute: false }) public locale!: HomeAssistant["locale"]; + @property({ attribute: false }) public release!: { + tag: string; + published_at: string; + name: string; + prerelease: boolean; + }; + + protected render() { + return html` + + ${this.release.tag} + ${this.release.prerelease ? html`pre-release` : nothing} + + + ${relativeTime(new Date(this.release.published_at), this.locale)} + ${this.release.name && this.release.name !== this.release.tag + ? html` - ${this.release.name}` + : nothing} + + `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + flex-direction: column; + } + .secondary { + font-size: 0.8em; + color: var(--secondary-text-color); + font-style: italic; + } + .pre-release { + background-color: var(--accent-color); + padding: 2px 4px; + font-size: 0.8em; + font-weight: 600; + border-radius: 12px; + margin: 0 2px; + color: var(--secondary-background-color); + } + `; + } +} +@customElement("hacs-download-dialog") +export class HacsDonwloadDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _waiting = true; + + @state() private _installing = false; + + @state() private _error?: any; + + @state() private _releases?: { + tag: string; + name: string; + published_at: string; + prerelease: boolean; + }[]; + + @state() public _repository?: RepositoryInfo; + + @state() _dialogParams?: HacsDownloadDialogParams; + + @state() _selectedVersion?: string; + + public async showDialog(dialogParams: HacsDownloadDialogParams): Promise { + this._dialogParams = dialogParams; + this._waiting = false; + if (dialogParams.repository) { + this._repository = dialogParams.repository; + } else { + await this._fetchRepository(); + } + + if (this._repository && this._repository.version_or_commit !== "commit") { + this._selectedVersion = this._repository.available_version; + } + this._releases = undefined; + + websocketSubscription( + this.hass, + (data) => { + this._error = data; + this._installing = false; + }, + HacsDispatchEvent.ERROR, + ); + await this.updateComplete; + } + + public closeDialog(): void { + this._dialogParams = undefined; + this._repository = undefined; + this._error = undefined; + this._installing = false; + this._waiting = false; + this._releases = undefined; + this._selectedVersion = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private _getInstallPath = memoizeOne((repository: RepositoryBase) => { + let path: string = repository.local_path; + if (["template", "theme", "python_script"].includes(repository.category)) { + path = `${path}/${repository.file_name}`; + } + return path; + }); + + private async _fetchRepository() { + try { + this._repository = await fetchRepositoryInformation( + this.hass, + this._dialogParams!.repositoryId, + this.hass.language, + ); + } catch (err: any) { + this._error = err; + } + } + + protected render() { + if (!this._dialogParams) { + return nothing; + } + if (!this._repository) { + return html` + +
+ + ${this._error + ? html` + ${this._error.message || this._error} + ` + : nothing} +
+
+ `; + } + + const installPath = this._getInstallPath(this._repository); + return html` + +
+

+ ${this._dialogParams.hacs.localize( + this._repository.version_or_commit === "commit" + ? "dialog_download.will_download_commit" + : "dialog_download.will_download_version", + { + ref: html` + ${this._selectedVersion || this._repository.available_version} + `, + }, + )} +

+
+ ${this._dialogParams.hacs.localize("dialog_download.note_downloaded", { + location: html`'${installPath}'`, + })} + ${this._repository.category === "plugin" && + this._dialogParams.hacs.info.lovelace_mode !== "storage" + ? html` +

${this._dialogParams.hacs.localize(`dialog_download.lovelace_instruction`)}

+
+                url: ${generateFrontendResourceURL({ repository: this._repository })}
+                type: module
+                
+ ` + : nothing} + ${this._repository.category === "integration" + ? html`

${this._dialogParams.hacs.localize("dialog_download.restart")}

` + : nothing} +
+ ${this._selectedVersion + ? html` +

${this._dialogParams!.hacs.localize("dialog_download.release_warning")}

+ ${this._releases === undefined + ? this._dialogParams.hacs.localize("dialog_download.fetching_releases") + : this._releases.length === 0 + ? this._dialogParams.hacs.localize("dialog_download.no_releases") + : html` ({ + value: release.tag, + label: html` + ${release.tag} + `, + })), + }, + }, + }, + ]} + >`} +
` + : nothing} + ${this._error + ? html` + ${this._error.message || this._error} + ` + : nothing} + ${this._installing + ? html`` + : nothing} +
+ + ${this._dialogParams.hacs.localize("common.cancel")} + + + ${this._dialogParams.hacs.localize("common.download")} + +
+ `; + } + + private _computeLabel = (entry: any): string => + entry.name === "release" + ? this._dialogParams!.hacs.localize("dialog_download.release") + : entry.name; + + private async _installRepository(): Promise { + if (!this._repository) { + return; + } + + if (this._waiting) { + this._error = "Waiting to update repository information, try later."; + return; + } + + if (this._installing) { + this._error = "Already installing, please wait."; + return; + } + + this._installing = true; + this._error = undefined; + + try { + await repositoryDownloadVersion( + this.hass, + String(this._repository.id), + this._selectedVersion || this._repository.available_version, + ); + } catch (err: any) { + this._error = err || { + message: "Could not download repository, check core logs for more information.", + }; + this._installing = false; + return; + } + + this._dialogParams!.hacs.log.debug(this._repository.category, "_installRepository"); + this._dialogParams!.hacs.log.debug( + this._dialogParams!.hacs.info.lovelace_mode, + "_installRepository", + ); + this._installing = false; + + if (this._repository.category === "plugin") { + showConfirmationDialog(this, { + title: this._dialogParams!.hacs.localize!("common.reload"), + text: html`${this._dialogParams!.hacs.localize!("dialog.reload.description")}
${this + ._dialogParams!.hacs.localize!("dialog.reload.confirm")}`, + dismissText: this._dialogParams!.hacs.localize!("common.cancel"), + confirmText: this._dialogParams!.hacs.localize!("common.reload"), + confirm: () => { + // eslint-disable-next-line + mainWindow.location.href = mainWindow.location.href; + }, + }); + } + if (this._error === undefined) { + this.closeDialog(); + } + } + + async _fetchReleases() { + if (this._releases !== undefined) { + return; + } + try { + this._releases = await repositoryReleases(this.hass, this._repository!.id); + } catch (error) { + this._error = error; + } + } + + private _versionChanged(ev: CustomEvent) { + this._selectedVersion = ev.detail.value.release; + } + + static get styles(): CSSResultGroup { + return [ + HacsStyles, + css` + .note { + margin-top: 12px; + } + pre { + white-space: pre-line; + user-select: all; + padding: 8px; + } + mwc-linear-progress { + margin-bottom: -8px; + margin-top: 4px; + } + ha-expansion-panel { + background-color: var(--secondary-background-color); + padding: 8px; + } + .loading { + text-align: center; + padding: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hacs-download-dialog": HacsDonwloadDialog; + "release-item": ReleaseItem; + } +} diff --git a/src/dashboards/hacs-repository-dashboard.ts b/src/dashboards/hacs-repository-dashboard.ts index 7db2a7036..dfc9f9990 100644 --- a/src/dashboards/hacs-repository-dashboard.ts +++ b/src/dashboards/hacs-repository-dashboard.ts @@ -1,378 +1,382 @@ -import { - mdiAccount, - mdiArrowDownBold, - mdiCube, - mdiDotsVertical, - mdiDownload, - mdiExclamationThick, - mdiStar, -} from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { mainWindow } from "../../homeassistant-frontend/src/common/dom/get_main_window"; -import { extractSearchParamsObject } from "../../homeassistant-frontend/src/common/url/search-params"; -import "../../homeassistant-frontend/src/components/chips/ha-assist-chip"; -import "../../homeassistant-frontend/src/components/chips/ha-chip-set"; -import "../../homeassistant-frontend/src/components/ha-alert"; -import "../../homeassistant-frontend/src/components/ha-card"; -import "../../homeassistant-frontend/src/components/ha-fab"; -import "../../homeassistant-frontend/src/components/ha-markdown"; -import "../../homeassistant-frontend/src/components/ha-menu"; -import type { HaMenu } from "../../homeassistant-frontend/src/components/ha-menu"; -import "../../homeassistant-frontend/src/components/ha-md-menu-item"; -import { showConfirmationDialog } from "../../homeassistant-frontend/src/dialogs/generic/show-dialog-box"; -import "../../homeassistant-frontend/src/layouts/hass-error-screen"; -import "../../homeassistant-frontend/src/layouts/hass-loading-screen"; -import "../../homeassistant-frontend/src/layouts/hass-subpage"; -import type { HomeAssistant, Route } from "../../homeassistant-frontend/src/types"; -import { showHacsDownloadDialog } from "../components/dialogs/show-hacs-dialog"; -import { repositoryMenuItems } from "../components/hacs-repository-owerflow-menu"; -import type { Hacs } from "../data/hacs"; -import type { RepositoryBase, RepositoryInfo } from "../data/repository"; -import { fetchRepositoryInformation } from "../data/repository"; -import { getRepositories, repositoryAdd } from "../data/websocket"; -import { HacsStyles } from "../styles/hacs-common-style"; -import { markdownWithRepositoryContext } from "../tools/markdown"; - -@customElement("hacs-repository-dashboard") -export class HacsRepositoryDashboard extends LitElement { - @property({ attribute: false }) public hacs!: Hacs; - - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public narrow!: boolean; - - @property({ attribute: false }) public isWide!: boolean; - - @property({ attribute: false }) public route!: Route; - - @state() public _repository?: RepositoryInfo; - - @state() private _error?: string; - - @query("#overflow-menu") - private _repositoryOverflowMenu!: HaMenu; - - public connectedCallback() { - super.connectedCallback(); - document.body.addEventListener("keydown", this._generateMyLink); - } - - public disconnectedCallback() { - super.disconnectedCallback(); - document.body.removeEventListener("keydown", this._generateMyLink); - } - - private _generateMyLink = (ev: KeyboardEvent) => { - if (ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.altKey) { - // Ignore if modifier keys are pressed - return; - } - if (ev.key === "m" && mainWindow.location.pathname.startsWith("/hacs/repository/")) { - if (!this._repository) { - return; - } - const myParams = new URLSearchParams({ - redirect: "hacs_repository", - owner: this._repository!.full_name.split("/")[0], - repository: this._repository!.full_name.split("/")[1], - category: this._repository!.category, - }); - window.open(`https://my.home-assistant.io/create-link/?${myParams.toString()}`, "_blank"); - } - }; - - protected async firstUpdated(changedProperties: PropertyValues): Promise { - super.firstUpdated(changedProperties); - - const params = extractSearchParamsObject(); - if (Object.entries(params).length) { - let existing: RepositoryBase | undefined; - const requestedRepository = `${params.owner}/${params.repository}`; - existing = this.hacs.repositories.find( - (repository) => - repository.full_name.toLocaleLowerCase() === requestedRepository.toLocaleLowerCase(), - ); - if (!existing && params.category) { - if ( - !(await showConfirmationDialog(this, { - title: this.hacs.localize("my.add_repository_title"), - text: this.hacs.localize("my.add_repository_description", { - repository: requestedRepository, - }), - confirmText: this.hacs.localize("common.add"), - dismissText: this.hacs.localize("common.cancel"), - })) - ) { - this._error = this.hacs.localize("my.repository_not_found", { - repository: requestedRepository, - }); - return; - } - try { - await repositoryAdd(this.hass, requestedRepository, params.category); - this.hacs.repositories = await getRepositories(this.hass); - existing = this.hacs.repositories.find( - (repository) => - repository.full_name.toLocaleLowerCase() === requestedRepository.toLocaleLowerCase(), - ); - } catch (err: any) { - this._error = err; - return; - } - } - if (existing) { - this._fetchRepository(String(existing.id)); - } else { - this._error = this.hacs.localize("my.repository_not_found", { - repository: requestedRepository, - }); - } - } else { - const dividerPos = this.route.path.indexOf("/", 1); - const repositoryId = this.route.path.substr(dividerPos + 1); - if (!repositoryId) { - this._error = "Missing repositoryId from route"; - return; - } - this._fetchRepository(repositoryId); - } - } - - protected updated(changedProps) { - super.updated(changedProps); - if (changedProps.has("repositories") && this._repository) { - this._fetchRepository(); - } - } - - private async _fetchRepository(repositoryId?: string) { - try { - this._repository = await fetchRepositoryInformation( - this.hass, - repositoryId || String(this._repository!.id), - ); - } catch (err: any) { - this._error = err?.message; - } - } - - private _getAuthors = memoizeOne((repository: RepositoryInfo) => { - const authors: string[] = []; - if (!repository.authors) return authors; - repository.authors.forEach((author) => authors.push(author.replace("@", ""))); - if (authors.length === 0) { - const author = repository.full_name.split("/")[0]; - if ( - ["custom-cards", "custom-components", "home-assistant-community-themes"].includes(author) - ) { - return authors; - } - authors.push(author); - } - return authors; - }); - - protected render(): TemplateResult { - if (this._error) { - return html``; - } - - if (!this._repository) { - return html``; - } - - const authors = this._getAuthors(this._repository); - - return html` - - -
- - - ${this._repository.installed - ? html` - - - - ` - : ""} - ${authors - ? authors.map( - (author) => - html` - - - @${author} - - `, - ) - : ""} - ${this._repository.downloads - ? html` - - ` - : ""} - - - ${this._repository.stars} - - - - - ${this._repository.issues} - - - - - -
- - ${!this._repository.installed_version - ? html` - - ` - : ""} -
- - ${repositoryMenuItems(this, this._repository, this.hacs.localize).map((entry) => - entry.divider - ? html`
  • ` - : html` - { - entry?.action && entry.action(); - }} - > - -
    ${entry.label}
    -
    - `, - )} -
    - `; - } - - private _showOverflowRepositoryMenu = (ev: any) => { - if ( - this._repositoryOverflowMenu.open && - ev.target === this._repositoryOverflowMenu.anchorElement - ) { - this._repositoryOverflowMenu.close(); - return; - } - this._repositoryOverflowMenu.anchorElement = ev.target; - this._repositoryOverflowMenu.show(); - }; - - private _downloadRepositoryDialog() { - showHacsDownloadDialog(this, { - hacs: this.hacs, - repositoryId: this._repository!.id, - repository: this._repository!, - }); - } - - static get styles() { - return [ - HacsStyles, - css` - hass-loading-screen { - --app-header-background-color: var(--sidebar-background-color); - --app-header-text-color: var(--sidebar-text-color); - height: 100vh; - } - - hass-subpage { - position: absolute; - width: 100vw; - } - - ha-fab ha-svg-icon { - color: var(--hcv-text-color-on-background); - } - - ha-fab { - position: fixed; - float: right; - right: calc(18px + env(safe-area-inset-right)); - bottom: calc(16px + env(safe-area-inset-bottom)); - z-index: 1; - } - - ha-fab.rtl { - float: left; - right: auto; - left: calc(18px + env(safe-area-inset-left)); - } - - ha-card { - display: block; - padding: 16px; - } - .content { - margin: auto; - padding: 8px; - max-width: 1536px; - } - - ha-chip-set { - padding-bottom: 8px; - } - - @media all and (max-width: 500px) { - .content { - margin: 8px 4px 64px; - max-width: none; - } - } - `, - ]; - } -} +import { + mdiAccount, + mdiArrowDownBold, + mdiCube, + mdiDotsVertical, + mdiDownload, + mdiExclamationThick, + mdiStar, +} from "@mdi/js"; +import type { PropertyValues, TemplateResult } from "lit"; +import { LitElement, css, html } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { mainWindow } from "../../homeassistant-frontend/src/common/dom/get_main_window"; +import { extractSearchParamsObject } from "../../homeassistant-frontend/src/common/url/search-params"; +import "../../homeassistant-frontend/src/components/chips/ha-assist-chip"; +import "../../homeassistant-frontend/src/components/chips/ha-chip-set"; +import "../../homeassistant-frontend/src/components/ha-alert"; +import "../../homeassistant-frontend/src/components/ha-card"; +import "../../homeassistant-frontend/src/components/ha-fab"; +import "../../homeassistant-frontend/src/components/ha-markdown"; +import "../../homeassistant-frontend/src/components/ha-menu"; +import type { HaMenu } from "../../homeassistant-frontend/src/components/ha-menu"; +import "../../homeassistant-frontend/src/components/ha-md-menu-item"; +import { showConfirmationDialog } from "../../homeassistant-frontend/src/dialogs/generic/show-dialog-box"; +import "../../homeassistant-frontend/src/layouts/hass-error-screen"; +import "../../homeassistant-frontend/src/layouts/hass-loading-screen"; +import "../../homeassistant-frontend/src/layouts/hass-subpage"; +import type { HomeAssistant, Route } from "../../homeassistant-frontend/src/types"; +import { showHacsDownloadDialog } from "../components/dialogs/show-hacs-dialog"; +import { repositoryMenuItems } from "../components/hacs-repository-owerflow-menu"; +import type { Hacs } from "../data/hacs"; +import type { RepositoryBase, RepositoryInfo } from "../data/repository"; +import { fetchRepositoryInformation } from "../data/repository"; +import { getRepositories, repositoryAdd } from "../data/websocket"; +import { HacsStyles } from "../styles/hacs-common-style"; +import { markdownWithRepositoryContext } from "../tools/markdown"; + +@customElement("hacs-repository-dashboard") +export class HacsRepositoryDashboard extends LitElement { + @property({ attribute: false }) public hacs!: Hacs; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public narrow!: boolean; + + @property({ attribute: false }) public isWide!: boolean; + + @property({ attribute: false }) public route!: Route; + + @state() public _repository?: RepositoryInfo; + + @state() private _error?: string; + + @query("#overflow-menu") + private _repositoryOverflowMenu!: HaMenu; + + public connectedCallback() { + super.connectedCallback(); + document.body.addEventListener("keydown", this._generateMyLink); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + document.body.removeEventListener("keydown", this._generateMyLink); + } + + private _generateMyLink = (ev: KeyboardEvent) => { + if (ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.altKey) { + // Ignore if modifier keys are pressed + return; + } + if (ev.key === "m" && mainWindow.location.pathname.startsWith("/hacs/repository/")) { + if (!this._repository) { + return; + } + const myParams = new URLSearchParams({ + redirect: "hacs_repository", + owner: this._repository!.full_name.split("/")[0], + repository: this._repository!.full_name.split("/")[1], + category: this._repository!.category, + }); + window.open(`https://my.home-assistant.io/create-link/?${myParams.toString()}`, "_blank"); + } + }; + + protected async firstUpdated(changedProperties: PropertyValues): Promise { + super.firstUpdated(changedProperties); + + const params = extractSearchParamsObject(); + if (Object.entries(params).length) { + let existing: RepositoryBase | undefined; + const requestedRepository = `${params.owner}/${params.repository}`; + existing = this.hacs.repositories.find( + (repository) => + repository.full_name.toLocaleLowerCase() === requestedRepository.toLocaleLowerCase(), + ); + if (!existing && params.category) { + if ( + !(await showConfirmationDialog(this, { + title: this.hacs.localize("my.add_repository_title"), + text: this.hacs.localize("my.add_repository_description", { + repository: requestedRepository, + }), + confirmText: this.hacs.localize("common.add"), + dismissText: this.hacs.localize("common.cancel"), + })) + ) { + this._error = this.hacs.localize("my.repository_not_found", { + repository: requestedRepository, + }); + return; + } + try { + await repositoryAdd(this.hass, requestedRepository, params.category); + this.hacs.repositories = await getRepositories(this.hass); + existing = this.hacs.repositories.find( + (repository) => + repository.full_name.toLocaleLowerCase() === requestedRepository.toLocaleLowerCase(), + ); + } catch (err: any) { + this._error = err; + return; + } + } + if (existing) { + this._fetchRepository(String(existing.id)); + } else { + this._error = this.hacs.localize("my.repository_not_found", { + repository: requestedRepository, + }); + } + } else { + const dividerPos = this.route.path.indexOf("/", 1); + const repositoryId = this.route.path.substr(dividerPos + 1); + if (!repositoryId) { + this._error = "Missing repositoryId from route"; + return; + } + this._fetchRepository(repositoryId); + } + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (changedProps.has("hass") && this._repository) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (oldHass && oldHass.language !== this.hass.language) { + this._fetchRepository(); + } + } + } + + private async _fetchRepository(repositoryId?: string) { + try { + this._repository = await fetchRepositoryInformation( + this.hass, + repositoryId || String(this._repository!.id), + this.hass.language, + ); + } catch (err: any) { + this._error = err?.message; + } + } + + private _getAuthors = memoizeOne((repository: RepositoryInfo) => { + const authors: string[] = []; + if (!repository.authors) return authors; + repository.authors.forEach((author) => authors.push(author.replace("@", ""))); + if (authors.length === 0) { + const author = repository.full_name.split("/")[0]; + if ( + ["custom-cards", "custom-components", "home-assistant-community-themes"].includes(author) + ) { + return authors; + } + authors.push(author); + } + return authors; + }); + + protected render(): TemplateResult { + if (this._error) { + return html``; + } + + if (!this._repository) { + return html``; + } + + const authors = this._getAuthors(this._repository); + + return html` + + +
    + + + ${this._repository.installed + ? html` + + + + ` + : ""} + ${authors + ? authors.map( + (author) => + html` + + + @${author} + + `, + ) + : ""} + ${this._repository.downloads + ? html` + + ` + : ""} + + + ${this._repository.stars} + + + + + ${this._repository.issues} + + + + + +
    + + ${!this._repository.installed_version + ? html` + + ` + : ""} +
    + + ${repositoryMenuItems(this, this._repository, this.hacs.localize).map((entry) => + entry.divider + ? html`
  • ` + : html` + { + entry?.action && entry.action(); + }} + > + +
    ${entry.label}
    +
    + `, + )} +
    + `; + } + + private _showOverflowRepositoryMenu = (ev: any) => { + if ( + this._repositoryOverflowMenu.open && + ev.target === this._repositoryOverflowMenu.anchorElement + ) { + this._repositoryOverflowMenu.close(); + return; + } + this._repositoryOverflowMenu.anchorElement = ev.target; + this._repositoryOverflowMenu.show(); + }; + + private _downloadRepositoryDialog() { + showHacsDownloadDialog(this, { + hacs: this.hacs, + repositoryId: this._repository!.id, + repository: this._repository!, + }); + } + + static get styles() { + return [ + HacsStyles, + css` + hass-loading-screen { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + height: 100vh; + } + + hass-subpage { + position: absolute; + width: 100vw; + } + + ha-fab ha-svg-icon { + color: var(--hcv-text-color-on-background); + } + + ha-fab { + position: fixed; + float: right; + right: calc(18px + env(safe-area-inset-right)); + bottom: calc(16px + env(safe-area-inset-bottom)); + z-index: 1; + } + + ha-fab.rtl { + float: left; + right: auto; + left: calc(18px + env(safe-area-inset-left)); + } + + ha-card { + display: block; + padding: 16px; + } + .content { + margin: auto; + padding: 8px; + max-width: 1536px; + } + + ha-chip-set { + padding-bottom: 8px; + } + + @media all and (max-width: 500px) { + .content { + margin: 8px 4px 64px; + max-width: none; + } + } + `, + ]; + } +} diff --git a/src/data/repository.ts b/src/data/repository.ts index e83acd2a0..d382243bd 100644 --- a/src/data/repository.ts +++ b/src/data/repository.ts @@ -1,78 +1,83 @@ -import type { HomeAssistant } from "../../homeassistant-frontend/src/types"; - -export type RepositoryType = - | "appdaemon" - | "integration" - | "netdaemon" - | "plugin" - | "python_script" - | "template" - | "theme"; - -export interface RepositoryBase { - authors: string[]; - available_version: string; - can_download: boolean; - category: RepositoryType; - config_flow: boolean; - country: string[]; - custom: boolean; - description: string; - domain: string | null; - downloads: number; - file_name: string; - full_name: string; - hide: boolean; - homeassistant: string | null; - id: string; - installed_version: string; - installed: boolean; - last_updated: string; - local_path: string; - name: string; - new: boolean; - pending_upgrade: boolean; - stars: number; - state: string; - status: "pending-restart" | "pending-upgrade" | "new" | "installed" | "default"; - topics: string[]; -} - -export interface RepositoryInfo extends RepositoryBase { - additional_info: string; - default_branch: string; - hide_default_branch: boolean; - issues: number; - releases: string[]; - ref: string; - selected_tag: string | null; - version_or_commit: "version" | "commit"; -} - -export const fetchRepositoryInformation = async ( - hass: HomeAssistant, - repositoryId: string, -): Promise => - hass.connection.sendMessagePromise({ - type: "hacs/repository/info", - repository_id: repositoryId, - }); - -export const repositoryDownloadVersion = async ( - hass: HomeAssistant, - repository: string, - version?: string, -) => - hass.connection.sendMessagePromise({ - type: "hacs/repository/download", - repository: repository, - version, - }); - -export const repositoryReleases = async (hass: HomeAssistant, repositoryId: string) => - hass.connection.sendMessagePromise< - { tag: string; name: string; published_at: string; prerelease: boolean }[] - >({ - type: "hacs/repository/releases", - repository_id: repositoryId, - }); +import type { HomeAssistant } from "../../homeassistant-frontend/src/types"; + +export type RepositoryType = + | "appdaemon" + | "integration" + | "netdaemon" + | "plugin" + | "python_script" + | "template" + | "theme"; + +export interface RepositoryBase { + authors: string[]; + available_version: string; + can_download: boolean; + category: RepositoryType; + config_flow: boolean; + country: string[]; + custom: boolean; + description: string; + domain: string | null; + downloads: number; + file_name: string; + full_name: string; + hide: boolean; + homeassistant: string | null; + id: string; + installed_version: string; + installed: boolean; + last_updated: string; + local_path: string; + name: string; + new: boolean; + pending_upgrade: boolean; + stars: number; + state: string; + status: "pending-restart" | "pending-upgrade" | "new" | "installed" | "default"; + topics: string[]; +} + +export interface RepositoryInfo extends RepositoryBase { + additional_info: string; + default_branch: string; + hide_default_branch: boolean; + issues: number; + releases: string[]; + ref: string; + selected_tag: string | null; + version_or_commit: "version" | "commit"; +} + +export const fetchRepositoryInformation = async ( + hass: HomeAssistant, + repositoryId: string, + language?: string, +): Promise => { + const message: any = { + type: "hacs/repository/info", + repository_id: repositoryId, + language: language ?? hass.language, + }; + + return hass.connection.sendMessagePromise(message); +}; + +export const repositoryDownloadVersion = async ( + hass: HomeAssistant, + repository: string, + version?: string, +) => + hass.connection.sendMessagePromise({ + type: "hacs/repository/download", + repository: repository, + version, + }); + +export const repositoryReleases = async (hass: HomeAssistant, repositoryId: string) => + hass.connection.sendMessagePromise< + { tag: string; name: string; published_at: string; prerelease: boolean }[] + >({ + type: "hacs/repository/releases", + repository_id: repositoryId, + }); diff --git a/src/data/websocket.ts b/src/data/websocket.ts index 3757c19d6..e729363da 100644 --- a/src/data/websocket.ts +++ b/src/data/websocket.ts @@ -1,61 +1,61 @@ -import type { HomeAssistant } from "../../homeassistant-frontend/src/types"; -import type { Hacs, HacsInfo } from "./hacs"; -import type { HacsDispatchEvent } from "./common"; -import type { RepositoryBase } from "./repository"; - -export const fetchHacsInfo = async (hass: HomeAssistant) => - hass.connection.sendMessagePromise({ - type: "hacs/info", - }); - -export const getRepositories = async (hass: HomeAssistant) => - hass.connection.sendMessagePromise({ - type: "hacs/repositories/list", - }); - -export const repositoryUninstall = async (hass: HomeAssistant, repository: string) => - hass.connection.sendMessagePromise({ - type: "hacs/repository/remove", - repository, - }); - -export const repositoryAdd = async (hass: HomeAssistant, repository: string, category: string) => - hass.connection.sendMessagePromise>({ - type: "hacs/repositories/add", - repository: repository, - category, - }); - -export const repositoryUpdate = async (hass: HomeAssistant, repository: string) => - hass.connection.sendMessagePromise({ - type: "hacs/repository/refresh", - repository, - }); - -export const repositoryDelete = async (hass: HomeAssistant, repository: string) => - hass.connection.sendMessagePromise({ - type: "hacs/repositories/remove", - repository, - }); - -export const repositoriesClearNew = async (hass: HomeAssistant, hacs: Hacs) => - hass.connection.sendMessagePromise({ - type: "hacs/repositories/clear_new", - categories: hacs.info.categories, - }); - -export const repositoriesClearNewRepository = async (hass: HomeAssistant, repository: string) => - hass.connection.sendMessagePromise({ - type: "hacs/repositories/clear_new", - repository, - }); - -export const websocketSubscription = ( - hass: HomeAssistant, - onChange: (result: Record | null) => void, - event: HacsDispatchEvent, -) => - hass.connection.subscribeMessage(onChange, { - type: "hacs/subscribe", - signal: event, - }); +import type { HomeAssistant } from "../../homeassistant-frontend/src/types"; +import type { Hacs, HacsInfo } from "./hacs"; +import type { HacsDispatchEvent } from "./common"; +import type { RepositoryBase } from "./repository"; + +export const fetchHacsInfo = async (hass: HomeAssistant) => + hass.connection.sendMessagePromise({ + type: "hacs/info", + }); + +export const getRepositories = async (hass: HomeAssistant) => + hass.connection.sendMessagePromise({ + type: "hacs/repositories/list", + }); + +export const repositoryUninstall = async (hass: HomeAssistant, repository: string) => + hass.connection.sendMessagePromise({ + type: "hacs/repository/remove", + repository, + }); + +export const repositoryAdd = async (hass: HomeAssistant, repository: string, category: string) => + hass.connection.sendMessagePromise>({ + type: "hacs/repositories/add", + repository: repository, + category, + }); + +export const repositoryUpdate = async (hass: HomeAssistant, repository: string) => + hass.connection.sendMessagePromise({ + type: "hacs/repository/refresh", + repository, + }); + +export const repositoryDelete = async (hass: HomeAssistant, repository: string) => + hass.connection.sendMessagePromise({ + type: "hacs/repositories/remove", + repository, + }); + +export const repositoriesClearNew = async (hass: HomeAssistant, hacs: Hacs) => + hass.connection.sendMessagePromise({ + type: "hacs/repositories/clear_new", + categories: hacs.info.categories, + }); + +export const repositoriesClearNewRepository = async (hass: HomeAssistant, repository: string) => + hass.connection.sendMessagePromise({ + type: "hacs/repositories/clear_new", + repository, + }); + +export const websocketSubscription = ( + hass: HomeAssistant, + onChange: (result: Record | null) => void, + event: HacsDispatchEvent, +) => + hass.connection.subscribeMessage(onChange, { + type: "hacs/subscribe", + signal: event, + });