From ef4d94b37670d550b87e9a34ccd207b70f2d49e1 Mon Sep 17 00:00:00 2001 From: Lennard Scheibel Date: Fri, 16 Sep 2022 21:06:41 +0200 Subject: [PATCH 1/2] Add shopping-list card --- .hass_dev/lovelace-mushroom-showcase.yaml | 5 +- .hass_dev/views/shopping-list-view.yaml | 34 +++ README.md | 1 + docs/cards/shopping-list.md | 22 ++ docs/images/shopping-list-dark.png | Bin 0 -> 44879 bytes docs/images/shopping-list-light.png | Bin 0 -> 46277 bytes src/cards/shopping-list-card/const.ts | 9 + .../shopping-list-card-config.ts | 26 ++ .../shopping-list-card-divider.ts | 84 ++++++ .../shopping-list-card-editor.ts | 84 ++++++ .../shopping-list-card-input.ts | 61 ++++ .../shopping-list-card-item.ts | 120 ++++++++ .../shopping-list-card/shopping-list-card.ts | 275 ++++++++++++++++++ src/cards/shopping-list-card/utils.ts | 45 +++ src/ha/data/shopping-list.ts | 5 + src/mushroom.ts | 1 + src/translations/de.json | 8 + src/translations/en.json | 8 + 18 files changed, 786 insertions(+), 2 deletions(-) create mode 100644 .hass_dev/views/shopping-list-view.yaml create mode 100644 docs/cards/shopping-list.md create mode 100644 docs/images/shopping-list-dark.png create mode 100644 docs/images/shopping-list-light.png create mode 100644 src/cards/shopping-list-card/const.ts create mode 100644 src/cards/shopping-list-card/shopping-list-card-config.ts create mode 100644 src/cards/shopping-list-card/shopping-list-card-divider.ts create mode 100644 src/cards/shopping-list-card/shopping-list-card-editor.ts create mode 100644 src/cards/shopping-list-card/shopping-list-card-input.ts create mode 100644 src/cards/shopping-list-card/shopping-list-card-item.ts create mode 100644 src/cards/shopping-list-card/shopping-list-card.ts create mode 100644 src/cards/shopping-list-card/utils.ts create mode 100644 src/ha/data/shopping-list.ts diff --git a/.hass_dev/lovelace-mushroom-showcase.yaml b/.hass_dev/lovelace-mushroom-showcase.yaml index d8ec02622..7cf6ac3c3 100644 --- a/.hass_dev/lovelace-mushroom-showcase.yaml +++ b/.hass_dev/lovelace-mushroom-showcase.yaml @@ -1,4 +1,4 @@ -title: Mushroom Showcase +title: Mushroom Showcase views: - !include views/light-view.yaml - !include views/cover-view.yaml @@ -14,4 +14,5 @@ views: - !include views/vacuum-view.yaml - !include views/lock-view.yaml - !include views/humidifier-view.yaml - - !include views/select-view.yaml \ No newline at end of file + - !include views/select-view.yaml + - !include views/shopping-list-view.yaml diff --git a/.hass_dev/views/shopping-list-view.yaml b/.hass_dev/views/shopping-list-view.yaml new file mode 100644 index 000000000..6783a36eb --- /dev/null +++ b/.hass_dev/views/shopping-list-view.yaml @@ -0,0 +1,34 @@ +title: Shopping Lists +icon: mdi:cart +cards: + - type: grid + title: Shopping List + cards: + - type: custom:mushroom-shopping-list + name: My Shopping List + icon: mdi:cart + primary_info: name + secondary_info: state + columns: 1 + square: false + - type: vertical-stack + title: Layout + cards: + - type: custom:mushroom-shopping-list + name: Horizontal Shopping List + layout: horizontal + primary_info: name + secondary_info: state + - type: grid + columns: 2 + square: false + cards: + - type: custom:mushroom-shopping-list + checked_icon: mdi:checkbox-blank-circle + unchecked_icon: mdi:checkbox-blank-circle-outline + - type: custom:mushroom-shopping-list + name: Vertical Shopping List + icon: mdi:arrow-down + layout: vertical + primary_info: name + secondary_info: state diff --git a/README.md b/README.md index 9075f39b9..189119a00 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Different cards are available for differents entities : - 💧 [Humidifier card](docs/cards/humidifier.md) - 🌡 [Climate card](docs/cards/climate.md) - 📑 [Select card](docs/cards/select.md) +- 🛒 [Shopping list card](docs/cards/shopping-list.md) ### Theme customization diff --git a/docs/cards/shopping-list.md b/docs/cards/shopping-list.md new file mode 100644 index 000000000..55a8f4ee1 --- /dev/null +++ b/docs/cards/shopping-list.md @@ -0,0 +1,22 @@ +# Shopping list card + +![Shooping list light](../images/shopping-list-light.png) +![Shopping list dark](../images/shopping-list-dark.png) + +## Description + +A mushroom card for the [shopping_list](https://www.home-assistant.io/integrations/shopping_list) integration. + +## Configuration variables + +All options are available in the lovelace editor but you can use `yaml` if you want. + +| Name | Type | Default | Description | +| :--------------- | :-------------------------------- | :--------------------------- | :----------------------------------------------- | +| `name` | string | Shopping List | Name of the card. May be displayed as its title. | +| `icon` | string | `mdi:cart` | Icon displayed next to the title. | +| `layout` | `default` `horizontal` `vertical` | `default` | Affects the internal layout of the card. | +| `primary_info` | `name` `state` `none` | `name` | Info to show as title. | +| `secondary_info` | `name` `state` `none` | `state` | Info to show as subtitle. | +| `checked_icon` | string | `mdi:checkbox-marked` | Icon used for completed items. | +| `unchecked_icon` | string | `mdi:checkbox-blank-outline` | Icon used for open items. | diff --git a/docs/images/shopping-list-dark.png b/docs/images/shopping-list-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a046f6fdde2ed217269199e6bc51006e24b862f4 GIT binary patch literal 44879 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 Qfq!S!G)|?SG`sV^0N_9UkN^Mx literal 0 HcmV?d00001 diff --git a/src/cards/shopping-list-card/const.ts b/src/cards/shopping-list-card/const.ts new file mode 100644 index 000000000..4c9cef337 --- /dev/null +++ b/src/cards/shopping-list-card/const.ts @@ -0,0 +1,9 @@ +import { PREFIX_NAME } from "../../const"; + +export const SHOPPING_LIST_CARD_NAME = `${PREFIX_NAME}-shopping-list`; +export const SHOPPING_LIST_CARD_EDITOR_NAME = `${SHOPPING_LIST_CARD_NAME}-editor`; +export const SHOPPING_LIST_CARD_ITEM_NAME = `${SHOPPING_LIST_CARD_NAME}-item`; +export const SHOPPING_LIST_CARD_DIVIDER_NAME = `${SHOPPING_LIST_CARD_NAME}-divider`; +export const SHOPPING_LIST_CARD_INPUT_NAME = `${SHOPPING_LIST_CARD_NAME}-input`; +export const SHOPPING_LIST_ENTITY_DOMAINS = []; +export const SHOPPING_LIST_UPDATED_EVENT = "shopping_list_updated"; diff --git a/src/cards/shopping-list-card/shopping-list-card-config.ts b/src/cards/shopping-list-card/shopping-list-card-config.ts new file mode 100644 index 000000000..8773a03cd --- /dev/null +++ b/src/cards/shopping-list-card/shopping-list-card-config.ts @@ -0,0 +1,26 @@ +import { assign, string, object, optional } from "superstruct"; +import { LovelaceCardConfig } from "../../ha"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, +} from "../../shared/config/appearance-config"; +import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; + +export type ShoppingListCardConfig = LovelaceCardConfig & + AppearanceSharedConfig & { + name?: string; + icon?: string; + checked_icon?: string; + unchecked_icon?: string; + }; + +export const shoppingListCardConfigStruct = assign( + lovelaceCardConfigStruct, + appearanceSharedConfigStruct, + object({ + name: optional(string()), + icon: optional(string()), + checked_icon: optional(string()), + unchecked_icon: optional(string()), + }) +); diff --git a/src/cards/shopping-list-card/shopping-list-card-divider.ts b/src/cards/shopping-list-card/shopping-list-card-divider.ts new file mode 100644 index 000000000..323897223 --- /dev/null +++ b/src/cards/shopping-list-card/shopping-list-card-divider.ts @@ -0,0 +1,84 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; +import { fireEvent } from "../../ha"; +import setupCustomlocalize from "../../localize"; +import "../../shared/badge-icon"; +import "../../shared/button"; +import "../../shared/card"; +import "../../shared/shape-avatar"; +import "../../shared/shape-icon"; +import "../../shared/state-info"; +import "../../shared/state-item"; +import { SHOPPING_LIST_CARD_DIVIDER_NAME } from "./const"; + +@customElement(SHOPPING_LIST_CARD_DIVIDER_NAME) +export class ShoppingListCardDivider extends LitElement { + @property() localize: (key: string) => string = (key) => key; + + private _buttonClicked() { + const event = new CustomEvent("clear-completed", { bubbles: true, composed: true }); + this.dispatchEvent(event); + } + + protected render(): TemplateResult { + return html` +
+
+ +
+
+ `; + } + + static get styles(): CSSResultGroup { + return [ + css` + .divider * { + box-sizing: border-box; + } + + .divider { + display: flex; + align-items: center; + margin: 0 calc(var(--spacing) * -1); + } + + .hr-start, + .hr-end { + height: 1px; + background: rgba(var(--rgb-primary-text-color), 0.05); + flex-shrink: 0; + border: none; + min-width: calc(var(--spacing) * 1); + } + + .hr-start { + flex: 1; + } + + .button { + flex-shrink: 0; + box-sizing: border-box; + background: transparent; + border: none; + font-weight: 600; + color: var(--secondary-text-color); + display: flex; + align-items: center; + justify-content: center; + padding: 0 var(--spacing); + cursor: pointer; + } + + .button-icon { + --mdc-icon-size: 20px; + margin-inline-end: 0.5rem; + } + `, + ]; + } +} diff --git a/src/cards/shopping-list-card/shopping-list-card-editor.ts b/src/cards/shopping-list-card/shopping-list-card-editor.ts new file mode 100644 index 000000000..dd459ff96 --- /dev/null +++ b/src/cards/shopping-list-card/shopping-list-card-editor.ts @@ -0,0 +1,84 @@ +import { html, TemplateResult } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import memoizeOne from "memoize-one"; +import { assert } from "superstruct"; +import { fireEvent, LovelaceCardEditor } from "../../ha"; +import setupCustomlocalize from "../../localize"; +import { APPEARANCE_FORM_SCHEMA } from "../../shared/config/appearance-config"; +import { MushroomBaseElement } from "../../utils/base-element"; +import { GENERIC_LABELS } from "../../utils/form/generic-fields"; +import { HaFormSchema } from "../../utils/form/ha-form"; +import { loadHaComponents } from "../../utils/loader"; +import { SHOPPING_LIST_CARD_EDITOR_NAME } from "./const"; +import { ShoppingListCardConfig, shoppingListCardConfigStruct } from "./shopping-list-card-config"; + +export const SHOPPING_LIST_LABELS = ["checked_icon", "unchecked_icon"]; + +const schema = [ + { name: "name", selector: { text: {} } }, + { name: "icon", selector: { icon: {} } }, + { + type: "grid", + name: "", + schema: [ + { name: "layout", selector: { "mush-layout": {} } }, + { name: "primary_info", selector: { "mush-info": {} } }, + { name: "secondary_info", selector: { "mush-info": {} } }, + ], + }, + { + type: "grid", + name: "", + schema: [ + { name: "unchecked_icon", selector: { icon: {} } }, + { name: "checked_icon", selector: { icon: {} } }, + ], + }, +]; + +@customElement(SHOPPING_LIST_CARD_EDITOR_NAME) +export class ShoppingListCardEditor extends MushroomBaseElement implements LovelaceCardEditor { + @state() private _config?: ShoppingListCardConfig; + + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } + + public setConfig(config: ShoppingListCardConfig): void { + assert(config, shoppingListCardConfigStruct); + this._config = config; + } + + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); + + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); + } + if (SHOPPING_LIST_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.shopping_list.${schema.name}`); + } + return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); + }; + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } +} diff --git a/src/cards/shopping-list-card/shopping-list-card-input.ts b/src/cards/shopping-list-card/shopping-list-card-input.ts new file mode 100644 index 000000000..77b736a65 --- /dev/null +++ b/src/cards/shopping-list-card/shopping-list-card-input.ts @@ -0,0 +1,61 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; +import { fireEvent } from "../../ha"; +import "../../shared/badge-icon"; +import "../../shared/button"; +import "../../shared/card"; +import "../../shared/shape-avatar"; +import "../../shared/shape-icon"; +import "../../shared/state-info"; +import "../../shared/state-item"; +import { SHOPPING_LIST_CARD_INPUT_NAME } from "./const"; + +@customElement(SHOPPING_LIST_CARD_INPUT_NAME) +export class ShoppingListItemInput extends LitElement { + @property() value: string = ""; + @property() placeholder: string = ""; + + private _handleInput(e) { + fireEvent(this, "value-changed", { value: e.target.value }); + } + + protected render(): TemplateResult { + return html` + + `; + } + + static get styles(): CSSResultGroup { + return [ + css` + .input { + height: 42px; + display: block; + background: rgba(var(--rgb-primary-text-color), 0.05); + transition: background-color 280ms ease-in-out 0s; + border-radius: var(--control-border-radius); + border: none; + box-sizing: border-box; + padding: 0 1rem; + font-weight: 600; + color: var(--primary-text-color); + outline: none !important; + min-width: 0; + width: 100%; + } + + .input:placeholder { + font-weight: 600; + color: var(--secondary-text-color); + } + `, + ]; + } +} diff --git a/src/cards/shopping-list-card/shopping-list-card-item.ts b/src/cards/shopping-list-card/shopping-list-card-item.ts new file mode 100644 index 000000000..f80a10e82 --- /dev/null +++ b/src/cards/shopping-list-card/shopping-list-card-item.ts @@ -0,0 +1,120 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; +import "../../shared/badge-icon"; +import "../../shared/button"; +import "../../shared/card"; +import "../../shared/shape-avatar"; +import "../../shared/shape-icon"; +import "../../shared/state-info"; +import "../../shared/state-item"; +import { SHOPPING_LIST_CARD_ITEM_NAME } from "./const"; + +@customElement(SHOPPING_LIST_CARD_ITEM_NAME) +export class ShoppingListItem extends LitElement { + @property() checked: boolean = false; + @property() value: string = ""; + @property() highlighted: boolean = false; + @property() checkedIcon = "mdi:checkbox-marked"; + @property() uncheckedIcon = "mdi:checkbox-blank-outline"; + + protected render(): TemplateResult { + return html` +
+
+ + +
+ +
+ `; + } + + private _onTextInput(e: Event) { + const event = new CustomEvent("name-change", { + bubbles: true, + composed: true, + detail: { value: (e.target as HTMLInputElement).value }, + }); + this.dispatchEvent(event); + } + + private _onCheckboxChange(e: Event) { + const event = new CustomEvent("complete", { + bubbles: true, + composed: true, + detail: { checked: (e.target as HTMLInputElement).checked }, + }); + this.dispatchEvent(event); + } + + static get styles(): CSSResultGroup { + return [ + css` + .input-wrapper * { + box-sizing: border-box; + outline: none !important; + } + + .input-wrapper { + display: flex; + align-items: center; + border-radius: var(--control-border-radius); + } + + .input-wrapper.greyed-out .input { + color: var(--secondary-text-color); // #9b9b9b; + text-decoration: line-through; + } + + .checkbox-wrapper { + position: relative; + flex-shrink: 0; + height: 42px; + width: 42px; + display: flex; + align-items: center; + justify-content: center; + } + + .checkbox { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + appearance: none; + cursor: pointer; + margin: 0; + } + + .checkbox-icon { + width: 24px; + height: 24px; + pointer-events: none; + } + + .input { + flex: 1; + height: 42px; + min-width: 0; + background: transparent; + border: none; + font-weight: 600; + color: var(--primary-text-color); + overflow: hidden; + text-overflow: ellipsis; + margin-right: var(--spacing); + } + `, + ]; + } +} diff --git a/src/cards/shopping-list-card/shopping-list-card.ts b/src/cards/shopping-list-card/shopping-list-card.ts new file mode 100644 index 000000000..82996f563 --- /dev/null +++ b/src/cards/shopping-list-card/shopping-list-card.ts @@ -0,0 +1,275 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, PropertyValues, TemplateResult } from "lit"; +import { customElement, query, state } from "lit/decorators.js"; +import { repeat } from "lit/directives/repeat.js"; +import { classMap } from "lit/directives/class-map.js"; +import { computeRTL, LovelaceCard, LovelaceCardEditor } from "../../ha"; +import "../../shared/badge-icon"; +import "../../shared/button"; +import "../../shared/card"; +import "../../shared/shape-avatar"; +import "../../shared/shape-icon"; +import "../../shared/state-info"; +import "../../shared/state-item"; +import "./shopping-list-card-item"; +import "./shopping-list-card-divider"; +import "./shopping-list-card-input"; +import { computeAppearance } from "../../utils/appearance"; +import { MushroomBaseCard } from "../../utils/base-card"; +import { cardStyle } from "../../utils/card-styles"; +import { registerCustomCard } from "../../utils/custom-cards"; +import { + SHOPPING_LIST_CARD_EDITOR_NAME, + SHOPPING_LIST_CARD_NAME, + SHOPPING_LIST_UPDATED_EVENT, +} from "./const"; +import { ShoppingListCardConfig } from "./shopping-list-card-config"; +import { addItem, clearItems, fetchItems, getStateDisplay, updateItem } from "./utils"; +import { ShoppingListItem } from "../../ha/data/shopping-list"; +import setupCustomlocalize from "../../localize"; + +registerCustomCard({ + type: SHOPPING_LIST_CARD_NAME, + name: "Mushroom Shopping List Card", + description: "Card for the shopping list integration", +}); + +@customElement(SHOPPING_LIST_CARD_NAME) +export class ShoppingListCard extends MushroomBaseCard implements LovelaceCard { + public static async getConfigElement(): Promise { + await import("./shopping-list-card-editor"); + return document.createElement(SHOPPING_LIST_CARD_EDITOR_NAME) as LovelaceCardEditor; + } + + public static getStubConfig(): ShoppingListCardConfig { + return { + type: `custom:${SHOPPING_LIST_CARD_NAME}`, + name: "Shopping List", + icon: "mdi:cart", + primary_info: "name", + secondary_info: "state", + checked_icon: "mdi:checkbox-marked", + unchecked_icon: "mdi:checkbox-blank-outline", + }; + } + + @state() private _config?: ShoppingListCardConfig; + + @state() private _items?: ShoppingListItem[]; + + @state() private _newItemInputValue = ""; + + getCardSize(): number { + return (this._items?.length ?? 3) + 1; + } + + public setConfig(config: ShoppingListCardConfig): void { + this._config = config; + } + + subscription: Promise | null = null; + private subscribeToExternalEvents() { + if (!this.hass) return; + this.subscription = this.hass.connection.subscribeEvents( + () => this._fetchData(), + SHOPPING_LIST_UPDATED_EVENT + ); + } + + public connectedCallback() { + super.connectedCallback(); + this._fetchData(); + this.subscribeToExternalEvents(); + } + + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (changedProperties.has("hass") && !this.subscription) { + this._fetchData(); + this.subscribeToExternalEvents(); + } + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this.subscription?.then((unsub) => unsub()); + } + + protected render(): TemplateResult { + if (!this._config || !this.hass) { + return html``; + } + + const customLocalize = setupCustomlocalize(this.hass); + + const uncheckedItems = (this._items || []).filter((i) => !i.complete); + const checkedItems = (this._items || []).filter((i) => i.complete); + + const name = this._config.name || ""; + const icon = this._config.icon; + const rtl = computeRTL(this.hass); + const { layout, ...computedAppearance } = computeAppearance(this._config); + + const appearance = { + ...computedAppearance, + // We handle a horizontal layout on our own. + layout: layout === "horizontal" ? "default" : layout, + }; + + const completedCount = this._items?.filter((i) => i.complete)?.length; + const displayState = customLocalize("editor.card.shopping_list.status") + .replace("{completed_count}", `${completedCount || 0}`) + .replace("{total_count}", `${this._items?.length || 0}`); + const primaryState = getStateDisplay(this._config.primary_info, name, displayState); + const secondaryState = getStateDisplay(this._config.secondary_info, name, displayState); + + return html` + + +
+ ${ + primaryState || secondaryState + ? html` + + ${icon + ? html`` + : html``} + + + + ` + : html`` + } +
+ (this._newItemInputValue = e.detail.value)} + @keydown=${this._submitOnEnter} + /> + +
+
+ + ${ + this._items?.length + ? html` +
+ ${repeat(uncheckedItems, (item) => item.id, this._renderItem)} + ${checkedItems.length > 0 + ? html`` + : html``} + ${repeat(checkedItems, (item) => item.id, this._renderItem)} +
+ ` + : html`` + } + ${this.renderIntegrationMissingMessage()} +
+
+ `; + } + + private _renderItem = (item: ShoppingListItem) => { + const { checked_icon, unchecked_icon } = ShoppingListCard.getStubConfig(); + return html` + this._updateItem(item.id, { complete: e.detail.checked })} + @name-change=${(e) => this._updateItem(item.id, { name: e.detail.value })} + > + `; + }; + + private renderIntegrationMissingMessage() { + const customLocalize = setupCustomlocalize(this.hass); + const integrationLoaded = this.hass?.config.components.includes("shopping_list"); + + if (integrationLoaded) return html``; + return html`${customLocalize("editor.card.shopping_list.integration_missing")}`; + } + + private _fetchData = async (): Promise => { + if (!this.hass) return; + this._items = await fetchItems(this.hass); + }; + + private _updateItem(id: string, changes: Partial) { + if (!this.hass) return; + updateItem(this.hass, id, changes).catch(() => this._fetchData()); + } + + private _clearItems(): void { + if (!this.hass) return; + clearItems(this.hass).catch(() => this._fetchData()); + } + + private _addItem(value: string): void { + if (!this.hass) return; + addItem(this.hass, value).catch(() => this._fetchData()); + } + + private _submitNewItem(): void { + if (!this._newItemInputValue) return; + this._addItem(this._newItemInputValue); + this._newItemInputValue = ""; + } + + private _submitOnEnter(ev): void { + if (ev.key === "Enter") { + this._submitNewItem(); + } + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + .layout-stack { + display: flex; + flex-direction: column; + } + + .layout-stack > :not(:last-child) { + margin-bottom: var(--spacing); + } + + .layout-inline { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(0px, 1fr); + gap: var(--spacing); + } + + .new-item-input { + flex: 1; + } + `, + ]; + } +} diff --git a/src/cards/shopping-list-card/utils.ts b/src/cards/shopping-list-card/utils.ts new file mode 100644 index 000000000..18665f191 --- /dev/null +++ b/src/cards/shopping-list-card/utils.ts @@ -0,0 +1,45 @@ +import { HomeAssistant } from "../../ha/types"; +import { ShoppingListItem } from "../../ha/data/shopping-list"; +import { Info } from "../../utils/info"; + +export const getStateDisplay = (info: Info = "name", name: string, stateDisplay: string) => { + switch (info) { + case "name": + return name; + case "state": + return stateDisplay; + case "none": + default: + return undefined; + } +}; + +export const fetchItems = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "shopping_list/items", + }); + +export const updateItem = ( + hass: HomeAssistant, + itemId: string, + item: { + name?: string; + complete?: boolean; + } +): Promise => + hass.callWS({ + type: "shopping_list/items/update", + item_id: itemId, + ...item, + }); + +export const clearItems = (hass: HomeAssistant): Promise => + hass.callWS({ + type: "shopping_list/items/clear", + }); + +export const addItem = (hass: HomeAssistant, name: string): Promise => + hass.callWS({ + type: "shopping_list/items/add", + name, + }); diff --git a/src/ha/data/shopping-list.ts b/src/ha/data/shopping-list.ts new file mode 100644 index 000000000..2a1564bcd --- /dev/null +++ b/src/ha/data/shopping-list.ts @@ -0,0 +1,5 @@ +export interface ShoppingListItem { + id: string; + name: string; + complete: boolean; +} diff --git a/src/mushroom.ts b/src/mushroom.ts index cf8a1c6fe..19865d115 100644 --- a/src/mushroom.ts +++ b/src/mushroom.ts @@ -17,6 +17,7 @@ export { LockCard } from "./cards/lock-card/lock-card"; export { MediaPlayerCard } from "./cards/media-player-card/media-player-card"; export { PersonCard } from "./cards/person-card/person-card"; export { SelectCard } from "./cards/select-card/select-card"; +export { ShoppingListCard } from "./cards/shopping-list-card/shopping-list-card"; export { TemplateCard } from "./cards/template-card/template-card"; export { TitleCard } from "./cards/title-card/title-card"; export { UpdateCard } from "./cards/update-card/update-card"; diff --git a/src/translations/de.json b/src/translations/de.json index c24903bcd..c3d25697e 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -136,6 +136,14 @@ "climate": { "show_temperature_control": "Temperatursteuerung?", "hvac_modes": "HVAC-Modi" + }, + "shopping_list": { + "add_item_placeholder": "Eintrag hinzufügen …", + "clear_completed": "Erledigte entfernen", + "status": "{completed_count} von {total_count} erledigt", + "checked_icon": "Erledigter Eintrag Icon", + "unchecked_icon": "Offener Eintrag Icon", + "integration_missing": "'shopping_list' Integration ist nicht installiert." } }, "chip": { diff --git a/src/translations/en.json b/src/translations/en.json index ff50e278e..32f299668 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -136,6 +136,14 @@ "climate": { "show_temperature_control": "Temperature control?", "hvac_modes": "HVAC Modes" + }, + "shopping_list": { + "add_item_placeholder": "Add item …", + "clear_completed": "Clear completed", + "status": "{completed_count} of {total_count} completed", + "checked_icon": "Checked item icon", + "unchecked_icon": "Unchecked item icon", + "integration_missing": "'shopping_list' integration is not installed." } }, "chip": { From fd08607477987dfc830b0b2e1135122a89674b08 Mon Sep 17 00:00:00 2001 From: Lennard Scheibel Date: Sun, 5 Nov 2023 16:08:34 +0100 Subject: [PATCH 2/2] Use `todo` entity for todo-list-card --- .hass_dev/lovelace-mushroom-showcase.yaml | 2 +- ...ing-list-view.yaml => todo-list-view.yaml} | 24 +- README.md | 2 +- docs/cards/shopping-list.md | 22 -- docs/cards/todo-list.md | 26 ++ ...pping-list-dark.png => todo-list-dark.png} | Bin ...ing-list-light.png => todo-list-light.png} | Bin src/cards/shopping-list-card/const.ts | 9 - .../shopping-list-card/shopping-list-card.ts | 275 ------------- src/cards/shopping-list-card/utils.ts | 45 --- src/cards/todo-list-card/const.ts | 12 + .../todo-list-card-config.ts} | 15 +- .../todo-list-card-divider.ts} | 11 +- .../todo-list-card-editor.ts} | 31 +- .../todo-list-card-input.ts} | 7 +- .../todo-list-card-item.ts} | 10 +- src/cards/todo-list-card/todo-list-card.ts | 363 ++++++++++++++++++ src/cards/todo-list-card/utils.ts | 67 ++++ src/ha/data/shopping-list.ts | 5 - src/ha/data/todo-list.ts | 21 + src/mushroom.ts | 2 +- src/translations/de.json | 5 +- src/translations/en.json | 5 +- 23 files changed, 545 insertions(+), 414 deletions(-) rename .hass_dev/views/{shopping-list-view.yaml => todo-list-view.yaml} (55%) delete mode 100644 docs/cards/shopping-list.md create mode 100644 docs/cards/todo-list.md rename docs/images/{shopping-list-dark.png => todo-list-dark.png} (100%) rename docs/images/{shopping-list-light.png => todo-list-light.png} (100%) delete mode 100644 src/cards/shopping-list-card/const.ts delete mode 100644 src/cards/shopping-list-card/shopping-list-card.ts delete mode 100644 src/cards/shopping-list-card/utils.ts create mode 100644 src/cards/todo-list-card/const.ts rename src/cards/{shopping-list-card/shopping-list-card-config.ts => todo-list-card/todo-list-card-config.ts} (51%) rename src/cards/{shopping-list-card/shopping-list-card-divider.ts => todo-list-card/todo-list-card-divider.ts} (86%) rename src/cards/{shopping-list-card/shopping-list-card-editor.ts => todo-list-card/todo-list-card-editor.ts} (65%) rename src/cards/{shopping-list-card/shopping-list-card-input.ts => todo-list-card/todo-list-card-input.ts} (89%) rename src/cards/{shopping-list-card/shopping-list-card-item.ts => todo-list-card/todo-list-card-item.ts} (92%) create mode 100644 src/cards/todo-list-card/todo-list-card.ts create mode 100644 src/cards/todo-list-card/utils.ts delete mode 100644 src/ha/data/shopping-list.ts create mode 100644 src/ha/data/todo-list.ts diff --git a/.hass_dev/lovelace-mushroom-showcase.yaml b/.hass_dev/lovelace-mushroom-showcase.yaml index 73c73c73b..f6ff5e2c9 100644 --- a/.hass_dev/lovelace-mushroom-showcase.yaml +++ b/.hass_dev/lovelace-mushroom-showcase.yaml @@ -16,4 +16,4 @@ views: - !include views/humidifier-view.yaml - !include views/select-view.yaml - !include views/number-view.yaml - - !include views/shopping-list-view.yaml + - !include views/todo-list-view.yaml diff --git a/.hass_dev/views/shopping-list-view.yaml b/.hass_dev/views/todo-list-view.yaml similarity index 55% rename from .hass_dev/views/shopping-list-view.yaml rename to .hass_dev/views/todo-list-view.yaml index 6783a36eb..aaa752e71 100644 --- a/.hass_dev/views/shopping-list-view.yaml +++ b/.hass_dev/views/todo-list-view.yaml @@ -1,12 +1,13 @@ -title: Shopping Lists -icon: mdi:cart +title: Todo Lists +icon: mdi:clipboard-list cards: - type: grid - title: Shopping List + title: Todo List cards: - - type: custom:mushroom-shopping-list + - type: custom:mushroom-todo-list + entity: todo.shopping_list name: My Shopping List - icon: mdi:cart + icon: mdi:format-list-checks primary_info: name secondary_info: state columns: 1 @@ -14,8 +15,9 @@ cards: - type: vertical-stack title: Layout cards: - - type: custom:mushroom-shopping-list - name: Horizontal Shopping List + - type: custom:mushroom-todo-list + entity: todo.shopping_list + name: Horizontal Todo List layout: horizontal primary_info: name secondary_info: state @@ -23,11 +25,13 @@ cards: columns: 2 square: false cards: - - type: custom:mushroom-shopping-list + - type: custom:mushroom-todo-list + entity: todo.shopping_list checked_icon: mdi:checkbox-blank-circle unchecked_icon: mdi:checkbox-blank-circle-outline - - type: custom:mushroom-shopping-list - name: Vertical Shopping List + - type: custom:mushroom-todo-list + entity: todo.shopping_list + name: Vertical Todo List icon: mdi:arrow-down layout: vertical primary_info: name diff --git a/README.md b/README.md index b8f1e2b44..eda8f8853 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Different cards are available for differents entities : - 🌡 [Climate card](docs/cards/climate.md) - 📑 [Select card](docs/cards/select.md) - 🔢 [Number card](docs/cards/number.md) -- 🛒 [Shopping list card](docs/cards/shopping-list.md) +- 📃 [Todo list card](docs/cards/todo-list.md) ### Theme customization diff --git a/docs/cards/shopping-list.md b/docs/cards/shopping-list.md deleted file mode 100644 index 55a8f4ee1..000000000 --- a/docs/cards/shopping-list.md +++ /dev/null @@ -1,22 +0,0 @@ -# Shopping list card - -![Shooping list light](../images/shopping-list-light.png) -![Shopping list dark](../images/shopping-list-dark.png) - -## Description - -A mushroom card for the [shopping_list](https://www.home-assistant.io/integrations/shopping_list) integration. - -## Configuration variables - -All options are available in the lovelace editor but you can use `yaml` if you want. - -| Name | Type | Default | Description | -| :--------------- | :-------------------------------- | :--------------------------- | :----------------------------------------------- | -| `name` | string | Shopping List | Name of the card. May be displayed as its title. | -| `icon` | string | `mdi:cart` | Icon displayed next to the title. | -| `layout` | `default` `horizontal` `vertical` | `default` | Affects the internal layout of the card. | -| `primary_info` | `name` `state` `none` | `name` | Info to show as title. | -| `secondary_info` | `name` `state` `none` | `state` | Info to show as subtitle. | -| `checked_icon` | string | `mdi:checkbox-marked` | Icon used for completed items. | -| `unchecked_icon` | string | `mdi:checkbox-blank-outline` | Icon used for open items. | diff --git a/docs/cards/todo-list.md b/docs/cards/todo-list.md new file mode 100644 index 000000000..7ff444fe3 --- /dev/null +++ b/docs/cards/todo-list.md @@ -0,0 +1,26 @@ +# Todo list card + +![Todo list light](../images/todo-list-light.png) +![Todo list dark](../images/todo-list-dark.png) + +## Description + +A mushroom card for the todo list entity. + +## Configuration variables + +All options are available in the lovelace editor but you can use `yaml` if you want. + +| Name | Type | Default | Description | +|:--------------------|:----------------------------------|:-----------------------------|:-------------------------------------------------| +| `entity` | string | Required | Todo list entity. | +| `name` | string | Todo List | Name of the card. May be displayed as its title. | +| `icon` | string | `mdi:format-list-checks` | Icon displayed next to the title. | +| `layout` | `default` `horizontal` `vertical` | `default` | Affects the internal layout of the card. | +| `primary_info` | `name` `state` `none` | `name` | Info to show as title. | +| `secondary_info` | `name` `state` `none` | `state` | Info to show as subtitle. | +| `checked_icon` | string | `mdi:checkbox-marked` | Icon used for completed items. | +| `unchecked_icon` | string | `mdi:checkbox-blank-outline` | Icon used for open items. | +| `tap_action` | action | `none` | Home assistant action to perform on tap. | +| `hold_action` | action | `more-info` | Home assistant action to perform on hold. | +| `double_tap_action` | action | `none` | Home assistant action to perform on double_tap. | diff --git a/docs/images/shopping-list-dark.png b/docs/images/todo-list-dark.png similarity index 100% rename from docs/images/shopping-list-dark.png rename to docs/images/todo-list-dark.png diff --git a/docs/images/shopping-list-light.png b/docs/images/todo-list-light.png similarity index 100% rename from docs/images/shopping-list-light.png rename to docs/images/todo-list-light.png diff --git a/src/cards/shopping-list-card/const.ts b/src/cards/shopping-list-card/const.ts deleted file mode 100644 index 4c9cef337..000000000 --- a/src/cards/shopping-list-card/const.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { PREFIX_NAME } from "../../const"; - -export const SHOPPING_LIST_CARD_NAME = `${PREFIX_NAME}-shopping-list`; -export const SHOPPING_LIST_CARD_EDITOR_NAME = `${SHOPPING_LIST_CARD_NAME}-editor`; -export const SHOPPING_LIST_CARD_ITEM_NAME = `${SHOPPING_LIST_CARD_NAME}-item`; -export const SHOPPING_LIST_CARD_DIVIDER_NAME = `${SHOPPING_LIST_CARD_NAME}-divider`; -export const SHOPPING_LIST_CARD_INPUT_NAME = `${SHOPPING_LIST_CARD_NAME}-input`; -export const SHOPPING_LIST_ENTITY_DOMAINS = []; -export const SHOPPING_LIST_UPDATED_EVENT = "shopping_list_updated"; diff --git a/src/cards/shopping-list-card/shopping-list-card.ts b/src/cards/shopping-list-card/shopping-list-card.ts deleted file mode 100644 index 82996f563..000000000 --- a/src/cards/shopping-list-card/shopping-list-card.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, PropertyValues, TemplateResult } from "lit"; -import { customElement, query, state } from "lit/decorators.js"; -import { repeat } from "lit/directives/repeat.js"; -import { classMap } from "lit/directives/class-map.js"; -import { computeRTL, LovelaceCard, LovelaceCardEditor } from "../../ha"; -import "../../shared/badge-icon"; -import "../../shared/button"; -import "../../shared/card"; -import "../../shared/shape-avatar"; -import "../../shared/shape-icon"; -import "../../shared/state-info"; -import "../../shared/state-item"; -import "./shopping-list-card-item"; -import "./shopping-list-card-divider"; -import "./shopping-list-card-input"; -import { computeAppearance } from "../../utils/appearance"; -import { MushroomBaseCard } from "../../utils/base-card"; -import { cardStyle } from "../../utils/card-styles"; -import { registerCustomCard } from "../../utils/custom-cards"; -import { - SHOPPING_LIST_CARD_EDITOR_NAME, - SHOPPING_LIST_CARD_NAME, - SHOPPING_LIST_UPDATED_EVENT, -} from "./const"; -import { ShoppingListCardConfig } from "./shopping-list-card-config"; -import { addItem, clearItems, fetchItems, getStateDisplay, updateItem } from "./utils"; -import { ShoppingListItem } from "../../ha/data/shopping-list"; -import setupCustomlocalize from "../../localize"; - -registerCustomCard({ - type: SHOPPING_LIST_CARD_NAME, - name: "Mushroom Shopping List Card", - description: "Card for the shopping list integration", -}); - -@customElement(SHOPPING_LIST_CARD_NAME) -export class ShoppingListCard extends MushroomBaseCard implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./shopping-list-card-editor"); - return document.createElement(SHOPPING_LIST_CARD_EDITOR_NAME) as LovelaceCardEditor; - } - - public static getStubConfig(): ShoppingListCardConfig { - return { - type: `custom:${SHOPPING_LIST_CARD_NAME}`, - name: "Shopping List", - icon: "mdi:cart", - primary_info: "name", - secondary_info: "state", - checked_icon: "mdi:checkbox-marked", - unchecked_icon: "mdi:checkbox-blank-outline", - }; - } - - @state() private _config?: ShoppingListCardConfig; - - @state() private _items?: ShoppingListItem[]; - - @state() private _newItemInputValue = ""; - - getCardSize(): number { - return (this._items?.length ?? 3) + 1; - } - - public setConfig(config: ShoppingListCardConfig): void { - this._config = config; - } - - subscription: Promise | null = null; - private subscribeToExternalEvents() { - if (!this.hass) return; - this.subscription = this.hass.connection.subscribeEvents( - () => this._fetchData(), - SHOPPING_LIST_UPDATED_EVENT - ); - } - - public connectedCallback() { - super.connectedCallback(); - this._fetchData(); - this.subscribeToExternalEvents(); - } - - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (changedProperties.has("hass") && !this.subscription) { - this._fetchData(); - this.subscribeToExternalEvents(); - } - } - - public disconnectedCallback() { - super.disconnectedCallback(); - this.subscription?.then((unsub) => unsub()); - } - - protected render(): TemplateResult { - if (!this._config || !this.hass) { - return html``; - } - - const customLocalize = setupCustomlocalize(this.hass); - - const uncheckedItems = (this._items || []).filter((i) => !i.complete); - const checkedItems = (this._items || []).filter((i) => i.complete); - - const name = this._config.name || ""; - const icon = this._config.icon; - const rtl = computeRTL(this.hass); - const { layout, ...computedAppearance } = computeAppearance(this._config); - - const appearance = { - ...computedAppearance, - // We handle a horizontal layout on our own. - layout: layout === "horizontal" ? "default" : layout, - }; - - const completedCount = this._items?.filter((i) => i.complete)?.length; - const displayState = customLocalize("editor.card.shopping_list.status") - .replace("{completed_count}", `${completedCount || 0}`) - .replace("{total_count}", `${this._items?.length || 0}`); - const primaryState = getStateDisplay(this._config.primary_info, name, displayState); - const secondaryState = getStateDisplay(this._config.secondary_info, name, displayState); - - return html` - - -
- ${ - primaryState || secondaryState - ? html` - - ${icon - ? html`` - : html``} - - - - ` - : html`` - } -
- (this._newItemInputValue = e.detail.value)} - @keydown=${this._submitOnEnter} - /> - -
-
- - ${ - this._items?.length - ? html` -
- ${repeat(uncheckedItems, (item) => item.id, this._renderItem)} - ${checkedItems.length > 0 - ? html`` - : html``} - ${repeat(checkedItems, (item) => item.id, this._renderItem)} -
- ` - : html`` - } - ${this.renderIntegrationMissingMessage()} -
-
- `; - } - - private _renderItem = (item: ShoppingListItem) => { - const { checked_icon, unchecked_icon } = ShoppingListCard.getStubConfig(); - return html` - this._updateItem(item.id, { complete: e.detail.checked })} - @name-change=${(e) => this._updateItem(item.id, { name: e.detail.value })} - > - `; - }; - - private renderIntegrationMissingMessage() { - const customLocalize = setupCustomlocalize(this.hass); - const integrationLoaded = this.hass?.config.components.includes("shopping_list"); - - if (integrationLoaded) return html``; - return html`${customLocalize("editor.card.shopping_list.integration_missing")}`; - } - - private _fetchData = async (): Promise => { - if (!this.hass) return; - this._items = await fetchItems(this.hass); - }; - - private _updateItem(id: string, changes: Partial) { - if (!this.hass) return; - updateItem(this.hass, id, changes).catch(() => this._fetchData()); - } - - private _clearItems(): void { - if (!this.hass) return; - clearItems(this.hass).catch(() => this._fetchData()); - } - - private _addItem(value: string): void { - if (!this.hass) return; - addItem(this.hass, value).catch(() => this._fetchData()); - } - - private _submitNewItem(): void { - if (!this._newItemInputValue) return; - this._addItem(this._newItemInputValue); - this._newItemInputValue = ""; - } - - private _submitOnEnter(ev): void { - if (ev.key === "Enter") { - this._submitNewItem(); - } - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - .layout-stack { - display: flex; - flex-direction: column; - } - - .layout-stack > :not(:last-child) { - margin-bottom: var(--spacing); - } - - .layout-inline { - display: grid; - grid-auto-flow: column; - grid-auto-columns: minmax(0px, 1fr); - gap: var(--spacing); - } - - .new-item-input { - flex: 1; - } - `, - ]; - } -} diff --git a/src/cards/shopping-list-card/utils.ts b/src/cards/shopping-list-card/utils.ts deleted file mode 100644 index 18665f191..000000000 --- a/src/cards/shopping-list-card/utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { HomeAssistant } from "../../ha/types"; -import { ShoppingListItem } from "../../ha/data/shopping-list"; -import { Info } from "../../utils/info"; - -export const getStateDisplay = (info: Info = "name", name: string, stateDisplay: string) => { - switch (info) { - case "name": - return name; - case "state": - return stateDisplay; - case "none": - default: - return undefined; - } -}; - -export const fetchItems = (hass: HomeAssistant): Promise => - hass.callWS({ - type: "shopping_list/items", - }); - -export const updateItem = ( - hass: HomeAssistant, - itemId: string, - item: { - name?: string; - complete?: boolean; - } -): Promise => - hass.callWS({ - type: "shopping_list/items/update", - item_id: itemId, - ...item, - }); - -export const clearItems = (hass: HomeAssistant): Promise => - hass.callWS({ - type: "shopping_list/items/clear", - }); - -export const addItem = (hass: HomeAssistant, name: string): Promise => - hass.callWS({ - type: "shopping_list/items/add", - name, - }); diff --git a/src/cards/todo-list-card/const.ts b/src/cards/todo-list-card/const.ts new file mode 100644 index 000000000..bf35f098d --- /dev/null +++ b/src/cards/todo-list-card/const.ts @@ -0,0 +1,12 @@ +import { PREFIX_NAME } from "../../const"; + +export const TODO_LIST_CARD_NAME = `${PREFIX_NAME}-todo-list`; +export const TODO_LIST_CARD_EDITOR_NAME = `${TODO_LIST_CARD_NAME}-editor`; +export const TODO_LIST_CARD_ITEM_NAME = `${TODO_LIST_CARD_NAME}-item`; +export const TODO_LIST_CARD_DIVIDER_NAME = `${TODO_LIST_CARD_NAME}-divider`; +export const TODO_LIST_CARD_INPUT_NAME = `${TODO_LIST_CARD_NAME}-input`; +export const TODO_LIST_ENTITY_DOMAINS = ["todo"]; +export const TODO_LIST_UPDATED_EVENT = "shopping_list_updated"; // Still called shopping_list + +export const DEFAULT_CHECKED_ICON = "mdi:checkbox-marked"; +export const DEFAULT_UNCHECKED_ICON = "mdi:checkbox-blank-outline"; diff --git a/src/cards/shopping-list-card/shopping-list-card-config.ts b/src/cards/todo-list-card/todo-list-card-config.ts similarity index 51% rename from src/cards/shopping-list-card/shopping-list-card-config.ts rename to src/cards/todo-list-card/todo-list-card-config.ts index 8773a03cd..a90231409 100644 --- a/src/cards/shopping-list-card/shopping-list-card-config.ts +++ b/src/cards/todo-list-card/todo-list-card-config.ts @@ -1,25 +1,24 @@ -import { assign, string, object, optional } from "superstruct"; +import { assign, object, optional, string } from "superstruct"; import { LovelaceCardConfig } from "../../ha"; import { AppearanceSharedConfig, appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; +import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { actionsSharedConfigStruct } from "../../shared/config/actions-config"; -export type ShoppingListCardConfig = LovelaceCardConfig & +export type TodoListCardConfig = LovelaceCardConfig & + EntitySharedConfig & AppearanceSharedConfig & { - name?: string; - icon?: string; checked_icon?: string; unchecked_icon?: string; }; -export const shoppingListCardConfigStruct = assign( +export const todoListCardConfigStruct = assign( lovelaceCardConfigStruct, - appearanceSharedConfigStruct, + assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), object({ - name: optional(string()), - icon: optional(string()), checked_icon: optional(string()), unchecked_icon: optional(string()), }) diff --git a/src/cards/shopping-list-card/shopping-list-card-divider.ts b/src/cards/todo-list-card/todo-list-card-divider.ts similarity index 86% rename from src/cards/shopping-list-card/shopping-list-card-divider.ts rename to src/cards/todo-list-card/todo-list-card-divider.ts index 323897223..578c9a9d7 100644 --- a/src/cards/shopping-list-card/shopping-list-card-divider.ts +++ b/src/cards/todo-list-card/todo-list-card-divider.ts @@ -1,8 +1,5 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { classMap } from "lit/directives/class-map.js"; -import { fireEvent } from "../../ha"; -import setupCustomlocalize from "../../localize"; import "../../shared/badge-icon"; import "../../shared/button"; import "../../shared/card"; @@ -10,10 +7,10 @@ import "../../shared/shape-avatar"; import "../../shared/shape-icon"; import "../../shared/state-info"; import "../../shared/state-item"; -import { SHOPPING_LIST_CARD_DIVIDER_NAME } from "./const"; +import { TODO_LIST_CARD_DIVIDER_NAME } from "./const"; -@customElement(SHOPPING_LIST_CARD_DIVIDER_NAME) -export class ShoppingListCardDivider extends LitElement { +@customElement(TODO_LIST_CARD_DIVIDER_NAME) +export class TodoListCardDivider extends LitElement { @property() localize: (key: string) => string = (key) => key; private _buttonClicked() { @@ -27,7 +24,7 @@ export class ShoppingListCardDivider extends LitElement {

