mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-02 14:07:55 +00:00
Initial Implementation
This commit is contained in:
parent
fc06b9c29d
commit
43246029a1
288
src/data/quick-bar.ts
Normal file
288
src/data/quick-bar.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import {
|
||||
mdiServerNetwork,
|
||||
mdiNavigationVariantOutline,
|
||||
mdiEarth,
|
||||
mdiReload,
|
||||
} from "@mdi/js";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { componentsWithService } from "../common/config/components_with_service";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { domainIcon } from "../common/entity/domain_icon";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { ScorableTextItem } from "../common/string/filter/sequence-matching";
|
||||
import {
|
||||
ConfirmationDialogParams,
|
||||
showConfirmationDialog,
|
||||
} from "../dialogs/generic/show-dialog-box";
|
||||
import { QuickBar } from "../dialogs/quick-bar/ha-quick-bar";
|
||||
import { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../panels/config/ha-panel-config";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { domainToName } from "./integration";
|
||||
import { getPanelNameTranslationKey } from "./panel";
|
||||
|
||||
export interface QuickBarItem extends ScorableTextItem {
|
||||
primaryText: string;
|
||||
primaryTextAlt?: string;
|
||||
secondaryText: string;
|
||||
categoryKey:
|
||||
| "reload"
|
||||
| "navigation"
|
||||
| "server_control"
|
||||
| "entity"
|
||||
| "suggestion";
|
||||
action(data?: any): void;
|
||||
iconPath?: string;
|
||||
icon?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export type NavigationInfo = PageNavigation &
|
||||
Pick<QuickBarItem, "primaryText" | "secondaryText">;
|
||||
|
||||
export type BaseNavigationCommand = Pick<QuickBarItem, "primaryText" | "path">;
|
||||
|
||||
export const generateEntityItems = (
|
||||
element: QuickBar,
|
||||
hass: HomeAssistant
|
||||
): QuickBarItem[] =>
|
||||
Object.keys(hass.states)
|
||||
.map((entityId) => {
|
||||
const entityState = hass.states[entityId];
|
||||
const entityItem = {
|
||||
primaryText: computeStateName(entityState),
|
||||
primaryTextAlt: entityId,
|
||||
secondaryText: hass.userData?.showAdvanced
|
||||
? entityId
|
||||
: computeStateDisplay(hass.localize, entityState, hass.locale),
|
||||
icon: entityState.attributes.icon,
|
||||
iconPath: entityState.attributes.icon
|
||||
? undefined
|
||||
: domainIcon(computeDomain(entityId), entityState),
|
||||
action: () => fireEvent(element, "hass-more-info", { entityId }),
|
||||
categoryKey: "entity" as const,
|
||||
};
|
||||
|
||||
return {
|
||||
...entityItem,
|
||||
strings: [entityItem.primaryText, entityItem.secondaryText],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.primaryText, b.primaryText));
|
||||
|
||||
export const generateCommandItems = (
|
||||
element: QuickBar,
|
||||
hass: HomeAssistant
|
||||
): Array<QuickBarItem[]> => [
|
||||
generateNavigationCommands(hass),
|
||||
generateReloadCommands(hass),
|
||||
generateServerControlCommands(element, hass),
|
||||
];
|
||||
|
||||
export const generateReloadCommands = (hass: HomeAssistant): QuickBarItem[] => {
|
||||
// Get all domains that have a direct "reload" service
|
||||
const reloadableDomains = componentsWithService(hass, "reload");
|
||||
|
||||
const commands = reloadableDomains.map((domain) => ({
|
||||
primaryText:
|
||||
hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||
"domain",
|
||||
domainToName(hass.localize, domain)
|
||||
),
|
||||
action: () => hass.callService(domain, "reload"),
|
||||
secondaryText: "Reload changes made to the domain file",
|
||||
}));
|
||||
|
||||
// Add "frontend.reload_themes"
|
||||
commands.push({
|
||||
primaryText: hass.localize("ui.dialogs.quick-bar.commands.reload.themes"),
|
||||
action: () => hass.callService("frontend", "reload_themes"),
|
||||
secondaryText: "Reload changes made to themes.yaml",
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_core_config"
|
||||
commands.push({
|
||||
primaryText: hass.localize("ui.dialogs.quick-bar.commands.reload.core"),
|
||||
action: () => hass.callService("homeassistant", "reload_core_config"),
|
||||
secondaryText: "Reload changes made to configuration.yaml",
|
||||
});
|
||||
|
||||
return commands.map((command) => ({
|
||||
...command,
|
||||
categoryKey: "reload",
|
||||
iconPath: mdiReload,
|
||||
strings: [
|
||||
`${hass.localize("ui.dialogs.quick-bar.commands.types.reload")} ${
|
||||
command.primaryText
|
||||
}`,
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
export const generateServerControlCommands = (
|
||||
element: QuickBar,
|
||||
hass: HomeAssistant
|
||||
): QuickBarItem[] => {
|
||||
const serverActions = ["restart", "stop"];
|
||||
|
||||
return serverActions.map((action) => {
|
||||
const categoryKey: QuickBarItem["categoryKey"] = "server_control";
|
||||
|
||||
const item = {
|
||||
primaryText: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
hass.localize(`ui.dialogs.quick-bar.commands.server_control.${action}`)
|
||||
),
|
||||
categoryKey,
|
||||
action: () => hass.callService("homeassistant", action),
|
||||
};
|
||||
|
||||
return generateConfirmationCommand(
|
||||
element,
|
||||
{
|
||||
...item,
|
||||
strings: [
|
||||
`${hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
)} ${item.primaryText}`,
|
||||
],
|
||||
secondaryText: "Control your server",
|
||||
iconPath: mdiServerNetwork,
|
||||
},
|
||||
hass.localize("ui.dialogs.generic.ok")
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const generateNavigationCommands = (
|
||||
hass: HomeAssistant
|
||||
): QuickBarItem[] => {
|
||||
const panelItems = generateNavigationPanelCommands(hass);
|
||||
const sectionItems = generateNavigationConfigSectionCommands(hass);
|
||||
|
||||
return finalizeNavigationCommands([...panelItems, ...sectionItems], hass);
|
||||
};
|
||||
|
||||
export const generateNavigationPanelCommands = (
|
||||
hass: HomeAssistant
|
||||
): BaseNavigationCommand[] =>
|
||||
Object.keys(hass.panels)
|
||||
.filter((panelKey) => panelKey !== "_my_redirect")
|
||||
.map((panelKey) => {
|
||||
const panel = hass.panels[panelKey];
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
|
||||
const primaryText =
|
||||
hass.localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
return {
|
||||
primaryText,
|
||||
path: `/${panel.url_path}`,
|
||||
icon: panel.icon,
|
||||
secondaryText: "Panel",
|
||||
};
|
||||
});
|
||||
|
||||
export const generateNavigationConfigSectionCommands = (
|
||||
hass: HomeAssistant
|
||||
): BaseNavigationCommand[] => {
|
||||
const items: NavigationInfo[] = [];
|
||||
|
||||
for (const sectionKey of Object.keys(configSections)) {
|
||||
for (const page of configSections[sectionKey]) {
|
||||
if (!canShowPage(hass, page)) {
|
||||
continue;
|
||||
}
|
||||
if (!page.component) {
|
||||
continue;
|
||||
}
|
||||
const info = getNavigationInfoFromConfig(page, hass);
|
||||
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (
|
||||
items.some(
|
||||
(e) => e.path === info.path && e.component === info.component
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconPath: mdiNavigationVariantOutline,
|
||||
...info,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const getNavigationInfoFromConfig = (
|
||||
page: PageNavigation,
|
||||
hass: HomeAssistant
|
||||
): NavigationInfo | undefined => {
|
||||
if (!page.component) {
|
||||
return undefined;
|
||||
}
|
||||
const caption = hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
|
||||
);
|
||||
|
||||
if (page.translationKey && caption) {
|
||||
return {
|
||||
...page,
|
||||
primaryText: caption,
|
||||
secondaryText: "Configuration Page",
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const generateConfirmationCommand = (
|
||||
element: QuickBar,
|
||||
item: QuickBarItem,
|
||||
confirmText: ConfirmationDialogParams["confirmText"]
|
||||
): QuickBarItem => ({
|
||||
...item,
|
||||
action: () =>
|
||||
showConfirmationDialog(element, {
|
||||
confirmText,
|
||||
confirm: item.action,
|
||||
}),
|
||||
});
|
||||
|
||||
const finalizeNavigationCommands = (
|
||||
items: BaseNavigationCommand[],
|
||||
hass: HomeAssistant
|
||||
): QuickBarItem[] =>
|
||||
items.map((item) => {
|
||||
const categoryKey: QuickBarItem["categoryKey"] = "navigation";
|
||||
|
||||
const navItem = {
|
||||
secondaryText: "Navigation",
|
||||
iconPath: mdiEarth,
|
||||
...item,
|
||||
action: () => navigate(item.path!),
|
||||
};
|
||||
|
||||
return {
|
||||
categoryKey,
|
||||
...navItem,
|
||||
strings: [
|
||||
`${hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
)} ${navItem.primaryText}`,
|
||||
],
|
||||
};
|
||||
});
|
@ -2,97 +2,39 @@ import "@lit-labs/virtualizer";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import type { ListItem } from "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
mdiClose,
|
||||
mdiEarth,
|
||||
mdiMagnify,
|
||||
mdiNavigationVariantOutline,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
} from "@mdi/js";
|
||||
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { canShowPage } from "../../common/config/can_show_page";
|
||||
import { componentsWithService } from "../../common/config/components_with_service";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { domainIcon } from "../../common/entity/domain_icon";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
fuzzyFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../../common/string/filter/sequence-matching";
|
||||
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import "../../components/ha-chip";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-header-bar";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-textfield";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../../panels/config/ha-panel-config";
|
||||
import {
|
||||
generateCommandItems,
|
||||
generateEntityItems,
|
||||
QuickBarItem,
|
||||
} from "../../data/quick-bar";
|
||||
import {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
haStyleScrollbar,
|
||||
} from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import {
|
||||
ConfirmationDialogParams,
|
||||
showConfirmationDialog,
|
||||
} from "../generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { QuickBarParams } from "./show-dialog-quick-bar";
|
||||
|
||||
interface QuickBarItem extends ScorableTextItem {
|
||||
primaryText: string;
|
||||
iconPath?: string;
|
||||
altText?: string;
|
||||
action(data?: any): void;
|
||||
}
|
||||
|
||||
interface CommandItem extends QuickBarItem {
|
||||
altText: string;
|
||||
icon?: string;
|
||||
categoryKey: "reload" | "navigation" | "server_control";
|
||||
categoryText: string;
|
||||
}
|
||||
|
||||
interface EntityItem extends QuickBarItem {
|
||||
altText: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const isCommandItem = (item: QuickBarItem): item is CommandItem =>
|
||||
(item as CommandItem).categoryKey !== undefined;
|
||||
|
||||
interface QuickBarNavigationItem extends CommandItem {
|
||||
path: string;
|
||||
}
|
||||
|
||||
type NavigationInfo = PageNavigation &
|
||||
Pick<QuickBarItem, "primaryText" | "altText">;
|
||||
|
||||
type BaseNavigationCommand = Pick<
|
||||
QuickBarNavigationItem,
|
||||
"primaryText" | "path"
|
||||
>;
|
||||
|
||||
@customElement("ha-quick-bar")
|
||||
export class QuickBar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _commandItems?: CommandItem[];
|
||||
@state() private _items?: Array<QuickBarItem[]>;
|
||||
|
||||
@state() private _entityItems?: EntityItem[];
|
||||
|
||||
@state() private _items?: (EntityItem | CommandItem)[];
|
||||
private _filteredItems?: QuickBarItem[];
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
@ -122,13 +64,18 @@ export class QuickBar extends LitElement {
|
||||
|
||||
private _focusListElement?: ListItem | null;
|
||||
|
||||
private _filterItems = memoizeOne(
|
||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
);
|
||||
|
||||
public async showDialog(params: QuickBarParams) {
|
||||
this._hint = params.hint;
|
||||
this._narrow = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
this._initializeItemsIfNeeded();
|
||||
this._open = true;
|
||||
this._initializeItems();
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@ -145,13 +92,25 @@ export class QuickBar extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
let items: QuickBarItem[] | undefined =
|
||||
this._filter === ""
|
||||
? this._suggestions
|
||||
: [...this._commandItems!, ...this._entityItems!];
|
||||
let sectionCount = 0;
|
||||
|
||||
if (items && this._filter && this._filter !== " ") {
|
||||
items = this._filterItems(items, this._filter);
|
||||
if (this._items && this._filter && this._filter !== "") {
|
||||
const newFilteredItems: QuickBarItem[] = [];
|
||||
this._items.forEach((arr) => {
|
||||
const items = this._filterItems(arr, this._filter).slice(0, 3);
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
sectionCount++;
|
||||
newFilteredItems.push(...items);
|
||||
});
|
||||
|
||||
this._filteredItems = newFilteredItems;
|
||||
} else {
|
||||
sectionCount++;
|
||||
this._filteredItems = this._suggestions;
|
||||
}
|
||||
|
||||
return html`
|
||||
@ -205,11 +164,11 @@ export class QuickBar extends LitElement {
|
||||
: ""}
|
||||
</ha-textfield>
|
||||
</div>
|
||||
${!items
|
||||
${!this._filteredItems
|
||||
? html`
|
||||
<ha-circular-progress size="small" active></ha-circular-progress>
|
||||
`
|
||||
: items.length === 0 && this._filter !== ""
|
||||
: this._filteredItems.length === 0 && this._filter !== ""
|
||||
? html`
|
||||
<div class="nothing-found">
|
||||
${this.hass.localize("ui.dialogs.quick-bar.nothing_found")}
|
||||
@ -249,11 +208,13 @@ export class QuickBar extends LitElement {
|
||||
height: this._narrow
|
||||
? "calc(100vh - 56px)"
|
||||
: `${Math.min(
|
||||
items.length * 54 + 26,
|
||||
this._done ? 500 : 0
|
||||
this._filteredItems.length * 72 +
|
||||
sectionCount * 37 +
|
||||
18,
|
||||
this._done ? 600 : 0
|
||||
)}px`,
|
||||
})}
|
||||
.items=${items}
|
||||
.items=${this._filteredItems}
|
||||
.renderItem=${this._renderItem}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
@ -265,101 +226,67 @@ export class QuickBar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _initializeItemsIfNeeded() {
|
||||
this._commandItems = this._commandItems || this._generateCommandItems();
|
||||
this._entityItems = this._entityItems || this._generateEntityItems();
|
||||
}
|
||||
|
||||
private _handleOpened() {
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
private async _handleRangeChanged(e) {
|
||||
if (this._focusSet) {
|
||||
return;
|
||||
}
|
||||
if (e.firstVisible > -1) {
|
||||
this._focusSet = true;
|
||||
await this.updateComplete;
|
||||
this._setFocusFirstListItem();
|
||||
}
|
||||
}
|
||||
|
||||
private _renderItem = (item: QuickBarItem, index: number): TemplateResult => {
|
||||
if (!item) {
|
||||
return html``;
|
||||
}
|
||||
return isCommandItem(item)
|
||||
? this._renderCommandItem(item, index)
|
||||
: this._renderEntityItem(item as EntityItem, index);
|
||||
};
|
||||
|
||||
private _renderEntityItem(item: EntityItem, index?: number) {
|
||||
const previous = this._filteredItems![index - 1];
|
||||
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.twoline=${Boolean(this.hass.userData?.showAdvanced && item.altText)}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
graphic="icon"
|
||||
class=${this.hass.userData?.showAdvanced && item.altText
|
||||
? "single-line"
|
||||
: ""}
|
||||
>
|
||||
${item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`}
|
||||
<span>${item.primaryText}</span>
|
||||
${item.altText && this.hass.userData?.showAdvanced
|
||||
<div class="entry-container" style="z-index: 5">
|
||||
${index === 0 || item?.categoryKey !== previous?.categoryKey
|
||||
? html`
|
||||
<span slot="secondary" class="item-text secondary"
|
||||
>${item.altText}</span
|
||||
>
|
||||
<div class="entry-title">
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${item.categoryKey}`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</mwc-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderCommandItem(item: CommandItem, index?: number) {
|
||||
return html`
|
||||
<mwc-list-item
|
||||
.item=${item}
|
||||
twoline
|
||||
index=${ifDefined(index)}
|
||||
class="command-item"
|
||||
graphic="icon"
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon
|
||||
.icon=${item.icon}
|
||||
class="entity"
|
||||
slot="graphic"
|
||||
></ha-icon>`
|
||||
: item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
|
||||
<span>${item.primaryText}</span>
|
||||
<span slot="secondary" class="secondary item-text"
|
||||
>${item.altText}</span
|
||||
<mwc-list-item
|
||||
.twoline=${item.secondaryText}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
graphic="icon"
|
||||
class=${item.secondaryText ? "single-line" : ""}
|
||||
>
|
||||
</mwc-list-item>
|
||||
${item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`}
|
||||
<span
|
||||
>${item.primaryText}
|
||||
<span class="secondary">${item.primaryTextAlt}</span></span
|
||||
>
|
||||
${item.secondaryText
|
||||
? html`
|
||||
<span slot="secondary" class="item-text secondary"
|
||||
>${item.secondaryText}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</mwc-list-item>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
private _initializeItems() {
|
||||
this._items = this._items || [
|
||||
generateEntityItems(this, this.hass),
|
||||
...generateCommandItems(this, this.hass),
|
||||
];
|
||||
}
|
||||
|
||||
private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
|
||||
if (!this._suggestions.includes(item)) {
|
||||
this._suggestions.unshift(item);
|
||||
this._suggestions.unshift({ ...item, categoryKey: "suggestion" });
|
||||
this._suggestions = this._suggestions.slice(0, 3);
|
||||
}
|
||||
|
||||
this._addSpinnerToCommandItem(index);
|
||||
this._addSpinnerToItem(index);
|
||||
|
||||
await item.action();
|
||||
this.closeDialog();
|
||||
@ -380,18 +307,6 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _getItemAtIndex(index: number): ListItem | null {
|
||||
return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
|
||||
}
|
||||
|
||||
private _addSpinnerToCommandItem(index: number): void {
|
||||
const spinner = document.createElement("ha-circular-progress");
|
||||
spinner.size = "small";
|
||||
spinner.slot = "meta";
|
||||
spinner.active = true;
|
||||
this._getItemAtIndex(index)?.appendChild(spinner);
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
const newFilter = (ev.currentTarget as any).value;
|
||||
let newSearch: string;
|
||||
@ -459,256 +374,59 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
const listItem = ev.target.closest("mwc-list-item");
|
||||
const target =
|
||||
ev.target.nodeName === "MWC-LIST-ITEM"
|
||||
? ev.target
|
||||
: ev.target.parentElement === "MWC-LIST-ITEM"
|
||||
? ev.target.parentElement
|
||||
: ev.target.parentElement.parentElement;
|
||||
|
||||
this.processItemAndCloseDialog(
|
||||
listItem.item,
|
||||
Number(listItem.getAttribute("index"))
|
||||
target.item,
|
||||
Number((target as HTMLElement).getAttribute("index"))
|
||||
);
|
||||
}
|
||||
|
||||
private _generateEntityItems(): EntityItem[] {
|
||||
return Object.keys(this.hass.states)
|
||||
.map((entityId) => {
|
||||
const entityState = this.hass.states[entityId];
|
||||
const entityItem = {
|
||||
primaryText: computeStateName(entityState),
|
||||
altText: entityId,
|
||||
icon: entityState.attributes.icon,
|
||||
iconPath: entityState.attributes.icon
|
||||
? undefined
|
||||
: domainIcon(computeDomain(entityId), entityState),
|
||||
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
||||
};
|
||||
|
||||
return {
|
||||
...entityItem,
|
||||
strings: [entityItem.primaryText, entityItem.altText],
|
||||
};
|
||||
})
|
||||
.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.primaryText, b.primaryText)
|
||||
);
|
||||
}
|
||||
|
||||
private _generateCommandItems(): CommandItem[] {
|
||||
return [
|
||||
...this._generateReloadCommands(),
|
||||
...this._generateServerControlCommands(),
|
||||
...this._generateNavigationCommands(),
|
||||
].sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.strings.join(" "), b.strings.join(" "))
|
||||
);
|
||||
}
|
||||
|
||||
private _generateReloadCommands(): CommandItem[] {
|
||||
// Get all domains that have a direct "reload" service
|
||||
const reloadableDomains = componentsWithService(this.hass, "reload");
|
||||
|
||||
const commands = reloadableDomains.map((domain) => ({
|
||||
primaryText:
|
||||
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
),
|
||||
action: () => this.hass.callService(domain, "reload"),
|
||||
iconPath: mdiReload,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.reload`
|
||||
),
|
||||
altText: "Reload",
|
||||
}));
|
||||
|
||||
// Add "frontend.reload_themes"
|
||||
commands.push({
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.themes"
|
||||
),
|
||||
action: () => this.hass.callService("frontend", "reload_themes"),
|
||||
iconPath: mdiReload,
|
||||
categoryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.reload"
|
||||
),
|
||||
altText: "Reload",
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_core_config"
|
||||
commands.push({
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.reload.core"
|
||||
),
|
||||
action: () =>
|
||||
this.hass.callService("homeassistant", "reload_core_config"),
|
||||
iconPath: mdiReload,
|
||||
categoryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.reload"
|
||||
),
|
||||
altText: "Reload",
|
||||
});
|
||||
|
||||
return commands.map((command) => ({
|
||||
...command,
|
||||
categoryKey: "reload",
|
||||
strings: [`${command.categoryText} ${command.primaryText}`],
|
||||
}));
|
||||
}
|
||||
|
||||
private _generateServerControlCommands(): CommandItem[] {
|
||||
const serverActions = ["restart", "stop"];
|
||||
|
||||
return serverActions.map((action) => {
|
||||
const categoryKey: CommandItem["categoryKey"] = "server_control";
|
||||
|
||||
const item = {
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
"action",
|
||||
this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
)
|
||||
),
|
||||
iconPath: mdiServerNetwork,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
categoryKey,
|
||||
action: () => this.hass.callService("homeassistant", action),
|
||||
};
|
||||
|
||||
return this._generateConfirmationCommand(
|
||||
{
|
||||
...item,
|
||||
strings: [`${item.categoryText} ${item.primaryText}`],
|
||||
altText: "Server Control",
|
||||
},
|
||||
this.hass.localize("ui.dialogs.generic.ok")
|
||||
);
|
||||
private _handleOpened() {
|
||||
this.updateComplete.then(() => {
|
||||
this._done = true;
|
||||
});
|
||||
}
|
||||
|
||||
private _generateNavigationCommands(): CommandItem[] {
|
||||
const panelItems = this._generateNavigationPanelCommands();
|
||||
const sectionItems = this._generateNavigationConfigSectionCommands();
|
||||
|
||||
return this._finalizeNavigationCommands([...panelItems, ...sectionItems]);
|
||||
private async _handleRangeChanged(e) {
|
||||
if (this._focusSet) {
|
||||
return;
|
||||
}
|
||||
if (e.firstVisible > -1) {
|
||||
this._focusSet = true;
|
||||
await this.updateComplete;
|
||||
this._setFocusFirstListItem();
|
||||
}
|
||||
}
|
||||
|
||||
private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
|
||||
return Object.keys(this.hass.panels)
|
||||
.filter((panelKey) => panelKey !== "_my_redirect")
|
||||
.map((panelKey) => {
|
||||
const panel = this.hass.panels[panelKey];
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
|
||||
const primaryText =
|
||||
this.hass.localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
return {
|
||||
primaryText,
|
||||
path: `/${panel.url_path}`,
|
||||
icon: panel.icon,
|
||||
altText: "Panel",
|
||||
};
|
||||
});
|
||||
private _getItemAtIndex(index: number): ListItem | null {
|
||||
return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
|
||||
}
|
||||
|
||||
private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] {
|
||||
const items: NavigationInfo[] = [];
|
||||
private _addSpinnerToItem(index: number): void {
|
||||
const spinner = document.createElement("ha-circular-progress");
|
||||
spinner.size = "small";
|
||||
spinner.slot = "meta";
|
||||
spinner.active = true;
|
||||
this._getItemAtIndex(index)?.appendChild(spinner);
|
||||
}
|
||||
|
||||
for (const sectionKey of Object.keys(configSections)) {
|
||||
for (const page of configSections[sectionKey]) {
|
||||
if (!canShowPage(this.hass, page)) {
|
||||
continue;
|
||||
}
|
||||
if (!page.component) {
|
||||
continue;
|
||||
}
|
||||
const info = this._getNavigationInfoFromConfig(page);
|
||||
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (
|
||||
items.some(
|
||||
(e) => e.path === info.path && e.component === info.component
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconPath: mdiNavigationVariantOutline,
|
||||
altText: "Navigation",
|
||||
...info,
|
||||
});
|
||||
private _getSuggestionsWithActions(): QuickBarItem[] {
|
||||
return this._suggestions.map((item) => {
|
||||
let action;
|
||||
switch (item.categoryKey) {
|
||||
case "entity":
|
||||
action = () => fireEvent(this, "hass-more-info", {});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private _getNavigationInfoFromConfig(
|
||||
page: PageNavigation
|
||||
): NavigationInfo | undefined {
|
||||
if (!page.component) {
|
||||
return undefined;
|
||||
}
|
||||
const caption = this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
|
||||
);
|
||||
|
||||
if (page.translationKey && caption) {
|
||||
return { ...page, primaryText: caption };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _generateConfirmationCommand(
|
||||
item: CommandItem,
|
||||
confirmText: ConfirmationDialogParams["confirmText"]
|
||||
): CommandItem {
|
||||
return {
|
||||
...item,
|
||||
action: () =>
|
||||
showConfirmationDialog(this, {
|
||||
confirmText,
|
||||
confirm: item.action,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private _finalizeNavigationCommands(
|
||||
items: BaseNavigationCommand[]
|
||||
): CommandItem[] {
|
||||
return items.map((item) => {
|
||||
const categoryKey: CommandItem["categoryKey"] = "navigation";
|
||||
|
||||
const navItem = {
|
||||
...item,
|
||||
iconPath: mdiEarth,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
),
|
||||
action: () => navigate(item.path),
|
||||
altText: "Navigation",
|
||||
};
|
||||
|
||||
return {
|
||||
...navItem,
|
||||
strings: [`${navItem.categoryText} ${navItem.primaryText}`],
|
||||
categoryKey,
|
||||
};
|
||||
return { ...item, action };
|
||||
});
|
||||
}
|
||||
|
||||
private _filterItems = memoizeOne(
|
||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
);
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
@ -751,8 +469,8 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
ha-icon,
|
||||
ha-svg-icon {
|
||||
mwc-list-item ha-icon,
|
||||
mwc-list-item ha-svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@ -771,37 +489,19 @@ export class QuickBar extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.command-category {
|
||||
--ha-chip-icon-color: #585858;
|
||||
--ha-chip-text-color: #212121;
|
||||
.entry-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.command-category.reload {
|
||||
--ha-chip-background-color: #cddc39;
|
||||
}
|
||||
|
||||
.command-category.navigation {
|
||||
--ha-chip-background-color: var(--light-primary-color);
|
||||
}
|
||||
|
||||
.command-category.server_control {
|
||||
--ha-chip-background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
span.command-text {
|
||||
margin-left: 8px;
|
||||
.entry-title {
|
||||
padding-left: 16px;
|
||||
padding-top: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
mwc-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mwc-list-item.command-item {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
mwc-list-item.single-line {
|
||||
min-height: 48px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hint {
|
||||
|
@ -664,8 +664,10 @@
|
||||
},
|
||||
"types": {
|
||||
"reload": "Reload",
|
||||
"navigation": "Navigate",
|
||||
"server_control": "Server"
|
||||
"navigation": "Navigation",
|
||||
"server_control": "Server",
|
||||
"entity": "Entity",
|
||||
"suggestion": "Suggestions"
|
||||
},
|
||||
"navigation": {
|
||||
"logs": "[%key:ui::panel::config::logs::caption%]",
|
||||
|
Loading…
x
Reference in New Issue
Block a user