ha-frontend/src/panels/config/cloud/account/dialog-cloud-tts-try.ts

243 lines
7.0 KiB
TypeScript

import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { mdiPlayCircleOutline, mdiRobot } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-select";
import "../../../../components/ha-textarea";
import type { HaTextArea } from "../../../../components/ha-textarea";
import { showAutomationEditor } from "../../../../data/automation";
import { MediaPlayerEntityFeature } from "../../../../data/media-player";
import { convertTextToSpeech } from "../../../../data/tts";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { TryTtsDialogParams } from "./show-dialog-cloud-tts-try";
@customElement("dialog-cloud-try-tts")
export class DialogTryTts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _loadingExample = false;
@state() private _params?: TryTtsDialogParams;
@query("#message") private _messageInput?: HaTextArea;
@storage({
key: "cloudTtsTryMessage",
state: false,
subscribe: false,
})
private _message!: string;
@storage({
key: "cloudTtsTryTarget",
state: false,
subscribe: false,
})
private _target!: string;
public showDialog(params: TryTtsDialogParams) {
this._params = params;
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
const target = this._target || "browser";
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.cloud.account.tts.dialog.header")
)}
>
<div>
<ha-textarea
autogrow
id="message"
label="Message"
.value=${this._message ||
this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.example_message",
{ name: this.hass.user!.name }
)}
>
</ha-textarea>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.target"
)}
id="target"
.value=${target}
@selected=${this._handleTargetChanged}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
>
<mwc-list-item value="browser">
${this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.target_browser"
)}
</mwc-list-item>
${Object.values(this.hass.states)
.filter(
(entity) =>
computeStateDomain(entity) === "media_player" &&
supportsFeature(entity, MediaPlayerEntityFeature.PLAY_MEDIA)
)
.map(
(entity) => html`
<mwc-list-item .value=${entity.entity_id}>
${computeStateName(entity)}
</mwc-list-item>
`
)}
</ha-select>
</div>
<mwc-button
slot="primaryAction"
.label=${this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.play"
)}
@click=${this._playExample}
.disabled=${this._loadingExample}
>
<ha-svg-icon slot="icon" .path=${mdiPlayCircleOutline}></ha-svg-icon>
</mwc-button>
<mwc-button
slot="secondaryAction"
.disabled=${target === "browser"}
.label=${this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.create_automation"
)}
@click=${this._createAutomation}
>
<ha-svg-icon slot="icon" .path=${mdiRobot}></ha-svg-icon>
</mwc-button>
</ha-dialog>
`;
}
private _handleTargetChanged(ev) {
this._target = ev.target.value;
this.requestUpdate("_target");
}
private async _playExample() {
const message = this._messageInput?.value;
if (!message) {
return;
}
this._message = message;
if (this._target === "browser") {
// We create the audio element here + do a play, because iOS requires it to be done by user action
const audio = new Audio();
audio.play();
this._playBrowser(message, audio);
} else {
this.hass.callService("tts", "cloud_say", {
entity_id: this._target,
message,
});
}
}
private _createAutomation() {
const message = this._messageInput!.value!;
this._message = message;
showAutomationEditor({
action: [
{
service: "tts.cloud_say",
data: {
entity_id: this._target,
message: message,
},
},
],
});
this.closeDialog();
}
private async _playBrowser(message: string, audio: HTMLAudioElement) {
this._loadingExample = true;
const language = this._params!.defaultVoice[0];
const voice = this._params!.defaultVoice[1];
let url;
try {
const result = await convertTextToSpeech(this.hass, {
platform: "cloud",
message,
language,
options: { voice },
});
url = result.path;
} catch (err: any) {
this._loadingExample = false;
showAlertDialog(this, {
text: `Unable to load example. ${err.error || err.body || err}`,
warning: true,
});
return;
}
audio.src = url;
audio.addEventListener("canplaythrough", () => {
audio.play();
});
audio.addEventListener("playing", () => {
this._loadingExample = false;
});
audio.addEventListener("error", () => {
showAlertDialog(this, { title: "Error playing audio." });
this._loadingExample = false;
});
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
ha-textarea,
ha-select {
width: 100%;
}
ha-select {
margin-top: 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-cloud-try-tts": DialogTryTts;
}
}