diff --git a/src/cards/shopping-list-card/shopping-list-card-editor.ts b/src/cards/todo-list-card/todo-list-card-editor.ts similarity index 65% rename from src/cards/shopping-list-card/shopping-list-card-editor.ts rename to src/cards/todo-list-card/todo-list-card-editor.ts index dd459ff96..25a04fe5b 100644 --- a/src/cards/shopping-list-card/shopping-list-card-editor.ts +++ b/src/cards/todo-list-card/todo-list-card-editor.ts @@ -1,29 +1,29 @@ import { html, TemplateResult } from "lit"; import { customElement, state } from "lit/decorators.js"; -import memoizeOne from "memoize-one"; import { assert } from "superstruct"; import { fireEvent, LovelaceCardEditor } from "../../ha"; import setupCustomlocalize from "../../localize"; -import { APPEARANCE_FORM_SCHEMA } from "../../shared/config/appearance-config"; import { MushroomBaseElement } from "../../utils/base-element"; import { GENERIC_LABELS } from "../../utils/form/generic-fields"; import { HaFormSchema } from "../../utils/form/ha-form"; import { loadHaComponents } from "../../utils/loader"; -import { SHOPPING_LIST_CARD_EDITOR_NAME } from "./const"; -import { ShoppingListCardConfig, shoppingListCardConfigStruct } from "./shopping-list-card-config"; +import { TODO_LIST_CARD_EDITOR_NAME, TODO_LIST_ENTITY_DOMAINS } from "./const"; +import { TodoListCardConfig, todoListCardConfigStruct } from "./todo-list-card-config"; +import { computeActionsFormSchema } from "../../shared/config/actions-config"; -export const SHOPPING_LIST_LABELS = ["checked_icon", "unchecked_icon"]; +export const TODO_LIST_LABELS = ["checked_icon", "unchecked_icon"]; const schema = [ + { name: "entity", selector: { entity: { domain: TODO_LIST_ENTITY_DOMAINS } } }, { name: "name", selector: { text: {} } }, { name: "icon", selector: { icon: {} } }, { type: "grid", name: "", schema: [ - { name: "layout", selector: { "mush-layout": {} } }, - { name: "primary_info", selector: { "mush-info": {} } }, - { name: "secondary_info", selector: { "mush-info": {} } }, + { name: "layout", selector: { mush_layout: {} } }, + { name: "primary_info", selector: { mush_info: {} } }, + { name: "secondary_info", selector: { mush_info: {} } }, ], }, { @@ -34,19 +34,20 @@ const schema = [ { name: "checked_icon", selector: { icon: {} } }, ], }, + ...computeActionsFormSchema(), ]; -@customElement(SHOPPING_LIST_CARD_EDITOR_NAME) -export class ShoppingListCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: ShoppingListCardConfig; +@customElement(TODO_LIST_CARD_EDITOR_NAME) +export class TodoListCardEditor extends MushroomBaseElement implements LovelaceCardEditor { + @state() private _config?: TodoListCardConfig; connectedCallback() { super.connectedCallback(); void loadHaComponents(); } - public setConfig(config: ShoppingListCardConfig): void { - assert(config, shoppingListCardConfigStruct); + public setConfig(config: TodoListCardConfig): void { + assert(config, todoListCardConfigStruct); this._config = config; } @@ -56,8 +57,8 @@ export class ShoppingListCardEditor extends MushroomBaseElement implements Lovel if (GENERIC_LABELS.includes(schema.name)) { return customLocalize(`editor.card.generic.${schema.name}`); } - if (SHOPPING_LIST_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.shopping_list.${schema.name}`); + if (TODO_LIST_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.todo_list.${schema.name}`); } return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); }; diff --git a/src/cards/shopping-list-card/shopping-list-card-input.ts b/src/cards/todo-list-card/todo-list-card-input.ts similarity index 89% rename from src/cards/shopping-list-card/shopping-list-card-input.ts rename to src/cards/todo-list-card/todo-list-card-input.ts index 77b736a65..d3df070f5 100644 --- a/src/cards/shopping-list-card/shopping-list-card-input.ts +++ b/src/cards/todo-list-card/todo-list-card-input.ts @@ -1,6 +1,5 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { classMap } from "lit/directives/class-map.js"; import { fireEvent } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/button"; @@ -9,10 +8,10 @@ import "../../shared/shape-avatar"; import "../../shared/shape-icon"; import "../../shared/state-info"; import "../../shared/state-item"; -import { SHOPPING_LIST_CARD_INPUT_NAME } from "./const"; +import { TODO_LIST_CARD_INPUT_NAME } from "./const"; -@customElement(SHOPPING_LIST_CARD_INPUT_NAME) -export class ShoppingListItemInput extends LitElement { +@customElement(TODO_LIST_CARD_INPUT_NAME) +export class TodoListItemInput extends LitElement { @property() value: string = ""; @property() placeholder: string = ""; diff --git a/src/cards/shopping-list-card/shopping-list-card-item.ts b/src/cards/todo-list-card/todo-list-card-item.ts similarity index 92% rename from src/cards/shopping-list-card/shopping-list-card-item.ts rename to src/cards/todo-list-card/todo-list-card-item.ts index f80a10e82..cf44910fc 100644 --- a/src/cards/shopping-list-card/shopping-list-card-item.ts +++ b/src/cards/todo-list-card/todo-list-card-item.ts @@ -8,15 +8,15 @@ import "../../shared/shape-avatar"; import "../../shared/shape-icon"; import "../../shared/state-info"; import "../../shared/state-item"; -import { SHOPPING_LIST_CARD_ITEM_NAME } from "./const"; +import { DEFAULT_CHECKED_ICON, DEFAULT_UNCHECKED_ICON, TODO_LIST_CARD_ITEM_NAME } from "./const"; -@customElement(SHOPPING_LIST_CARD_ITEM_NAME) -export class ShoppingListItem extends LitElement { +@customElement(TODO_LIST_CARD_ITEM_NAME) +export class TodoListItem extends LitElement { @property() checked: boolean = false; @property() value: string = ""; @property() highlighted: boolean = false; - @property() checkedIcon = "mdi:checkbox-marked"; - @property() uncheckedIcon = "mdi:checkbox-blank-outline"; + @property() checkedIcon = DEFAULT_CHECKED_ICON; + @property() uncheckedIcon = DEFAULT_UNCHECKED_ICON; protected render(): TemplateResult { return html` diff --git a/src/cards/todo-list-card/todo-list-card.ts b/src/cards/todo-list-card/todo-list-card.ts new file mode 100644 index 000000000..421b98886 --- /dev/null +++ b/src/cards/todo-list-card/todo-list-card.ts @@ -0,0 +1,363 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, PropertyValues, TemplateResult } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { repeat } from "lit/directives/repeat.js"; +import { classMap } from "lit/directives/class-map.js"; +import { + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + isActive, + LovelaceCard, + LovelaceCardEditor, +} from "../../ha"; +import "../../shared/badge-icon"; +import "../../shared/button"; +import "../../shared/card"; +import "../../shared/shape-avatar"; +import "../../shared/shape-icon"; +import "../../shared/state-info"; +import "../../shared/state-item"; +import "./todo-list-card-item"; +import "./todo-list-card-divider"; +import "./todo-list-card-input"; +import { computeAppearance } from "../../utils/appearance"; +import { MushroomBaseCard } from "../../utils/base-card"; +import { cardStyle } from "../../utils/card-styles"; +import { registerCustomCard } from "../../utils/custom-cards"; +import { + DEFAULT_CHECKED_ICON, + DEFAULT_UNCHECKED_ICON, + TODO_LIST_CARD_EDITOR_NAME, + TODO_LIST_CARD_NAME, + TODO_LIST_ENTITY_DOMAINS, + TODO_LIST_UPDATED_EVENT, +} from "./const"; +import { TodoListCardConfig } from "./todo-list-card-config"; +import { createItem, deleteItems, fetchItems, getStateDisplay, updateItem } from "./utils"; +import setupCustomlocalize from "../../localize"; +import { TodoItem, TodoItemStatus } from "../../ha/data/todo-list"; + +registerCustomCard({ + type: TODO_LIST_CARD_NAME, + name: "Mushroom Todo List Card", + description: "Card for todo list entity", +}); + +@customElement(TODO_LIST_CARD_NAME) +export class TodoListCard extends MushroomBaseCard implements LovelaceCard { + public static async getConfigElement(): Promise { + await import("./todo-list-card-editor"); + return document.createElement(TODO_LIST_CARD_EDITOR_NAME) as LovelaceCardEditor; + } + + public static getStubConfig(hass: HomeAssistant): TodoListCardConfig { + const entities = Object.keys(hass.states); + const lists = entities.filter((e) => TODO_LIST_ENTITY_DOMAINS.includes(e.split(".")[0])); + + return { + type: `custom:${TODO_LIST_CARD_NAME}`, + entity: lists[0], + name: "Todo List", + icon: "mdi:format-list-checks", + primary_info: "name", + secondary_info: "state", + checked_icon: DEFAULT_CHECKED_ICON, + unchecked_icon: DEFAULT_UNCHECKED_ICON, + }; + } + + @state() private _config?: TodoListCardConfig; + + @state() private _items?: TodoItem[]; + + @state() private _newItemInputValue = ""; + + getCardSize(): number { + return (this._items?.length ?? 3) + 1; + } + + public setConfig(config: TodoListCardConfig): void { + this._config = { + tap_action: { + action: "none", + }, + hold_action: { + action: "more-info", + }, + ...config, + }; + } + + private isLegacyShoppingListEntity() { + if (!this._config?.entity) return false; + return this.hass?.entities[this._config.entity]?.platform === "shopping_list"; + } + + subscription: Promise | null = null; + private subscribeToLegacyShoppingListEvents() { + if (!this.hass) return; + this.subscription = this.hass.connection.subscribeEvents( + () => this._fetchData(), + TODO_LIST_UPDATED_EVENT + ); + } + + public connectedCallback() { + super.connectedCallback(); + this._fetchData(); + + if (this.isLegacyShoppingListEntity()) { + this.subscribeToLegacyShoppingListEvents(); + } + } + + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + + if (changedProperties.has("hass") && !this.subscription) { + this._fetchData(); + + if (this.isLegacyShoppingListEntity()) { + this.subscribeToLegacyShoppingListEvents(); + } + } + + if (!this.isLegacyShoppingListEntity()) { + const entityId = this._config?.entity; + const oldHass = changedProperties.get("hass") as HomeAssistant | undefined; + + if (entityId && oldHass && oldHass.states[entityId] !== this.hass.states[entityId]) { + this._fetchData(); + } + } + } + + public disconnectedCallback() { + super.disconnectedCallback(); + } + + protected render(): TemplateResult { + if (!this._config || !this.hass) { + return html``; + } + + const entityId = this._config.entity; + const stateObj = entityId ? this.hass.states[entityId] : null; + + if (!stateObj) { + return this.renderNotFound(this._config); + } + + const customLocalize = setupCustomlocalize(this.hass); + + const uncheckedItems = (this._items || []).filter( + (i) => i.status === TodoItemStatus.NeedsAction + ); + const checkedItems = (this._items || []).filter( + (i) => i.status === TodoItemStatus.Completed + ); + + const name = this._config.name || ""; + const icon = this._config.icon; + const rtl = computeRTL(this.hass); + const { layout, ...computedAppearance } = computeAppearance(this._config); + + const appearance = { + ...computedAppearance, + // We handle a horizontal layout on our own. + layout: layout === "horizontal" ? "default" : layout, + }; + + const completedCount = checkedItems?.length; + const displayState = customLocalize("editor.card.todo_list.status") + .replace("{completed_count}", `${completedCount || 0}`) + .replace("{total_count}", `${this._items?.length || 0}`); + const primaryState = getStateDisplay(this._config.primary_info, name, displayState); + const secondaryState = getStateDisplay(this._config.secondary_info, name, displayState); + + const active = isActive(stateObj); + return html` + + +
+ ${ + primaryState || secondaryState + ? html` + + ${icon + ? html` + + ` + : html``} + + + + ` + : html`` + } +
+ (this._newItemInputValue = e.detail.value)} + @keydown=${this._submitOnEnter} + /> + +
+
+ + ${ + this._items?.length + ? html` +
+ ${repeat( + uncheckedItems, + (item) => item.uid, + this._renderItem + )} + ${checkedItems.length > 0 + ? html`` + : html``} + ${repeat(checkedItems, (item) => item.uid, this._renderItem)} +
+ ` + : html`` + } +
+
+ `; + } + + private _renderItem = (item: TodoItem) => { + return html` + + this._updateItem(item.uid, { + status: e.detail.checked + ? TodoItemStatus.Completed + : TodoItemStatus.NeedsAction, + })} + @name-change=${(e) => this._updateItem(item.uid, { summary: e.detail.value })} + > + `; + }; + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + private _getItem(itemId: string) { + return this._items?.find((item) => item.uid === itemId); + } + + private _fetchData = async (): Promise => { + if (!this.hass || !this._config?.entity) return; + this._items = await fetchItems(this.hass, this._config.entity); + }; + + private _updateItem(id: string, changes: Partial) { + const item = this._getItem(id); + if (!item || !this.hass || !this._config?.entity) return; + updateItem(this.hass, this._config.entity, { ...item, ...changes }).catch(() => + this._fetchData() + ); + } + + private async _clearItems(): Promise { + if (!this.hass || !this._config?.entity) return; + const completedUids = this._items + ?.filter((i) => i.status === TodoItemStatus.Completed) + .map((item) => item.uid); + + if (!completedUids?.length) return; + + try { + await deleteItems(this.hass, this._config.entity, completedUids); + } finally { + // Have to use finally here because no change event is emitted by backend. + this._fetchData(); + } + } + + private _addItem(value: string): void { + if (!this.hass || !this._config?.entity) return; + createItem(this.hass, this._config.entity, value).catch(() => this._fetchData()); + } + + private _submitNewItem(): void { + if (!this._newItemInputValue) return; + this._addItem(this._newItemInputValue); + this._newItemInputValue = ""; + } + + private _submitOnEnter(ev): void { + if (ev.key === "Enter") { + this._submitNewItem(); + } + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + .layout-stack { + display: flex; + flex-direction: column; + } + + .layout-stack > :not(:last-child) { + margin-bottom: var(--spacing); + } + + .layout-inline { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(0px, 1fr); + gap: var(--spacing); + } + + .new-item-input { + flex: 1; + } + `, + ]; + } +} diff --git a/src/cards/todo-list-card/utils.ts b/src/cards/todo-list-card/utils.ts new file mode 100644 index 000000000..556c838a6 --- /dev/null +++ b/src/cards/todo-list-card/utils.ts @@ -0,0 +1,67 @@ +import { Info } from "../../utils/info"; +import { HomeAssistant, ServiceCallResponse } from "../../ha"; +import { TodoItem, TodoItems } from "../../ha/data/todo-list"; + +export const getStateDisplay = (info: Info = "name", name: string, stateDisplay: string) => { + switch (info) { + case "name": + return name; + case "state": + return stateDisplay; + case "none": + default: + return undefined; + } +}; + +export const fetchItems = async (hass: HomeAssistant, entityId: string): Promise => { + const result = await hass.callWS({ + type: "todo/item/list", + entity_id: entityId, + }); + return result.items; +}; + +export const updateItem = ( + hass: HomeAssistant, + entity_id: string, + item: TodoItem +): Promise => { + console.log(entity_id); + return hass.callService( + "todo", + "update_item", + { item: item.uid, rename: item.summary, status: item.status }, + { entity_id } + ); +}; + +export const createItem = ( + hass: HomeAssistant, + entity_id: string, + summary: string +): Promise => { + return hass.callService( + "todo", + "add_item", + { + item: summary, + }, + { entity_id } + ); +}; + +export const deleteItems = ( + hass: HomeAssistant, + entity_id: string, + uids: string[] +): Promise => { + return hass.callService( + "todo", + "remove_item", + { + item: uids, + }, + { entity_id } + ); +}; diff --git a/src/ha/data/shopping-list.ts b/src/ha/data/shopping-list.ts deleted file mode 100644 index 2a1564bcd..000000000 --- a/src/ha/data/shopping-list.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ShoppingListItem { - id: string; - name: string; - complete: boolean; -} diff --git a/src/ha/data/todo-list.ts b/src/ha/data/todo-list.ts new file mode 100644 index 000000000..b36d76364 --- /dev/null +++ b/src/ha/data/todo-list.ts @@ -0,0 +1,21 @@ +export const enum TodoItemStatus { + NeedsAction = "needs_action", + Completed = "completed", +} + +export interface TodoItem { + uid: string; + summary: string; + status: TodoItemStatus; +} + +export const enum TodoListEntityFeature { + CREATE_TODO_ITEM = 1, + DELETE_TODO_ITEM = 2, + UPDATE_TODO_ITEM = 4, + MOVE_TODO_ITEM = 8, +} + +export interface TodoItems { + items: TodoItem[]; +} diff --git a/src/mushroom.ts b/src/mushroom.ts index 8c848ea84..1301f5d0a 100644 --- a/src/mushroom.ts +++ b/src/mushroom.ts @@ -18,7 +18,7 @@ import "./cards/lock-card/lock-card"; import "./cards/media-player-card/media-player-card"; import "./cards/person-card/person-card"; import "./cards/select-card/select-card"; -import "./cards/shopping-list-card/shopping-list-card"; +import "./cards/todo-list-card/todo-list-card"; import "./cards/template-card/template-card"; import "./cards/title-card/title-card"; import "./cards/update-card/update-card"; diff --git a/src/translations/de.json b/src/translations/de.json index 2a3241176..ee2d41694 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -145,13 +145,12 @@ "buttons": "Buttons" } }, - "shopping_list": { + "todo_list": { "add_item_placeholder": "Eintrag hinzufügen …", "clear_completed": "Erledigte entfernen", "status": "{completed_count} von {total_count} erledigt", "checked_icon": "Erledigter Eintrag Icon", - "unchecked_icon": "Offener Eintrag Icon", - "integration_missing": "'shopping_list' Integration ist nicht installiert." + "unchecked_icon": "Offener Eintrag Icon" } }, "chip": { diff --git a/src/translations/en.json b/src/translations/en.json index 2016b3e05..c5d1f57ae 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -145,13 +145,12 @@ "buttons": "Buttons" } }, - "shopping_list": { + "todo_list": { "add_item_placeholder": "Add item …", "clear_completed": "Clear completed", "status": "{completed_count} of {total_count} completed", "checked_icon": "Checked item icon", - "unchecked_icon": "Unchecked item icon", - "integration_missing": "'shopping_list' integration is not installed." + "unchecked_icon": "Unchecked item icon" } }, "chip": {