mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-12 20:40:29 +00:00
Compare commits
5 Commits
fix/Dropdo
...
quickbar-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dfd55e773 | ||
|
|
976d3ddc87 | ||
|
|
6d3d194f42 | ||
|
|
43246029a1 | ||
|
|
fc06b9c29d |
278
src/data/quick-bar.ts
Normal file
278
src/data/quick-bar.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import {
|
||||||
|
mdiEarth,
|
||||||
|
mdiNavigationVariantOutline,
|
||||||
|
mdiReload,
|
||||||
|
mdiServerNetwork,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { canShowPage } from "../common/config/can_show_page";
|
||||||
|
import { componentsWithService } from "../common/config/components_with_service";
|
||||||
|
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 { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
|
import { ScorableTextItem } from "../common/string/filter/sequence-matching";
|
||||||
|
import { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||||
|
import { configSections } from "../panels/config/ha-panel-config";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import { AreaRegistryEntry } from "./area_registry";
|
||||||
|
import { computeDeviceName, DeviceRegistryEntry } from "./device_registry";
|
||||||
|
import { EntityRegistryEntry } from "./entity_registry";
|
||||||
|
import { domainToName } from "./integration";
|
||||||
|
import { getPanelNameTranslationKey } from "./panel";
|
||||||
|
|
||||||
|
export interface QuickBarItem extends ScorableTextItem {
|
||||||
|
primaryText: string;
|
||||||
|
primaryTextAlt?: string;
|
||||||
|
secondaryText?: string;
|
||||||
|
metaText?: string;
|
||||||
|
categoryKey:
|
||||||
|
| "reload"
|
||||||
|
| "navigation"
|
||||||
|
| "server_control"
|
||||||
|
| "entity"
|
||||||
|
| "suggestion";
|
||||||
|
actionData: string | string[];
|
||||||
|
iconPath?: string;
|
||||||
|
icon?: string;
|
||||||
|
path?: string;
|
||||||
|
isSuggestion?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NavigationInfo = PageNavigation &
|
||||||
|
Pick<QuickBarItem, "primaryText" | "secondaryText">;
|
||||||
|
|
||||||
|
export type BaseNavigationCommand = Pick<QuickBarItem, "primaryText" | "path">;
|
||||||
|
|
||||||
|
export const generateEntityItems = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entities: { [entityId: string]: EntityRegistryEntry },
|
||||||
|
devices: { [deviceId: string]: DeviceRegistryEntry },
|
||||||
|
areas: { [areaId: string]: AreaRegistryEntry }
|
||||||
|
): QuickBarItem[] =>
|
||||||
|
Object.keys(hass.states)
|
||||||
|
.map((entityId) => {
|
||||||
|
const entityState = hass.states[entityId];
|
||||||
|
const entity = entities[entityId];
|
||||||
|
const deviceName = entity?.device_id
|
||||||
|
? computeDeviceName(devices[entity.device_id], hass)
|
||||||
|
: undefined;
|
||||||
|
const entityItem = {
|
||||||
|
primaryText: computeStateName(entityState),
|
||||||
|
primaryTextAlt: computeStateDisplay(
|
||||||
|
hass.localize,
|
||||||
|
entityState,
|
||||||
|
hass.locale
|
||||||
|
),
|
||||||
|
secondaryText:
|
||||||
|
(deviceName ? `${deviceName} | ` : "") +
|
||||||
|
(hass.userData?.showAdvanced ? entityId : ""),
|
||||||
|
metaText: entity?.area_id ? areas[entity.area_id].name : undefined,
|
||||||
|
icon: entityState.attributes.icon,
|
||||||
|
iconPath: entityState.attributes.icon
|
||||||
|
? undefined
|
||||||
|
: domainIcon(computeDomain(entityId), entityState),
|
||||||
|
actionData: entityId,
|
||||||
|
categoryKey: "entity" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...entityItem,
|
||||||
|
strings: [entityItem.primaryText, entityItem.secondaryText],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => caseInsensitiveStringCompare(a.primaryText, b.primaryText));
|
||||||
|
|
||||||
|
export const generateCommandItems = (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Array<QuickBarItem[]> => [
|
||||||
|
generateNavigationCommands(hass),
|
||||||
|
generateReloadCommands(hass),
|
||||||
|
generateServerControlCommands(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)
|
||||||
|
),
|
||||||
|
actionData: [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"),
|
||||||
|
actionData: ["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"),
|
||||||
|
actionData: ["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 = (
|
||||||
|
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,
|
||||||
|
actionData: action,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
strings: [
|
||||||
|
`${hass.localize(
|
||||||
|
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||||
|
)} ${item.primaryText}`,
|
||||||
|
],
|
||||||
|
secondaryText: "Control your server",
|
||||||
|
iconPath: mdiServerNetwork,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalizeNavigationCommands = (
|
||||||
|
items: BaseNavigationCommand[],
|
||||||
|
hass: HomeAssistant
|
||||||
|
): QuickBarItem[] =>
|
||||||
|
items.map((item) => {
|
||||||
|
const categoryKey: QuickBarItem["categoryKey"] = "navigation";
|
||||||
|
|
||||||
|
const navItem = {
|
||||||
|
secondaryText: "Navigation",
|
||||||
|
iconPath: mdiEarth,
|
||||||
|
...item,
|
||||||
|
actionData: item.path!,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
categoryKey,
|
||||||
|
...navItem,
|
||||||
|
strings: [
|
||||||
|
`${hass.localize(
|
||||||
|
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||||
|
)} ${navItem.primaryText}`,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -2,118 +2,114 @@ import "@lit-labs/virtualizer";
|
|||||||
import "@material/mwc-list/mwc-list";
|
import "@material/mwc-list/mwc-list";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import type { ListItem } from "@material/mwc-list/mwc-list-item";
|
import type { ListItem } from "@material/mwc-list/mwc-list-item";
|
||||||
import {
|
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||||
mdiClose,
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
mdiConsoleLine,
|
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
mdiEarth,
|
|
||||||
mdiMagnify,
|
|
||||||
mdiReload,
|
|
||||||
mdiServerNetwork,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import { css, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { canShowPage } from "../../common/config/can_show_page";
|
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||||
import { componentsWithService } from "../../common/config/components_with_service";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
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 { navigate } from "../../common/navigate";
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||||
import {
|
|
||||||
fuzzyFilterSort,
|
|
||||||
ScorableTextItem,
|
|
||||||
} from "../../common/string/filter/sequence-matching";
|
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import "../../components/ha-chip";
|
import "../../components/ha-chip";
|
||||||
import "../../components/ha-circular-progress";
|
import "../../components/ha-circular-progress";
|
||||||
import "../../components/ha-header-bar";
|
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-textfield";
|
import "../../components/ha-textfield";
|
||||||
import { domainToName } from "../../data/integration";
|
|
||||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
|
||||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
|
||||||
import { configSections } from "../../panels/config/ha-panel-config";
|
|
||||||
import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
|
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
import {
|
import {
|
||||||
ConfirmationDialogParams,
|
AreaRegistryEntry,
|
||||||
showConfirmationDialog,
|
subscribeAreaRegistry,
|
||||||
} from "../generic/show-dialog-box";
|
} from "../../data/area_registry";
|
||||||
|
import {
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
subscribeDeviceRegistry,
|
||||||
|
} from "../../data/device_registry";
|
||||||
|
import {
|
||||||
|
EntityRegistryEntry,
|
||||||
|
subscribeEntityRegistry,
|
||||||
|
} from "../../data/entity_registry";
|
||||||
|
import {
|
||||||
|
generateCommandItems,
|
||||||
|
generateEntityItems,
|
||||||
|
QuickBarItem,
|
||||||
|
} from "../../data/quick-bar";
|
||||||
|
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||||
|
import {
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
haStyleScrollbar,
|
||||||
|
} from "../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||||
import { QuickBarParams } from "./show-dialog-quick-bar";
|
import { QuickBarParams } from "./show-dialog-quick-bar";
|
||||||
|
|
||||||
interface QuickBarItem extends ScorableTextItem {
|
|
||||||
primaryText: string;
|
|
||||||
iconPath?: string;
|
|
||||||
action(data?: any): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CommandItem extends QuickBarItem {
|
|
||||||
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">;
|
|
||||||
|
|
||||||
type BaseNavigationCommand = Pick<
|
|
||||||
QuickBarNavigationItem,
|
|
||||||
"primaryText" | "path"
|
|
||||||
>;
|
|
||||||
@customElement("ha-quick-bar")
|
@customElement("ha-quick-bar")
|
||||||
export class QuickBar extends LitElement {
|
export class QuickBar extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _commandItems?: CommandItem[];
|
@state() private _items?: Array<QuickBarItem[]>;
|
||||||
|
|
||||||
@state() private _entityItems?: EntityItem[];
|
private _filteredItems?: QuickBarItem[];
|
||||||
|
|
||||||
@state() private _filter = "";
|
@state() private _filter = "";
|
||||||
|
|
||||||
@state() private _search = "";
|
@state() private _search = "";
|
||||||
|
|
||||||
@state() private _open = false;
|
|
||||||
|
|
||||||
@state() private _commandMode = false;
|
|
||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _opened = false;
|
||||||
|
|
||||||
|
@state() private _done = false;
|
||||||
|
|
||||||
@state() private _narrow = false;
|
@state() private _narrow = false;
|
||||||
|
|
||||||
@state() private _hint?: string;
|
@state() private _hint?: string;
|
||||||
|
|
||||||
|
@state() private _entities?: EntityRegistryEntry[];
|
||||||
|
|
||||||
|
@state() private _areas?: AreaRegistryEntry[];
|
||||||
|
|
||||||
|
@state() private _devices?: DeviceRegistryEntry[];
|
||||||
|
|
||||||
@query("ha-textfield", false) private _filterInputField?: HTMLElement;
|
@query("ha-textfield", false) private _filterInputField?: HTMLElement;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
@LocalStorage("suggestions", true, {
|
||||||
|
attribute: false,
|
||||||
|
})
|
||||||
|
private _suggestions: QuickBarItem[] = [];
|
||||||
|
|
||||||
private _focusSet = false;
|
private _focusSet = false;
|
||||||
|
|
||||||
private _focusListElement?: ListItem | null;
|
private _focusListElement?: ListItem | null;
|
||||||
|
|
||||||
|
private _filterItems = memoizeOne(
|
||||||
|
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
||||||
|
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||||
|
);
|
||||||
|
|
||||||
public async showDialog(params: QuickBarParams) {
|
public async showDialog(params: QuickBarParams) {
|
||||||
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
|
|
||||||
this._hint = params.hint;
|
this._hint = params.hint;
|
||||||
this._narrow = matchMedia(
|
this._narrow = matchMedia(
|
||||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
).matches;
|
).matches;
|
||||||
this._initializeItemsIfNeeded();
|
}
|
||||||
this._open = true;
|
|
||||||
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
|
return [
|
||||||
|
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||||
|
this._areas = areas;
|
||||||
|
}),
|
||||||
|
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||||
|
this._devices = devices;
|
||||||
|
}),
|
||||||
|
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||||
|
this._entities = entities;
|
||||||
|
}),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this._open = false;
|
|
||||||
this._opened = false;
|
this._opened = false;
|
||||||
this._focusSet = false;
|
this._focusSet = false;
|
||||||
this._filter = "";
|
this._filter = "";
|
||||||
@@ -121,28 +117,46 @@ export class QuickBar extends LitElement {
|
|||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getItems = memoizeOne(
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
(commandMode: boolean, commandItems, entityItems, filter: string) => {
|
super.willUpdate(changedProperties);
|
||||||
const items = commandMode ? commandItems : entityItems;
|
if (
|
||||||
|
(changedProperties.has("_entity") ||
|
||||||
if (items && filter && filter !== " ") {
|
changedProperties.has("_areas") ||
|
||||||
return this._filterItems(items, filter);
|
changedProperties.has("_devices")) &&
|
||||||
}
|
this._areas &&
|
||||||
return items;
|
this._devices &&
|
||||||
|
this._entities
|
||||||
|
) {
|
||||||
|
this._initializeItems();
|
||||||
|
this._opened = true;
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render(): TemplateResult {
|
||||||
if (!this._open) {
|
if (!this._opened) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items: QuickBarItem[] | undefined = this._getItems(
|
let sectionCount = 0;
|
||||||
this._commandMode,
|
|
||||||
this._commandItems,
|
if (this._items && this._filter && this._filter !== "") {
|
||||||
this._entityItems,
|
const newFilteredItems: QuickBarItem[] = [];
|
||||||
this._filter
|
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`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
@@ -161,28 +175,18 @@ export class QuickBar extends LitElement {
|
|||||||
aria-label=${this.hass.localize(
|
aria-label=${this.hass.localize(
|
||||||
"ui.dialogs.quick-bar.filter_placeholder"
|
"ui.dialogs.quick-bar.filter_placeholder"
|
||||||
)}
|
)}
|
||||||
.value=${this._commandMode ? `>${this._search}` : this._search}
|
.value=${this._search}
|
||||||
icon
|
.icon=${true}
|
||||||
.iconTrailing=${this._search !== undefined || this._narrow}
|
.iconTrailing=${this._search !== undefined || this._narrow}
|
||||||
@input=${this._handleSearchChange}
|
@input=${this._handleSearchChange}
|
||||||
@keydown=${this._handleInputKeyDown}
|
@keydown=${this._handleInputKeyDown}
|
||||||
@focus=${this._setFocusFirstListItem}
|
@focus=${this._setFocusFirstListItem}
|
||||||
>
|
>
|
||||||
${this._commandMode
|
<ha-svg-icon
|
||||||
? html`
|
slot="leadingIcon"
|
||||||
<ha-svg-icon
|
class="prefix"
|
||||||
slot="leadingIcon"
|
.path=${mdiMagnify}
|
||||||
class="prefix"
|
></ha-svg-icon>
|
||||||
.path=${mdiConsoleLine}
|
|
||||||
></ha-svg-icon>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="leadingIcon"
|
|
||||||
class="prefix"
|
|
||||||
.path=${mdiMagnify}
|
|
||||||
></ha-svg-icon>
|
|
||||||
`}
|
|
||||||
${this._search || this._narrow
|
${this._search || this._narrow
|
||||||
? html`
|
? html`
|
||||||
<div slot="trailingIcon">
|
<div slot="trailingIcon">
|
||||||
@@ -205,12 +209,11 @@ export class QuickBar extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
</ha-textfield>
|
</ha-textfield>
|
||||||
</div>
|
</div>
|
||||||
${!items
|
${!this._filteredItems
|
||||||
? html`<ha-circular-progress
|
? html`
|
||||||
size="small"
|
<ha-circular-progress size="small" active></ha-circular-progress>
|
||||||
active
|
`
|
||||||
></ha-circular-progress>`
|
: this._filteredItems.length === 0 && this._filter !== ""
|
||||||
: items.length === 0
|
|
||||||
? html`
|
? html`
|
||||||
<div class="nothing-found">
|
<div class="nothing-found">
|
||||||
${this.hass.localize("ui.dialogs.quick-bar.nothing_found")}
|
${this.hass.localize("ui.dialogs.quick-bar.nothing_found")}
|
||||||
@@ -218,26 +221,26 @@ export class QuickBar extends LitElement {
|
|||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<mwc-list>
|
<mwc-list>
|
||||||
${this._opened
|
<lit-virtualizer
|
||||||
? html`<lit-virtualizer
|
scroller
|
||||||
scroller
|
@keydown=${this._handleListItemKeyDown}
|
||||||
@keydown=${this._handleListItemKeyDown}
|
@rangechange=${this._handleRangeChanged}
|
||||||
@rangechange=${this._handleRangeChanged}
|
@click=${this._handleItemClick}
|
||||||
@click=${this._handleItemClick}
|
class="ha-scrollbar"
|
||||||
class="ha-scrollbar"
|
style=${styleMap({
|
||||||
style=${styleMap({
|
height: this._narrow
|
||||||
height: this._narrow
|
? "calc(100vh - 56px)"
|
||||||
? "calc(100vh - 56px)"
|
: `${Math.min(
|
||||||
: `${Math.min(
|
this._filteredItems.length * 72 +
|
||||||
items.length * (this._commandMode ? 56 : 72) + 26,
|
sectionCount * 37 +
|
||||||
500
|
18,
|
||||||
)}px`,
|
this._done ? 600 : 0
|
||||||
})}
|
)}px`,
|
||||||
.items=${items}
|
})}
|
||||||
.renderItem=${this._renderItem}
|
.items=${this._filteredItems}
|
||||||
>
|
.renderItem=${this._renderItem}
|
||||||
</lit-virtualizer>`
|
>
|
||||||
: ""}
|
</lit-virtualizer>
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`}
|
`}
|
||||||
${this._hint ? html`<div class="hint">${this._hint}</div>` : ""}
|
${this._hint ? html`<div class="hint">${this._hint}</div>` : ""}
|
||||||
@@ -245,102 +248,111 @@ export class QuickBar extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _initializeItemsIfNeeded() {
|
|
||||||
if (this._commandMode) {
|
|
||||||
this._commandItems = this._commandItems || this._generateCommandItems();
|
|
||||||
} else {
|
|
||||||
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 => {
|
private _renderItem = (item: QuickBarItem, index: number): TemplateResult => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return isCommandItem(item)
|
|
||||||
? this._renderCommandItem(item, index)
|
const previous = this._filteredItems![index - 1];
|
||||||
: this._renderEntityItem(item as EntityItem, index);
|
|
||||||
|
return html`
|
||||||
|
<div class="entry-container" style="z-index: 5">
|
||||||
|
${index === 0 || item?.categoryKey !== previous?.categoryKey
|
||||||
|
? html`
|
||||||
|
<div class="entry-title">
|
||||||
|
${item.isSuggestion
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.dialogs.quick-bar.commands.types.suggestions"
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
`ui.dialogs.quick-bar.commands.types.${item.categoryKey}`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<mwc-list-item
|
||||||
|
.twoline=${Boolean(item.secondaryText)}
|
||||||
|
.hasMeta=${Boolean(item.metaText)}
|
||||||
|
.item=${item}
|
||||||
|
index=${ifDefined(index)}
|
||||||
|
graphic="icon"
|
||||||
|
class=${item.secondaryText ? "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 class="secondary">${item.primaryTextAlt}</span></span
|
||||||
|
>
|
||||||
|
${item.secondaryText
|
||||||
|
? html`
|
||||||
|
<span slot="secondary" class="item-text secondary"
|
||||||
|
>${item.secondaryText}</span
|
||||||
|
>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${item.metaText
|
||||||
|
? html`<ha-chip slot="meta">${item.metaText}</ha-chip>`
|
||||||
|
: ""}
|
||||||
|
</mwc-list-item>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _renderEntityItem(item: EntityItem, index?: number) {
|
private _initializeItems() {
|
||||||
return html`
|
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
|
||||||
<mwc-list-item
|
for (const device of this._devices!) {
|
||||||
.twoline=${Boolean(item.altText)}
|
deviceLookup[device.id] = device;
|
||||||
.item=${item}
|
}
|
||||||
index=${ifDefined(index)}
|
|
||||||
graphic="icon"
|
|
||||||
>
|
|
||||||
${item.iconPath
|
|
||||||
? html`<ha-svg-icon
|
|
||||||
.path=${item.iconPath}
|
|
||||||
class="entity"
|
|
||||||
slot="graphic"
|
|
||||||
></ha-svg-icon>`
|
|
||||||
: html`<ha-icon
|
|
||||||
.icon=${item.icon}
|
|
||||||
class="entity"
|
|
||||||
slot="graphic"
|
|
||||||
></ha-icon>`}
|
|
||||||
<span>${item.primaryText}</span>
|
|
||||||
${item.altText
|
|
||||||
? html`
|
|
||||||
<span slot="secondary" class="item-text secondary"
|
|
||||||
>${item.altText}</span
|
|
||||||
>
|
|
||||||
`
|
|
||||||
: null}
|
|
||||||
</mwc-list-item>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderCommandItem(item: CommandItem, index?: number) {
|
const entityLookup: { [entityId: string]: EntityRegistryEntry } = {};
|
||||||
return html`
|
for (const entity of this._entities!) {
|
||||||
<mwc-list-item
|
entityLookup[entity.entity_id] = entity;
|
||||||
.item=${item}
|
}
|
||||||
index=${ifDefined(index)}
|
|
||||||
class="command-item"
|
|
||||||
hasMeta
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<ha-chip
|
|
||||||
.label=${item.categoryText}
|
|
||||||
hasIcon
|
|
||||||
class="command-category ${item.categoryKey}"
|
|
||||||
>
|
|
||||||
${item.iconPath
|
|
||||||
? html`<ha-svg-icon
|
|
||||||
.path=${item.iconPath}
|
|
||||||
slot="icon"
|
|
||||||
></ha-svg-icon>`
|
|
||||||
: ""}
|
|
||||||
${item.categoryText}</ha-chip
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="command-text">${item.primaryText}</span>
|
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||||
</mwc-list-item>
|
for (const area of this._areas!) {
|
||||||
`;
|
areaLookup[area.area_id] = area;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._items = this._items || [
|
||||||
|
generateEntityItems(this.hass, entityLookup, deviceLookup, areaLookup),
|
||||||
|
...generateCommandItems(this.hass),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
|
private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
|
||||||
this._addSpinnerToCommandItem(index);
|
if (!this._suggestions.includes(item)) {
|
||||||
|
this._suggestions.unshift({ ...item, isSuggestion: true });
|
||||||
|
this._suggestions = this._suggestions.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
await item.action();
|
this._addSpinnerToItem(index);
|
||||||
|
|
||||||
|
switch (item.categoryKey) {
|
||||||
|
case "entity":
|
||||||
|
fireEvent(this, "hass-more-info", {
|
||||||
|
entityId: item.actionData as string,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "reload":
|
||||||
|
this.hass.callService(item.actionData[0], item.actionData[1]);
|
||||||
|
break;
|
||||||
|
case "navigation":
|
||||||
|
navigate(item.actionData as string);
|
||||||
|
break;
|
||||||
|
case "server_control":
|
||||||
|
showConfirmationDialog(this, {
|
||||||
|
confirmText: this.hass.localize("ui.dialogs.generic.ok"),
|
||||||
|
confirm: () =>
|
||||||
|
this.hass.callService("homeassistant", item.actionData as string),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,56 +371,28 @@ 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 {
|
private _handleSearchChange(ev: CustomEvent): void {
|
||||||
const newFilter = (ev.currentTarget as any).value;
|
const newFilter = (ev.currentTarget as any).value;
|
||||||
const oldCommandMode = this._commandMode;
|
|
||||||
const oldSearch = this._search;
|
|
||||||
let newCommandMode: boolean;
|
|
||||||
let newSearch: string;
|
let newSearch: string;
|
||||||
|
|
||||||
if (newFilter.startsWith(">")) {
|
if (newFilter.startsWith(">")) {
|
||||||
newCommandMode = true;
|
|
||||||
newSearch = newFilter.substring(1);
|
newSearch = newFilter.substring(1);
|
||||||
} else {
|
} else {
|
||||||
newCommandMode = false;
|
|
||||||
newSearch = newFilter;
|
newSearch = newFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldCommandMode === newCommandMode && oldSearch === newSearch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._commandMode = newCommandMode;
|
|
||||||
this._search = newSearch;
|
this._search = newSearch;
|
||||||
|
|
||||||
if (this._hint) {
|
if (this._hint) {
|
||||||
this._hint = undefined;
|
this._hint = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldCommandMode !== this._commandMode) {
|
if (this._focusSet && this._focusListElement) {
|
||||||
this._focusSet = false;
|
this._focusSet = false;
|
||||||
this._initializeItemsIfNeeded();
|
// @ts-ignore
|
||||||
this._filter = this._search;
|
this._focusListElement.rippleHandlers.endFocus();
|
||||||
} else {
|
|
||||||
if (this._focusSet && this._focusListElement) {
|
|
||||||
this._focusSet = false;
|
|
||||||
// @ts-ignore
|
|
||||||
this._focusListElement.rippleHandlers.endFocus();
|
|
||||||
}
|
|
||||||
this._debouncedSetFilter(this._search);
|
|
||||||
}
|
}
|
||||||
|
this._debouncedSetFilter(this._search);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearSearch() {
|
private _clearSearch() {
|
||||||
@@ -454,258 +438,62 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleItemClick(ev) {
|
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;
|
||||||
|
|
||||||
|
// Need to resolve when clicking on the chip
|
||||||
|
if (!target.item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.processItemAndCloseDialog(
|
this.processItemAndCloseDialog(
|
||||||
listItem.item,
|
target.item,
|
||||||
Number(listItem.getAttribute("index"))
|
Number((target as HTMLElement).getAttribute("index"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateEntityItems(): EntityItem[] {
|
private _handleOpened() {
|
||||||
return Object.keys(this.hass.states)
|
this.updateComplete.then(() => {
|
||||||
.map((entityId) => {
|
this._done = true;
|
||||||
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`
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
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}`],
|
|
||||||
},
|
|
||||||
this.hass.localize("ui.dialogs.generic.ok")
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateNavigationCommands(): CommandItem[] {
|
private async _handleRangeChanged(e) {
|
||||||
const panelItems = this._generateNavigationPanelCommands();
|
if (this._focusSet) {
|
||||||
const sectionItems = this._generateNavigationConfigSectionCommands();
|
return;
|
||||||
|
|
||||||
return this._finalizeNavigationCommands([...panelItems, ...sectionItems]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] {
|
|
||||||
const items: NavigationInfo[] = [];
|
|
||||||
|
|
||||||
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(info);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (e.firstVisible > -1) {
|
||||||
return items;
|
this._focusSet = true;
|
||||||
}
|
await this.updateComplete;
|
||||||
|
this._setFocusFirstListItem();
|
||||||
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(
|
private _getItemAtIndex(index: number): ListItem | null {
|
||||||
item: CommandItem,
|
return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
|
||||||
confirmText: ConfirmationDialogParams["confirmText"]
|
|
||||||
): CommandItem {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
action: () =>
|
|
||||||
showConfirmationDialog(this, {
|
|
||||||
confirmText,
|
|
||||||
confirm: item.action,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _finalizeNavigationCommands(
|
private _addSpinnerToItem(index: number): void {
|
||||||
items: BaseNavigationCommand[]
|
const spinner = document.createElement("ha-circular-progress");
|
||||||
): CommandItem[] {
|
spinner.size = "small";
|
||||||
return items.map((item) => {
|
spinner.slot = "meta";
|
||||||
const categoryKey: CommandItem["categoryKey"] = "navigation";
|
spinner.active = true;
|
||||||
|
this._getItemAtIndex(index)?.appendChild(spinner);
|
||||||
const navItem = {
|
|
||||||
...item,
|
|
||||||
iconPath: mdiEarth,
|
|
||||||
categoryText: this.hass.localize(
|
|
||||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
|
||||||
),
|
|
||||||
action: () => navigate(item.path),
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...navItem,
|
|
||||||
strings: [`${navItem.categoryText} ${navItem.primaryText}`],
|
|
||||||
categoryKey,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleIfAlreadyOpened() {
|
|
||||||
return this._opened ? !this._commandMode : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _filterItems = memoizeOne(
|
|
||||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
|
||||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
|
||||||
);
|
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
|
haStyle,
|
||||||
css`
|
css`
|
||||||
.heading {
|
.heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
--mdc-theme-primary: var(--primary-text-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading ha-textfield {
|
.heading ha-textfield {
|
||||||
@@ -739,8 +527,8 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon.entity,
|
mwc-list-item ha-icon,
|
||||||
ha-svg-icon.entity {
|
mwc-list-item ha-svg-icon {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,38 +536,30 @@ export class QuickBar extends LitElement {
|
|||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-textfield {
|
||||||
|
--mdc-text-field-fill-color: transparent;
|
||||||
|
--mdc-theme-primary: var(--divider-color);
|
||||||
|
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
ha-textfield ha-icon-button {
|
ha-textfield ha-icon-button {
|
||||||
--mdc-icon-button-size: 24px;
|
--mdc-icon-button-size: 24px;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-category {
|
.entry-container {
|
||||||
--ha-chip-icon-color: #585858;
|
width: 100%;
|
||||||
--ha-chip-text-color: #212121;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-category.reload {
|
.entry-title {
|
||||||
--ha-chip-background-color: #cddc39;
|
padding-left: 16px;
|
||||||
}
|
padding-top: 16px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mwc-list-item {
|
mwc-list-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
|
||||||
mwc-list-item.command-item {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
@@ -801,6 +581,10 @@ export class QuickBar extends LitElement {
|
|||||||
lit-virtualizer {
|
lit-virtualizer {
|
||||||
contain: size layout !important;
|
contain: size layout !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-chip {
|
||||||
|
margin-left: -48px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ export const haStyleDialog = css`
|
|||||||
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
||||||
--mdc-dialog-content-ink-color: var(--primary-text-color);
|
--mdc-dialog-content-ink-color: var(--primary-text-color);
|
||||||
--justify-action-buttons: space-between;
|
--justify-action-buttons: space-between;
|
||||||
|
--ha-dialog-border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-dialog .form {
|
ha-dialog .form {
|
||||||
|
|||||||
@@ -664,8 +664,10 @@
|
|||||||
},
|
},
|
||||||
"types": {
|
"types": {
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
"navigation": "Navigate",
|
"navigation": "Navigation",
|
||||||
"server_control": "Server"
|
"server_control": "Server",
|
||||||
|
"entity": "Entity",
|
||||||
|
"suggestion": "Suggestions"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"logs": "[%key:ui::panel::config::logs::caption%]",
|
"logs": "[%key:ui::panel::config::logs::caption%]",
|
||||||
@@ -689,7 +691,7 @@
|
|||||||
"server_control": "[%key:ui::panel::config::server_control::caption%]"
|
"server_control": "[%key:ui::panel::config::server_control::caption%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filter_placeholder": "Entity Filter",
|
"filter_placeholder": "Search for entities, pages or commands",
|
||||||
"title": "Quick Search",
|
"title": "Quick Search",
|
||||||
"key_c_hint": "Press 'c' on any page to open this search bar",
|
"key_c_hint": "Press 'c' on any page to open this search bar",
|
||||||
"nothing_found": "Nothing found!"
|
"nothing_found": "Nothing found!"
|
||||||
|
|||||||
Reference in New Issue
Block a user