diff --git a/src/components/dialogs/hacs-custom-repositories-dialog.ts b/src/components/dialogs/hacs-custom-repositories-dialog.ts index 16419b3f..e7cdd5b3 100644 --- a/src/components/dialogs/hacs-custom-repositories-dialog.ts +++ b/src/components/dialogs/hacs-custom-repositories-dialog.ts @@ -91,8 +91,8 @@ export class HacsCustomRepositoriesDialog extends HacsDialogBase { ${repo.full_name} (${repo.category}) { - ev.stopPropagation(); + @click=${(ev: Event) => { + ev.preventDefault(); this._removeRepository(String(repo.id)); }} > diff --git a/src/hacs-my-redirect.ts b/src/hacs-my-redirect.ts new file mode 100644 index 00000000..c360c5cd --- /dev/null +++ b/src/hacs-my-redirect.ts @@ -0,0 +1,100 @@ +import { html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { navigate } from "../homeassistant-frontend/src/common/navigate"; +import { + createSearchParam, + extractSearchParamsObject, +} from "../homeassistant-frontend/src/common/url/search-params"; +import "../homeassistant-frontend/src/layouts/hass-error-screen"; +import { + ParamType, + Redirect, + Redirects, +} from "../homeassistant-frontend/src/panels/my/ha-panel-my"; +import { HomeAssistant, Route } from "../homeassistant-frontend/src/types"; +import { Hacs } from "./data/hacs"; + +export const REDIRECTS: Redirects = { + hacs_repository: { + redirect: "/hacs/repository", + params: { + owner: "string", + repository: "string", + category: "string?", + }, + }, +}; + +@customElement("hacs-my-redirect") +class HacsMyRedirect extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public hacs!: Hacs; + + @property({ attribute: false }) public route!: Route; + + @state() private _error?: TemplateResult | string; + + protected firstUpdated(_changedProperties): void { + const dividerPos = this.route.path.indexOf("/", 1); + const path = this.route.path.substr(dividerPos + 1); + const redirect = REDIRECTS[path]; + + if (!redirect) { + this._error = this.hacs.localize("my.not_supported", { + link: html` + ${this.hacs.localize("my.faq_link")} + `, + }); + return; + } + + let url: string; + try { + url = this._createRedirectUrl(redirect); + } catch (err: any) { + this._error = this.hacs.localize("my.error"); + return; + } + + navigate(url, { replace: true }); + } + + protected render(): TemplateResult { + if (this._error) { + return html``; + } + return html``; + } + + private _createRedirectUrl(redirect: Redirect): string { + const params = this._createRedirectParams(redirect); + return `${redirect.redirect}${params}`; + } + + private _createRedirectParams(redirect: Redirect): string { + const params = extractSearchParamsObject(); + if (!redirect.params && !Object.keys(params).length) { + return ""; + } + const resultParams = {}; + for (const [key, type] of Object.entries(redirect.params || {})) { + if (!params[key] && type.endsWith("?")) { + continue; + } + if (!params[key] || !this._checkParamType(type, params[key])) { + throw Error(); + } + resultParams[key] = params[key]; + } + return `?${createSearchParam(resultParams)}`; + } + + private _checkParamType(type: ParamType, _value: string) { + return type === "string" || type === "string?"; + } +} diff --git a/src/hacs-router.ts b/src/hacs-router.ts index a6316c8d..f0be5cdd 100644 --- a/src/hacs-router.ts +++ b/src/hacs-router.ts @@ -52,7 +52,12 @@ class HacsRouter extends HassRouterPage { protected routerOptions: RouterOptions = { defaultPage: "entry", + showLoading: true, routes: { + _my_redirect: { + tag: "hacs-my-redirect", + load: () => import("./hacs-my-redirect"), + }, entry: { tag: "hacs-entry-panel", load: () => import("./panels/hacs-entry-panel"), diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 8d9ef2a6..4d1e219d 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -49,6 +49,15 @@ }, "pending_repository_upgrade": "You are running version {downloaded}, version {available} is available" }, + "my": { + "not_supported": "This redirect is not supported. Check the {link} for the supported redirects and the version they where introduced.", + "documentation": "documentation", + "faq_link": "My Home Assistant FAQ", + "error": "An unknown error occurred", + "repository_not_found": "Repository {repository} not found", + "add_repository_title": "Add custom repository", + "add_repository_description": "This will add the custom repository '{repository}' to be tracked by HACS, do you want to add it?" + }, "store": { "explore": "Explore & download repositories", "new_repositories_note": "There are some new repositories showing here", @@ -222,4 +231,4 @@ "information": "Information", "pending_updates": "Pending updates" } -} +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 7cf05bff..8ee0dd34 100644 --- a/src/main.ts +++ b/src/main.ts @@ -109,7 +109,7 @@ class HacsFrontend extends HacsElement { // Ignore if modifier keys are pressed return; } - if (["c", "m", "e"].includes(ev.key)) { + if (["c", "e"].includes(ev.key)) { // @ts-ignore fireEvent(mainWindow, "hass-quick-bar-trigger", ev, { bubbles: false, diff --git a/src/panels/hacs-repository-panel.ts b/src/panels/hacs-repository-panel.ts index 539b7c64..5d00a45a 100644 --- a/src/panels/hacs-repository-panel.ts +++ b/src/panels/hacs-repository-panel.ts @@ -20,6 +20,7 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { mainWindow } from "../../homeassistant-frontend/src/common/dom/get_main_window"; import { navigate } from "../../homeassistant-frontend/src/common/navigate"; +import { extractSearchParamsObject } from "../../homeassistant-frontend/src/common/url/search-params"; import "../../homeassistant-frontend/src/components/ha-fab"; import "../../homeassistant-frontend/src/components/ha-icon-overflow-menu"; import { getConfigEntries } from "../../homeassistant-frontend/src/data/config_entries"; @@ -31,8 +32,13 @@ import { HomeAssistant, Route } from "../../homeassistant-frontend/src/types"; import "../components/hacs-filter"; import "../components/hacs-repository-card"; import { Hacs } from "../data/hacs"; -import { fetchRepositoryInformation, RepositoryInfo } from "../data/repository"; -import { repositoryUninstall, repositoryUpdate } from "../data/websocket"; +import { fetchRepositoryInformation, RepositoryBase, RepositoryInfo } from "../data/repository"; +import { + getRepositories, + repositoryAdd, + repositoryUninstall, + repositoryUpdate, +} from "../data/websocket"; import { HacsStyles } from "../styles/hacs-common-style"; import { markdown } from "../tools/markdown/markdown"; @@ -52,15 +58,76 @@ export class HacsRepositoryPanel extends LitElement { @state() private _error?: string; - protected firstUpdated(changedProperties: PropertyValues): void { + protected async firstUpdated(changedProperties: PropertyValues): Promise { super.firstUpdated(changedProperties); - const dividerPos = this.route.path.indexOf("/", 1); - const repositoryId = this.route.path.substr(dividerPos + 1); - if (!repositoryId) { - this._error = "Missing repositoryId from route"; - return; + document.body.addEventListener("keydown", (ev: KeyboardEvent) => { + if (ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.altKey) { + // Ignore if modifier keys are pressed + return; + } + if (["m"].includes(ev.key)) { + const myParams = new URLSearchParams({ + redirect: "hacs_repository", + owner: this._repository!.full_name.split("/")[0], + repository: this._repository!.full_name.split("/")[0], + category: this._repository!.category, + }); + window.open(`https://my.home-assistant.io/create-link/?${myParams.toString()}`, "_blank"); + } + }); + + 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); } - this._fetchRepository(repositoryId); } protected updated(changedProps) {