mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-25 11:39:41 +00:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			20251001.1
			...
			quick-bar-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 0527fac2ce | ||
|   | 75e5d66966 | 
| @@ -42,7 +42,7 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => { | ||||
|  | ||||
| export interface ScorableTextItem { | ||||
|   score?: number; | ||||
|   text: string; | ||||
|   filterText: string; | ||||
|   altText?: string; | ||||
| } | ||||
|  | ||||
| @@ -55,8 +55,8 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { | ||||
|   return items | ||||
|     .map((item) => { | ||||
|       item.score = item.altText | ||||
|         ? fuzzySequentialMatch(filter, item.text, item.altText) | ||||
|         : fuzzySequentialMatch(filter, item.text); | ||||
|         ? fuzzySequentialMatch(filter, item.filterText, item.altText) | ||||
|         : fuzzySequentialMatch(filter, item.filterText); | ||||
|       return item; | ||||
|     }) | ||||
|     .filter((item) => item.score !== undefined && item.score > 0) | ||||
|   | ||||
| @@ -45,30 +45,43 @@ import { navigate } from "../../common/navigate"; | ||||
| import { configSections } from "../../panels/config/ha-panel-config"; | ||||
| import { PageNavigation } from "../../layouts/hass-tabs-subpage"; | ||||
| import { canShowPage } from "../../common/config/can_show_page"; | ||||
| import { getPanelIcon, getPanelNameTranslationKey } from "../../data/panel"; | ||||
| import { getPanelNameTranslationKey } from "../../data/panel"; | ||||
|  | ||||
| const DEFAULT_NAVIGATION_ICON = "hass:arrow-right-circle"; | ||||
| const DEFAULT_SERVER_ICON = "hass:server"; | ||||
|  | ||||
| interface QuickBarItem extends ScorableTextItem { | ||||
|   primaryText: string; | ||||
|   icon?: string; | ||||
|   iconPath?: string; | ||||
|   action(data?: any): void; | ||||
| } | ||||
|  | ||||
| interface QuickBarNavigationItem extends QuickBarItem { | ||||
| export interface CommandItem extends QuickBarItem { | ||||
|   categoryKey: "reload" | "navigation" | "server_control"; | ||||
|   categoryText: string; | ||||
| } | ||||
|  | ||||
| const isCommandItem = ( | ||||
|   item: QuickBarItem | CommandItem | ||||
| ): item is CommandItem => { | ||||
|   return (item as CommandItem).categoryKey !== undefined; | ||||
| }; | ||||
|  | ||||
| interface QuickBarNavigationItem extends CommandItem { | ||||
|   path: string; | ||||
| } | ||||
|  | ||||
| interface NavigationInfo extends PageNavigation { | ||||
|   text: string; | ||||
| } | ||||
| type NavigationInfo = PageNavigation & Pick<QuickBarItem, "primaryText">; | ||||
|  | ||||
| type BaseNavigationCommand = Pick< | ||||
|   QuickBarNavigationItem, | ||||
|   "primaryText" | "path" | ||||
| >; | ||||
| @customElement("ha-quick-bar") | ||||
| export class QuickBar extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @internalProperty() private _commandItems?: QuickBarItem[]; | ||||
|   @internalProperty() private _commandItems?: CommandItem[]; | ||||
|  | ||||
|   @internalProperty() private _entityItems?: QuickBarItem[]; | ||||
|  | ||||
| @@ -82,6 +95,8 @@ export class QuickBar extends LitElement { | ||||
|  | ||||
|   @internalProperty() private _commandMode = false; | ||||
|  | ||||
|   @internalProperty() private _commandType?: CommandItem["categoryKey"]; | ||||
|  | ||||
|   @internalProperty() private _done = false; | ||||
|  | ||||
|   @query("search-input", false) private _filterInputField?: HTMLElement; | ||||
| @@ -90,6 +105,10 @@ export class QuickBar extends LitElement { | ||||
|  | ||||
|   public async showDialog(params: QuickBarParams) { | ||||
|     this._commandMode = params.commandMode || this._toggleIfAlreadyOpened(); | ||||
|     this._commandType = params.commandType; | ||||
|     this._search = this._commandType | ||||
|       ? `${this._getCommandCategoryLabel(this._commandType).toLowerCase()} ` | ||||
|       : ""; | ||||
|     this._initializeItemsIfNeeded(); | ||||
|     this._opened = true; | ||||
|   } | ||||
| @@ -201,6 +220,12 @@ export class QuickBar extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _renderItem(item: QuickBarItem, index?: number) { | ||||
|     return isCommandItem(item) | ||||
|       ? this._renderCommandItem(item, index) | ||||
|       : this._renderEntityItem(item, index); | ||||
|   } | ||||
|  | ||||
|   private _renderEntityItem(item: QuickBarItem, index?: number) { | ||||
|     return html` | ||||
|       <mwc-list-item | ||||
|         .twoline=${Boolean(item.altText)} | ||||
| @@ -214,7 +239,30 @@ export class QuickBar extends LitElement { | ||||
|               slot="graphic" | ||||
|             ></ha-svg-icon>` | ||||
|           : html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`} | ||||
|         ${item.text} | ||||
|         <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) { | ||||
|     return html` | ||||
|       <mwc-list-item | ||||
|         .twoline=${Boolean(item.altText)} | ||||
|         .item=${item} | ||||
|         index=${ifDefined(index)} | ||||
|       > | ||||
|         <span class="command-category ${item.categoryKey}" | ||||
|           >${item.categoryText} | ||||
|         </span> | ||||
|  | ||||
|         <span>${item.primaryText}</span> | ||||
|         ${item.altText | ||||
|           ? html` | ||||
|               <span slot="secondary" class="item-text secondary" | ||||
| @@ -313,92 +361,107 @@ export class QuickBar extends LitElement { | ||||
|  | ||||
|   private _generateEntityItems(): QuickBarItem[] { | ||||
|     return Object.keys(this.hass.states) | ||||
|       .map((entityId) => ({ | ||||
|         text: computeStateName(this.hass.states[entityId]), | ||||
|       .map((entityId) => { | ||||
|         const primaryText = computeStateName(this.hass.states[entityId]); | ||||
|         return { | ||||
|           primaryText, | ||||
|           filterText: primaryText, | ||||
|           altText: entityId, | ||||
|           icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]), | ||||
|           action: () => fireEvent(this, "hass-more-info", { entityId }), | ||||
|       })) | ||||
|       .sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase())); | ||||
|         }; | ||||
|       }) | ||||
|       .sort((a, b) => | ||||
|         compare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase()) | ||||
|       ); | ||||
|   } | ||||
|  | ||||
|   private _generateCommandItems(): QuickBarItem[] { | ||||
|   private _generateCommandItems(): CommandItem[] { | ||||
|     return [ | ||||
|       ...this._generateReloadCommands(), | ||||
|       ...this._generateServerControlCommands(), | ||||
|       ...this._generateNavigationCommands(), | ||||
|     ].sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase())); | ||||
|     ].sort((a, b) => | ||||
|       compare(a.filterText.toLowerCase(), b.filterText.toLowerCase()) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private _generateReloadCommands(): QuickBarItem[] { | ||||
|   private _generateReloadCommands(): CommandItem[] { | ||||
|     const reloadableDomains = componentsWithService(this.hass, "reload").sort(); | ||||
|  | ||||
|     return reloadableDomains.map((domain) => ({ | ||||
|       text: | ||||
|     return reloadableDomains.map((domain) => { | ||||
|       const categoryKey = "reload"; | ||||
|       const categoryText = this._getCommandCategoryLabel(categoryKey); | ||||
|       const 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) | ||||
|         ), | ||||
|         ); | ||||
|  | ||||
|       return { | ||||
|         primaryText, | ||||
|         filterText: `${categoryText} ${primaryText}`, | ||||
|         icon: domainIcon(domain), | ||||
|         action: () => this.hass.callService(domain, "reload"), | ||||
|     })); | ||||
|         categoryKey, | ||||
|         categoryText, | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _generateServerControlCommands(): QuickBarItem[] { | ||||
|   private _generateServerControlCommands(): CommandItem[] { | ||||
|     const serverActions = ["restart", "stop"]; | ||||
|  | ||||
|     return serverActions.map((action) => | ||||
|       this._generateConfirmationCommand( | ||||
|         { | ||||
|           text: this.hass.localize( | ||||
|     return serverActions.map((action) => { | ||||
|       const categoryKey = "server_control"; | ||||
|       const categoryText = this._getCommandCategoryLabel(categoryKey); | ||||
|       const 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}` | ||||
|         ) | ||||
|           ), | ||||
|       ); | ||||
|  | ||||
|       return this._generateConfirmationCommand( | ||||
|         { | ||||
|           primaryText, | ||||
|           filterText: `${categoryText} ${primaryText}`, | ||||
|           categoryKey, | ||||
|           categoryText, | ||||
|           icon: DEFAULT_SERVER_ICON, | ||||
|           action: () => this.hass.callService("homeassistant", action), | ||||
|         }, | ||||
|         this.hass.localize("ui.dialogs.generic.ok") | ||||
|       ) | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _generateNavigationCommands(): QuickBarItem[] { | ||||
|   private _generateNavigationCommands(): CommandItem[] { | ||||
|     const panelItems = this._generateNavigationPanelCommands(); | ||||
|     const sectionItems = this._generateNavigationConfigSectionCommands(); | ||||
|  | ||||
|     return this._withNavigationActions([...panelItems, ...sectionItems]); | ||||
|     return this._finalizeNavigationCommands([...panelItems, ...sectionItems]); | ||||
|   } | ||||
|  | ||||
|   private _generateNavigationPanelCommands(): Omit< | ||||
|     QuickBarNavigationItem, | ||||
|     "action" | ||||
|   >[] { | ||||
|   private _generateNavigationPanelCommands(): BaseNavigationCommand[] { | ||||
|     return Object.keys(this.hass.panels).map((panelKey) => { | ||||
|       const panel = this.hass.panels[panelKey]; | ||||
|       const translationKey = getPanelNameTranslationKey(panel); | ||||
|  | ||||
|       const text = this.hass.localize( | ||||
|         "ui.dialogs.quick-bar.commands.navigation.navigate_to", | ||||
|         "panel", | ||||
|         this.hass.localize(translationKey) || panel.title || panel.url_path | ||||
|       ); | ||||
|       const primaryText = | ||||
|         this.hass.localize(translationKey) || panel.title || panel.url_path; | ||||
|  | ||||
|       return { | ||||
|         text, | ||||
|         icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON, | ||||
|         primaryText, | ||||
|         path: `/${panel.url_path}`, | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _generateNavigationConfigSectionCommands(): Partial< | ||||
|     QuickBarNavigationItem | ||||
|   >[] { | ||||
|   private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] { | ||||
|     const items: NavigationInfo[] = []; | ||||
|  | ||||
|     for (const sectionKey of Object.keys(configSections)) { | ||||
| @@ -422,28 +485,20 @@ export class QuickBar extends LitElement { | ||||
|     page: PageNavigation | ||||
|   ): NavigationInfo | undefined { | ||||
|     if (page.component) { | ||||
|       const shortCaption = this.hass.localize( | ||||
|       const caption = this.hass.localize( | ||||
|         `ui.dialogs.quick-bar.commands.navigation.${page.component}` | ||||
|       ); | ||||
|  | ||||
|       if (page.translationKey) { | ||||
|         const caption = this.hass.localize( | ||||
|           "ui.dialogs.quick-bar.commands.navigation.navigate_to_config", | ||||
|           "panel", | ||||
|           shortCaption | ||||
|         ); | ||||
|  | ||||
|         return { ...page, text: caption }; | ||||
|       } | ||||
|       return { ...page, primaryText: caption }; | ||||
|     } | ||||
|  | ||||
|     return undefined; | ||||
|   } | ||||
|  | ||||
|   private _generateConfirmationCommand( | ||||
|     item: QuickBarItem, | ||||
|     item: CommandItem, | ||||
|     confirmText: ConfirmationDialogParams["confirmText"] | ||||
|   ): QuickBarItem { | ||||
|   ): CommandItem { | ||||
|     return { | ||||
|       ...item, | ||||
|       action: () => | ||||
| @@ -454,13 +509,21 @@ export class QuickBar extends LitElement { | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   private _withNavigationActions(items) { | ||||
|     return items.map(({ text, icon, iconPath, path }) => ({ | ||||
|       text, | ||||
|       icon, | ||||
|       iconPath, | ||||
|       action: () => navigate(this, path), | ||||
|     })); | ||||
|   private _finalizeNavigationCommands( | ||||
|     items: BaseNavigationCommand[] | ||||
|   ): CommandItem[] { | ||||
|     return items.map((item) => { | ||||
|       const categoryKey = "navigation"; | ||||
|       const categoryText = this._getCommandCategoryLabel(categoryKey); | ||||
|  | ||||
|       return { | ||||
|         ...item, | ||||
|         categoryKey, | ||||
|         categoryText, | ||||
|         filterText: `${categoryText} ${item.primaryText}`, | ||||
|         action: () => navigate(this, item.path), | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _toggleIfAlreadyOpened() { | ||||
| @@ -479,6 +542,10 @@ export class QuickBar extends LitElement { | ||||
|       fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items) | ||||
|   ); | ||||
|  | ||||
|   private _getCommandCategoryLabel = ( | ||||
|     categoryKey: CommandItem["categoryKey"] | ||||
|   ) => this.hass.localize(`ui.dialogs.quick-bar.commands.types.${categoryKey}`); | ||||
|  | ||||
|   static get styles() { | ||||
|     return [ | ||||
|       haStyleDialog, | ||||
| @@ -512,6 +579,27 @@ export class QuickBar extends LitElement { | ||||
|           color: var(--primary-text-color); | ||||
|         } | ||||
|  | ||||
|         span.command-category { | ||||
|           font-weight: bold; | ||||
|           padding: 3px; | ||||
|           display: inline-flex; | ||||
|           border-radius: 6px; | ||||
|           color: black; | ||||
|         } | ||||
|  | ||||
|         span.command-category.reload { | ||||
|           background: pink; | ||||
|         } | ||||
|  | ||||
|         span.command-category.navigation { | ||||
|           background: lightblue; | ||||
|         } | ||||
|  | ||||
|         span.command-category.server_control { | ||||
|           background: orange; | ||||
|           color: white; | ||||
|         } | ||||
|  | ||||
|         .uni-virtualizer-host { | ||||
|           display: block; | ||||
|           position: relative; | ||||
| @@ -526,7 +614,6 @@ export class QuickBar extends LitElement { | ||||
|  | ||||
|         mwc-list-item { | ||||
|           width: 100%; | ||||
|           text-transform: capitalize; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { CommandItem } from "./ha-quick-bar"; | ||||
|  | ||||
| export interface QuickBarParams { | ||||
|   entityFilter?: string; | ||||
|   commandMode?: boolean; | ||||
|   commandType?: CommandItem["categoryKey"]; | ||||
| } | ||||
|  | ||||
| export const loadQuickBar = () => | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import { | ||||
| } from "../dialogs/quick-bar/show-dialog-quick-bar"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import { storeState } from "../util/ha-pref-storage"; | ||||
| import { CommandItem } from "../dialogs/quick-bar/ha-quick-bar"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
| @@ -32,15 +33,22 @@ export default <T extends Constructor<HassElement>>(superClass: T) => | ||||
|       tinykeys(window, { | ||||
|         e: (ev) => this._showQuickBar(ev), | ||||
|         c: (ev) => this._showQuickBar(ev, true), | ||||
|         g: (ev) => this._showQuickBar(ev, true, "navigation"), | ||||
|         r: (ev) => this._showQuickBar(ev, true, "reload"), | ||||
|         s: (ev) => this._showQuickBar(ev, true, "server_control"), | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     private _showQuickBar(e: KeyboardEvent, commandMode = false) { | ||||
|     private _showQuickBar( | ||||
|       e: KeyboardEvent, | ||||
|       commandMode = false, | ||||
|       commandType?: CommandItem["categoryKey"] | ||||
|     ) { | ||||
|       if (!this._canShowQuickBar(e)) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       showQuickBar(this, { commandMode }); | ||||
|       showQuickBar(this, { commandMode, commandType }); | ||||
|     } | ||||
|  | ||||
|     private _canShowQuickBar(e: KeyboardEvent) { | ||||
|   | ||||
| @@ -288,7 +288,6 @@ | ||||
|       "overflow_menu": "Overflow menu", | ||||
|       "successfully_saved": "Successfully saved", | ||||
|       "successfully_deleted": "Successfully deleted", | ||||
|  | ||||
|       "error_required": "Required", | ||||
|       "copied": "Copied" | ||||
|     }, | ||||
| @@ -496,13 +495,16 @@ | ||||
|             "rpi_gpio": "[%key:ui::panel::config::server_control::section::reloading::rpi_gpio%]" | ||||
|           }, | ||||
|           "server_control": { | ||||
|             "perform_action": "{action} Server", | ||||
|             "perform_action": "{action} server", | ||||
|             "restart": "[%key:ui::panel::config::server_control::section::server_management::restart%]", | ||||
|             "stop": "[%key:ui::panel::config::server_control::section::server_management::stop%]" | ||||
|           }, | ||||
|           "types": { | ||||
|             "reload": "Reload", | ||||
|             "navigation": "Go to", | ||||
|             "server_control": "Server" | ||||
|           }, | ||||
|           "navigation": { | ||||
|             "navigate_to": "Navigate to {panel}", | ||||
|             "navigate_to_config": "Navigate to {panel} configuration", | ||||
|             "logs": "[%key:ui::panel::config::logs::caption%]", | ||||
|             "automation": "[%key:ui::panel::config::automation::caption%]", | ||||
|             "script": "[%key:ui::panel::config::script::caption%]", | ||||
| @@ -1026,36 +1028,36 @@ | ||||
|               "heading": "YAML configuration reloading", | ||||
|               "introduction": "Some parts of Home Assistant can reload without requiring a restart. Hitting reload will unload their current YAML configuration and load the new one.", | ||||
|               "reload": "Reload {domain}", | ||||
|               "core": "Reload location & customizations", | ||||
|               "group": "Reload groups, group entities, and notify services", | ||||
|               "automation": "Reload automations", | ||||
|               "script": "Reload scripts", | ||||
|               "scene": "Reload scenes", | ||||
|               "person": "Reload people", | ||||
|               "zone": "Reload zones", | ||||
|               "input_boolean": "Reload input booleans", | ||||
|               "input_text": "Reload input texts", | ||||
|               "input_number": "Reload input numbers", | ||||
|               "input_datetime": "Reload input date times", | ||||
|               "input_select": "Reload input selects", | ||||
|               "template": "Reload template entities", | ||||
|               "universal": "Reload universal media player entities", | ||||
|               "rest": "Reload rest entities and notify services", | ||||
|               "command_line": "Reload command line entities", | ||||
|               "filter": "Reload filter entities", | ||||
|               "statistics": "Reload statistics entities", | ||||
|               "generic": "Reload generic IP camera entities", | ||||
|               "generic_thermostat": "Reload generic thermostat entities", | ||||
|               "homekit": "Reload HomeKit", | ||||
|               "min_max": "Reload min/max entities", | ||||
|               "history_stats": "Reload history stats entities", | ||||
|               "trend": "Reload trend entities", | ||||
|               "ping": "Reload ping binary sensor entities", | ||||
|               "filesize": "Reload file size entities", | ||||
|               "telegram": "Reload telegram notify services", | ||||
|               "smtp": "Reload SMTP notify services", | ||||
|               "mqtt": "Reload manually configured MQTT entities", | ||||
|               "rpi_gpio": "Reload Raspberry Pi GPIO entities" | ||||
|               "core": "Location & customizations", | ||||
|               "group": "Groups, group entities, and notify services", | ||||
|               "automation": "Automations", | ||||
|               "script": "Scripts", | ||||
|               "scene": "Scenes", | ||||
|               "person": "People", | ||||
|               "zone": "Zones", | ||||
|               "input_boolean": "Input booleans", | ||||
|               "input_text": "Input texts", | ||||
|               "input_number": "Input numbers", | ||||
|               "input_datetime": "Input date times", | ||||
|               "input_select": "Input selects", | ||||
|               "template": "Template entities", | ||||
|               "universal": "Universal media player entities", | ||||
|               "rest": "Rest entities and notify services", | ||||
|               "command_line": "Command line entities", | ||||
|               "filter": "Filter entities", | ||||
|               "statistics": "Statistics entities", | ||||
|               "generic": "Generic IP camera entities", | ||||
|               "generic_thermostat": "Generic thermostat entities", | ||||
|               "homekit": "HomeKit", | ||||
|               "min_max": "Min/max entities", | ||||
|               "history_stats": "History stats entities", | ||||
|               "trend": "Trend entities", | ||||
|               "ping": "Ping binary sensor entities", | ||||
|               "filesize": "File size entities", | ||||
|               "telegram": "Telegram notify services", | ||||
|               "smtp": "SMTP notify services", | ||||
|               "mqtt": "Manually configured MQTT entities", | ||||
|               "rpi_gpio": "Raspberry Pi GPIO entities" | ||||
|             }, | ||||
|             "server_management": { | ||||
|               "heading": "Server management", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user