diff --git a/src/bubble-card.ts b/src/bubble-card.ts deleted file mode 100644 index cb11d68e..00000000 --- a/src/bubble-card.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { version } from './var/version.ts'; -import { initializeContent } from './tools/init.ts'; -import { handlePopUp } from './cards/pop-up/index.ts'; -import { handleHorizontalButtonsStack } from './cards/horizontal-buttons-stack/index.ts'; -import { handleButton } from './cards/button/index.ts'; -import { handleSeparator } from './cards/separator/index.ts'; -import { handleCover } from './cards/cover/index.ts'; -import { handleEmptyColumn } from './cards/empty-column/index.ts'; -import { handleMediaPlayer } from './cards/media-player/index.ts'; -import { createBubbleCardEditor } from './editor/bubble-card-editor.ts'; - -class BubbleCard extends HTMLElement { - editor = false; - isConnected = false; - - connectedCallback() { - this.isConnected = true; - - if (this._hass) { - this.updateBubbleCard(); - } - } - - disconnectedCallback() { - this.isConnected = false; - } - - set editMode(editMode) { - if (this.editor === editMode) { - return; - } - - this.editor = editMode; - - if (this._hass) { - this.updateBubbleCard(); - } - } - - set hass(hass) { - initializeContent(this); - - this._hass = hass; - - if (this.isConnected || this.config.card_type === 'pop-up') { - this.updateBubbleCard(); - } - - if (!window.columnFix) { - window.columnFix = this.config.column_fix - } - } - - updateBubbleCard() { - - switch (this.config.card_type) { - - // Update pop-up card - case 'pop-up': - handlePopUp(this); - break; - - // Update button - case 'button' : - handleButton(this); - break; - - // Update separator - case 'separator' : - handleSeparator(this); - break; - - // Update cover card - case 'cover' : - handleCover(this); - break; - - // Update empty card - case 'empty-column' : - handleEmptyColumn(this); - break; - - // Update horizontal buttons stack - case 'horizontal-buttons-stack' : - handleHorizontalButtonsStack(this); - break; - - // Update media player - case 'media-player' : - handleMediaPlayer(this); - break; - } - } - - setConfig(config) { - if (config.card_type === 'pop-up') { - if (!config.hash) { - throw new Error("You need to define an hash. Please note that this card must be placed inside a vertical_stack to work as a pop-up."); - } - } else if (config.card_type === 'horizontal-buttons-stack') { - var definedLinks = {}; - - for (var key in config) { - if (key.match(/^\d+_icon$/)) { - var iconKey = key; - var linkKey = key.replace('_icon', '_link'); - - if (config[linkKey] === undefined) { - throw new Error("You need to define " + linkKey); - } - - if (definedLinks[config[linkKey]]) { - throw new Error("You can't use " + config[linkKey] + " twice" ); - } - - definedLinks[config[linkKey]] = true; - } - } - } else if (config.card_type === 'button' || config.card_type === 'cover') { - if (!config.entity && config.button_type !== 'name') { - throw new Error("You need to define an entity"); - } - } - - if (window.entityError) { - throw new Error("You need to define a valid entity"); - } - if (config.card_type === 'button') { - const enhancedConfig = {...config}; - const buttonType = enhancedConfig.button_type || 'switch'; - - enhancedConfig.tap_action = enhancedConfig.tap_action ?? { - action: "more-info" - }; - enhancedConfig.double_tap_action = enhancedConfig.double_tap_action ?? { - action: buttonType === "state" ? "more-info" : "toggle" - } - enhancedConfig.hold_action = enhancedConfig.hold_action ?? { - action: buttonType === "state" ? "more-info" : "toggle" - } - - this.config = enhancedConfig; - } else { - this.config = config; - } - - if (this._hass) { - this.updateBubbleCard(); - } - } - - getCardSize() { - switch (this.config.card_type) { - case 'pop-up': - return -100000; - case 'button': - return 1; - case 'separator': - return 1; - case 'cover': - return 2; - case 'empty-column': - return 1; - case 'horizontal-buttons-stack': - return 0; - case 'media-player': - return 1; - } - } - - static getConfigElement() { - createBubbleCardEditor(); - return document.createElement("bubble-card-editor"); - } - - getLayoutOptions() { - let defaultRows = 1; - if (['popup', 'horizontal-buttons-stack'].includes(this.config.card_type)) { - defaultRows = 0; - } else if (['cover'].includes(this.config.card_type)) { - defaultRows = 2; - } - - let defaultColumns = 4; - if (['popup', 'horizontal-buttons-stack'].includes(this.config.card_type)) { - defaultColumns = 0; - } - - return { - grid_columns: this.config.columns ?? defaultColumns, - grid_rows: this.config.rows ?? defaultRows, - } - } -} - -customElements.define("bubble-card", BubbleCard); - -window.customCards = window.customCards || []; -window.customCards.push({ - type: "bubble-card", - name: "Bubble Card", - preview: false, - description: "A minimalist card collection with a nice pop-up touch.", - documentationURL: "https://github.com/Clooos/Bubble-Card/" -}); - -console.info( - `%c Bubble Card %c ${version} `, - 'background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 14px 0 0 14px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)', - 'background-color: #506eac;color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 14px 14px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)' -); \ No newline at end of file diff --git a/src/cards/button/changes.ts b/src/cards/button/changes.ts deleted file mode 100644 index 75d76e1a..00000000 --- a/src/cards/button/changes.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { getButtonType } from "./helpers.ts"; -import { initializesubButtonIcon } from '../../tools/global-changes.ts'; -import { - applyScrollingEffect, - getIcon, - getIconColor, - getImage, - getName, - getState, - getAttribute, - isStateOn, - isEntityType, - getWeatherIcon, - setLayout -} from '../../tools/utils.ts'; - -export function changeButton(context) { - const buttonType = getButtonType(context); - const isLight = isEntityType(context, "light"); - const isOn = isStateOn(context); - const lightColor = getIconColor(context); - - if (buttonType === 'switch' && isOn) { - if (lightColor && isLight) { - context.card.style.setProperty('--bubble-button-background-color', getIconColor(context)); - context.elements.buttonBackground.style.opacity = '.5'; - } else { - context.card.style.setProperty('--bubble-button-background-color', 'var(--accent-color)'); - context.elements.buttonBackground.style.opacity = '1'; - } - } else { - context.card.style.setProperty('--bubble-button-background-color', 'rgba(0, 0, 0, 0)'); - context.elements.buttonBackground.style.opacity = '.5'; - } -} -export function changeIcon(context) { - const buttonType = getButtonType(context); - const isOn = buttonType !== 'name' ? isStateOn(context) : false; - const icon = buttonType !== 'name' ? getIcon(context) : context.config.icon; - const image = buttonType !== 'name' ? getImage(context) : ''; - const isLight = buttonType !== 'name' ? isEntityType(context, "light") : false; - - if (isLight && isOn) { - context.elements.iconContainer.style.color = getIconColor(context); - } else { - context.elements.iconContainer.style.color = ''; - } - - if (image !== '') { - context.elements.image.style.backgroundImage = 'url(' + image + ')'; - context.elements.icon.style.display = 'none'; - context.elements.image.style.display = ''; - } else if (icon !== '') { - context.elements.icon.icon = icon; - context.elements.icon.style.color = isOn && buttonType !== 'state' ? getIconColor(context) : 'inherit'; - context.elements.icon.style.display = ''; - context.elements.image.style.display = 'none'; - } else { - context.elements.icon.style.display = 'none'; - context.elements.image.style.display = 'none'; - } -} -export function changeName(context) { - const buttonType = getButtonType(context); - const name = buttonType !== 'name' ? getName(context) : context.config.name; - if (name !== context.elements.previousName) { - applyScrollingEffect(context, context.elements.name, name); - context.elements.previousName = name; - } -} -export function changeSlider(context) { - const buttonType = getButtonType(context); - - if (buttonType === 'slider') { - context.elements.rangeFill.style.backgroundColor = getIconColor(context); - - if (context.dragging) return; - - let percentage = 0; - - if (isEntityType(context, "light")) { - percentage = 100 * getAttribute(context, "brightness") / 255; - } else if (isEntityType(context, "media_player")) { - percentage = 100 * getAttribute(context, "volume_level"); - } else if (isEntityType(context, "cover")) { - percentage = getAttribute(context, "current_position"); - } else if (isEntityType(context, "input_number")) { - const minValue = getAttribute(context, "min"); - const maxValue = getAttribute(context, "max"); - const value = getState(context); - percentage = 100 * (value - minValue) / (maxValue - minValue); - } - - context.elements.rangeFill.style.transform = `translateX(${percentage}%)`; - } -} - -export function changeStatus(context) { - const state = getState(context); - - if (state === 'unavailable') { - context.card.classList.add('is-unavailable'); - } else { - context.card.classList.remove('is-unavailable'); - } - - if (isEntityType(context, "light")) { - context.card.classList.add('is-light'); - } else { - context.card.classList.remove('is-light'); - } - - if (isStateOn(context)) { - context.card.classList.add('is-on'); - } else { - context.card.classList.remove('is-on'); - } -} - -export function changeStyle(context) { - initializesubButtonIcon(context); - setLayout(context); - - const state = getState(context); - - const customStyle = context.config.styles - ? Function('hass', 'entityId', 'state', 'icon', 'subButtonIcon', 'getWeatherIcon', `return \`${context.config.styles}\`;`) - (context._hass, context.config.entity, state, context.elements.icon.icon, context.subButtonIcon, getWeatherIcon) - : ''; - - context.elements.customStyle.innerText = customStyle; -} diff --git a/src/cards/button/create.ts b/src/cards/button/create.ts deleted file mode 100644 index b946d3f0..00000000 --- a/src/cards/button/create.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { addActions, addFeedback } from "../../tools/tap-actions.ts"; -import { createElement, toggleEntity } from "../../tools/utils.ts"; -import { getButtonType, onSliderChange } from "./helpers.ts"; -import styles from "./styles.ts"; - -export function createStructure(context, container = context.content, appendTo = container) { - const buttonType = getButtonType(context); - - context.dragging = false; - - if (!context.elements) context.elements = {}; - - context.elements.buttonCardContainer = createElement('div', 'bubble-button-card-container button-container'); - context.elements.buttonCard = createElement('div', 'bubble-button-card switch-button'); - context.elements.buttonBackground = createElement('div', 'bubble-button-background'); - context.elements.nameContainer = createElement('div', 'bubble-name-container name-container'); - context.elements.iconContainer = createElement('div', 'bubble-icon-container icon-container'); - context.elements.name = createElement('div', 'bubble-name name'); - context.elements.state = createElement('div', 'bubble-state state'); - context.elements.feedback = createElement('div', 'bubble-feedback-element feedback-element'); - context.elements.icon = createElement('ha-icon', 'bubble-icon icon'); - context.elements.image = createElement('div', 'bubble-entity-picture entity-picture'); - context.elements.style = createElement('style'); - context.elements.customStyle = createElement('style'); - - context.elements.feedback.style.display = 'none'; - context.elements.style.innerText = styles; - - context.elements.iconContainer.appendChild(context.elements.icon); - context.elements.iconContainer.appendChild(context.elements.image) - - context.elements.nameContainer.appendChild(context.elements.name); - if (buttonType !== "name") { - context.elements.nameContainer.appendChild(context.elements.state); - } - - context.elements.buttonCard.appendChild(context.elements.buttonBackground); - context.elements.buttonCard.appendChild(context.elements.iconContainer); - context.elements.buttonCard.appendChild(context.elements.nameContainer); - context.elements.buttonCard.appendChild(context.elements.feedback); - context.elements.buttonCardContainer.appendChild(context.elements.buttonCard); - - container.innerHTML = ''; - - if (appendTo === container) { - container.appendChild(context.elements.buttonCardContainer); - } - - container.appendChild(context.elements.style); - container.appendChild(context.elements.customStyle); - - if (appendTo !== container) { - appendTo.innerHTML = ''; - context.elements.buttonCardContainer.appendChild(container); - appendTo.appendChild(context.elements.buttonCardContainer); - context.buttonType = buttonType; - } else { - context.cardType = `button-${buttonType}`; - } -} -export function createSwitchStructure(context) { - addActions(context.elements.iconContainer, context.config); - - const switchDefaultActions = { - tap_action: { action: "toggle" }, - double_tap_action: { action: "toggle" }, - hold_action: { action: "more-info" } - }; - addActions(context.elements.buttonBackground, context.config.button_action, context.config.entity, switchDefaultActions); - addFeedback(context.elements.buttonBackground, context.elements.feedback); -} -export function createNameStructure(context) { - const nameDefaultActions = { - tap_action: { action: "none" }, - double_tap_action: { action: "none" }, - hold_action: { action: "none" } - }; - - context.elements.buttonCard.style.cursor = 'auto'; - - addActions(context.elements.iconContainer, context.config, context.config.entity, nameDefaultActions); - addFeedback(context.elements.buttonBackground, context.elements.feedback); -} -export function createStateStructure(context) { - addActions(context.elements.buttonCardContainer, context.config); - addFeedback(context.elements.buttonBackground, context.elements.feedback); -} -export function createSliderStructure(context) { - addActions(context.elements.iconContainer, context.config); - - let initialX = 0; - - context.elements.rangeFill = createElement('div', 'bubble-range-fill range-fill'); - context.elements.rangeSlider = createElement('div', 'bubble-range-slider range-slider'); - context.elements.rangeSlider.appendChild(context.elements.rangeFill); - context.elements.buttonCardContainer.appendChild(context.elements.rangeSlider); - - context.elements.buttonCardContainer.addEventListener('pointercancel', onPointerCancel); - context.elements.buttonCardContainer.addEventListener('pointerdown', (e) => { - context.elements.buttonCardContainer.setPointerCapture(e.pointerId); - - if (context.card.classList.contains('is-unavailable')) { - return; - } - - context.dragging = true; - initialX = e.pageX || (e.touches ? e.touches[0].pageX : 0); - - context.elements.buttonCardContainer.classList.add('is-dragging'); - context.elements.buttonCardContainer.addEventListener('pointermove', onPointerMove); - window.addEventListener('pointerup', onPointerUp); - }); - - function onPointerCancel() { - context.dragging = false; - - context.elements.buttonCardContainer.classList.remove('is-dragging'); - context.elements.buttonCardContainer.removeEventListener('pointermove', onPointerMove); - window.removeEventListener('pointerup', onPointerUp); - } - - function onPointerMove(e) { - e.stopPropagation(); - - const moveX = e.pageX || (e.touches ? e.touches[0].pageX : 0); - if (Math.abs(initialX-moveX) > 10) { - onSliderChange(context, moveX, true); - } - - if (!context.dragging) { - onSliderChange(context, moveX); - } - } - function onPointerUp(e) { - e.stopPropagation(); - - context.dragging = false; - - const moveX = e.pageX || (e.touches ? e.touches[0].pageX : 0); - onSliderChange(context, moveX); - - context.elements.buttonCardContainer.classList.remove('is-dragging'); - context.elements.buttonCardContainer.removeEventListener('pointermove', onPointerMove); - window.removeEventListener('pointerup', onPointerUp); - } -} - diff --git a/src/cards/button/helpers.ts b/src/cards/button/helpers.ts deleted file mode 100644 index 814a5b1a..00000000 --- a/src/cards/button/helpers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { throttle, isEntityType, getAttribute } from "../../tools/utils.ts"; - -export function getButtonType(context) { - let buttonType = context.config.button_type; - - if (buttonType === 'custom') { - console.error('Buttons "custom" have been removed. Use either "switch", "slider", "state" or "name"'); - buttonType = ''; - } - - if (context.config.entity) { - return buttonType || 'switch'; - } else { - return buttonType || 'name'; - } -} - -export function updateEntity(context, value) { - if (isEntityType(context, "light")) { - context._hass.callService('light', 'turn_on', { - entity_id: context.config.entity, - brightness: 255 * value / 100 - }); - } else if (isEntityType(context, "media_player")) { - context._hass.callService('media_player', 'volume_set', { - entity_id: context.config.entity, - volume_level: value / 100 - }); - } else if (isEntityType(context, "cover")) { - context._hass.callService('cover', 'set_cover_position', { - entity_id: context.config.entity, - position: value - }); - } else if (isEntityType(context, "input_number")) { - const minValue = getAttribute(context, "min"); - const maxValue = getAttribute(context, "max"); - context._hass.callService('input_number', 'set_value', { - entity_id: context.config.entity, - value: Math.round((maxValue - minValue) * value / 100 + minValue) - }); - } -}; -export const throttledUpdateEntity = throttle(updateEntity); - -export function onSliderChange(context, leftDistance, throttle = false) { - const rect = context.elements.rangeSlider.getBoundingClientRect(); - const percentage = 100 * (leftDistance - rect.left) / rect.width; - const rangedPercentage = Math.min(100, Math.max(0, percentage)); - - context.elements.rangeFill.style.transform =`translateX(${rangedPercentage}%)`; - if (throttle) { - throttledUpdateEntity(context, rangedPercentage); - } else { - updateEntity(context, rangedPercentage); - } -} diff --git a/src/cards/button/index.ts b/src/cards/button/index.ts deleted file mode 100644 index 31a7609b..00000000 --- a/src/cards/button/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { changeState, changeSubButtonState } from "../../tools/global-changes.ts"; -import { - changeStatus, - changeButton, - changeName, - changeIcon, - changeSlider, - changeStyle -} from './changes.ts' -import { getButtonType } from './helpers.ts'; -import { - createNameStructure, - createSliderStructure, - createStateStructure, - createStructure, - createSwitchStructure -} from './create.ts'; - -export function handleButton(context, container = context.content, appendTo = container) { - const buttonType = getButtonType(context); - if (context.cardType !== `button-${buttonType}` && context.buttonType !== buttonType) { - createStructure(context, container, appendTo); - - if (buttonType === 'switch') { - createSwitchStructure(context); - } else if (buttonType === 'slider') { - createSliderStructure(context); - } else if (buttonType === 'state') { - createStateStructure(context); - } else if (buttonType === 'name') { - createNameStructure(context); - } - } - - if (buttonType !== 'name') { - changeStatus(context); - changeButton(context); - changeSlider(context); - } - - changeIcon(context); - changeName(context); - changeState(context); - changeSubButtonState(context, container, appendTo.firstChild.firstChild); - changeStyle(context); -} - diff --git a/src/cards/button/styles.ts b/src/cards/button/styles.ts deleted file mode 100644 index 24c44b8c..00000000 --- a/src/cards/button/styles.ts +++ /dev/null @@ -1,193 +0,0 @@ -export default ` - * { - -webkit-tap-highlight-color: transparent !important; - } - ha-card { - margin-top: 0; - background: none; - opacity: 1; - } - .is-unavailable { - opacity: 0.5; - } - - .bubble-button-card-container { - position: relative; - width: 100%; - height: 50px; - background-color: var(--background-color-2,var(--secondary-background-color)); - border-radius: 25px; - mask-image: radial-gradient(white, black); - -webkit-transform: translateZ(0); - overflow: hidden; - touch-action: pan-y; - } - - .bubble-button-card, - .bubble-range-slider, - .bubble-button-background { - display: flex; - position: absolute; - justify-content: space-between; - align-items: center; - height: 100%; - width: 100%; - transition: background-color 1.5s; - background-color: var(--bubble-button-background-color); - } - .bubble-button-background { - opacity: .5; - } - .bubble-range-fill { - z-index: -1; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 100%; - left: -100%; - transition: all .3s; - } - .is-dragging .bubble-range-fill { - transition: none; - } - .is-light .bubble-range-fill, - .bubble-button-background { - opacity: 0.5; - } - - .bubble-button-card { - cursor: pointer; - } - .is-unavailable .bubble-button-card, - .is-unavailable .bubble-range-slider { - cursor: not-allowed; - } - .bubble-range-slider { - cursor: ew-resize; - } - .bubble-icon-container { - display: flex; - flex-wrap: wrap; - align-content: center; - justify-content: center; - min-width: 38px; - min-height: 38px; - margin: 6px; - border-radius: 50%; - background-color: var(--card-background-color, var(--ha-card-background)); - overflow: hidden; - z-index: 1; - position: relative; - cursor: pointer; - } - .bubble-icon-container::after { - content: ''; - background-color: currentColor; - position: absolute; - display: block; - width: 100%; - height: 100%; - transition: all 1s; - left: 0; - right: 0; - opacity: 0; - } - .is-light.is-on .bubble-icon-container::after { - opacity: 0.2; - } - .is-unavailable.is-light .bubble-icon-container::after { - opacity: 0; - } - - .bubble-icon { - display: flex; - opacity: 0.6; - } - - .is-on .bubble-icon { - filter: brightness(1.1); - opacity: 1; - } - - .bubble-entity-picture { - background-size: cover; - background-position: center; - height: 100%; - width: 100%; - position: absolute; - } - - .bubble-name, - .bubble-state { - display: flex; - margin: 2px 0; - position: relative; - white-space: nowrap; - } - - .bubble-name-container { - display: flex; - line-height: 14px; - flex-direction: column; - justify-content: center; - flex-grow: 1; - margin: 0 16px 0 4px; - pointer-events: none; - position: relative; - overflow: hidden; - } - - .bubble-name { - font-weight: 600; - } - - .bubble-state { - font-size: 12px; - opacity: 0.7; - font-weight: normal; - } - - .bubble-feedback-element { - position: absolute; - top: 0; - left: 0; - opacity: 0; - width: 100%; - height: 100%; - background-color: rgb(0,0,0); - } - - @keyframes tap-feedback { - 0% {transform: translateX(-100%); opacity: 0;} - 64% {transform: translateX(0); opacity: 0.1;} - 100% {transform: translateX(100%); opacity: 0;} - } - - .large .bubble-button-card-container { - height: 64px; - border-radius: 32px; - } - - .large .bubble-icon-container { - --mdc-icon-size: 28px; - min-width: 48px !important; - min-height: 48px !important; - margin-left: 8px; - } - - .rows-2 .bubble-sub-button-container { - flex-direction: column; - gap: 4px !important; - display: grid !important; - grid-template-columns: repeat(2, min-content); - grid-template-rows: repeat(2, 1fr); - grid-auto-flow: column; - width: auto; - padding-right: 14px; - } - - .rows-2 .bubble-sub-button { - height: 20px !important; - } -`; \ No newline at end of file diff --git a/src/cards/cover/changes.ts b/src/cards/cover/changes.ts deleted file mode 100644 index a5405af2..00000000 --- a/src/cards/cover/changes.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - getName, - getState, - getAttribute, - getIcon, - applyScrollingEffect, - getWeatherIcon, - setLayout -} from "../../tools/utils.ts"; -import { initializesubButtonIcon } from '../../tools/global-changes.ts'; - -export function changeIcon(context) { - const iconOpen = context.config.icon_open; - const iconClosed = context.config.icon_close; - const isOpen = getState(context) !== 'closed'; - const isCurtains = getAttribute(context, 'device_class') === 'curtain'; - - context.elements.icon.icon = isOpen ? - getIcon(context, context.config.entity, context.config.icon_open) : - getIcon(context, context.config.entity, context.config.icon_close); - context.elements.iconOpen.icon = context.config.icon_up || (isCurtains ? "mdi:arrow-expand-horizontal" : "mdi:arrow-up"); - context.elements.iconClose.icon = context.config.icon_down || (isCurtains ? "mdi:arrow-collapse-horizontal" : "mdi:arrow-down"); -} -export function changeName(context) { - const name = getName(context); - if (name !== context.elements.previousName) { - context.elements.name.innerText = name; - applyScrollingEffect(context, context.elements.name, name); - context.elements.previousName = name; - } -} -export function changeStyle(context) { - initializesubButtonIcon(context); - setLayout(context); - - const state = getState(context); - - const customStyle = context.config.styles - ? Function('hass', 'entityId', 'state', 'icon', 'subButtonIcon', 'getWeatherIcon', `return \`${context.config.styles}\`;`) - (context._hass, context.config.entity, state, context.elements.icon.icon, context.subButtonIcon, getWeatherIcon) - : ''; - - context.elements.customStyle.innerText = customStyle; -} - - - diff --git a/src/cards/cover/create.ts b/src/cards/cover/create.ts deleted file mode 100644 index 1128a0bf..00000000 --- a/src/cards/cover/create.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { addActions } from "../../tools/tap-actions.ts"; -import { createElement } from "../../tools/utils.ts"; -import styles from "./styles.ts"; - -export function createStructure(context) { - context.elements = {}; - context.elements.coverCardContainer = createElement('div', 'bubble-cover-card-container cover-container'); - context.elements.headerContainer = createElement('div', 'bubble-header header-container'); - context.elements.buttonsContainer = createElement('div', 'bubble-buttons buttons-container'); - context.elements.iconContainer = createElement('div', 'bubble-icon-container icon-container'); - context.elements.icon = createElement('ha-icon', 'bubble-icon'); - context.elements.nameContainer = createElement('div', 'bubble-name-container name-container'); - context.elements.name = createElement('div', 'bubble-name name'); - context.elements.state = createElement('div', 'bubble-state state'); - context.elements.buttonOpen = createElement('div', 'bubble-button bubble-open button open'); - context.elements.buttonStop = createElement('div', 'bubble-button bubble-stop button stop'); - context.elements.buttonClose = createElement('div', 'bubble-button bubble-close button close'); - context.elements.iconOpen = createElement('ha-icon', 'bubble-icon bubble-icon-open'); - context.elements.iconStop = createElement('ha-icon', 'bubble-icon bubble-icon-stop'); - context.elements.iconStop.icon = 'mdi:stop'; - context.elements.iconClose = createElement('ha-icon', 'bubble-icon bubble-icon-close'); - - context.elements.style = createElement('style'); - context.elements.style.innerText = styles; - context.elements.customStyle = createElement('style'); - - context.elements.iconContainer.appendChild(context.elements.icon); - context.elements.headerContainer.appendChild(context.elements.iconContainer); - context.elements.headerContainer.appendChild(context.elements.nameContainer); - context.elements.nameContainer.appendChild(context.elements.name); - context.elements.nameContainer.appendChild(context.elements.state); - context.elements.buttonsContainer.appendChild(context.elements.buttonOpen); - context.elements.buttonsContainer.appendChild(context.elements.buttonStop); - context.elements.buttonsContainer.appendChild(context.elements.buttonClose); - - context.elements.buttonOpen.appendChild(context.elements.iconOpen); - context.elements.buttonOpen.addEventListener('click', () => { - const openCover = context.config.open_service ?? 'cover.open_cover'; - const [domain, action] = openCover.split('.'); - context._hass.callService(domain, action, { - entity_id: context.config.entity - }); - }); - - context.elements.buttonStop.appendChild(context.elements.iconStop); - context.elements.buttonStop.addEventListener('click', () => { - const stopCover = context.config.stop_service ?? 'cover.stop_cover'; - const [domain, action] = stopCover.split('.'); - context._hass.callService(domain, action, { - entity_id: context.config.entity - }); - }); - - context.elements.buttonClose.appendChild(context.elements.iconClose); - context.elements.buttonClose.addEventListener('click', () => { - const closeCover = context.config.close_service ?? 'cover.close_cover'; - const [domain, action] = closeCover.split('.'); - context._hass.callService(domain, action, { - entity_id: context.config.entity - }); - }); - - addActions(context.elements.iconContainer, context.config); - - context.content.innerHTML = ''; - - context.content.appendChild(context.elements.coverCardContainer); - context.content.appendChild(context.elements.style); - context.content.appendChild(context.elements.customStyle); - - context.elements.coverCardContainer.appendChild(context.elements.headerContainer); - context.elements.coverCardContainer.appendChild(context.elements.buttonsContainer); - - context.cardType = "cover"; -} \ No newline at end of file diff --git a/src/cards/cover/index.ts b/src/cards/cover/index.ts deleted file mode 100644 index d5e67c22..00000000 --- a/src/cards/cover/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { changeState, changeSubButtonState } from "../../tools/global-changes.ts"; -import { - changeIcon, - changeName, - changeStyle, -} from './changes.ts' -import { createStructure } from './create.ts'; - -export function handleCover(context) { - if (context.cardType !== "cover") { - createStructure(context); - } - - changeIcon(context); - changeName(context); - changeState(context); - changeSubButtonState(context, context.content, context.elements.headerContainer); - changeStyle(context); -} diff --git a/src/cards/cover/styles.ts b/src/cards/cover/styles.ts deleted file mode 100644 index 61298c7a..00000000 --- a/src/cards/cover/styles.ts +++ /dev/null @@ -1,143 +0,0 @@ -export default ` - * { - -webkit-tap-highlight-color: transparent !important; - } - ha-card { - margin-top: 0 !important; - background: none !important; - } - - .bubble-cover-card-container { - display: grid; - gap: 10px; - } - - .bubble-header { - display: flex; - align-items: center; - overflow: hidden; - } - - .bubble-icon-container { - display: flex; - flex-wrap: wrap; - align-content: center; - justify-content: center; - min-width: 38px; - min-height: 38px; - border-radius: 50%; - background-color: var(--card-background-color, var(--ha-card-background)); - border: 6px solid var(--background-color-2, var(--secondary-background-color)); - cursor: pointer; - } - - .bubble-name-container { - display: flex; - line-height: 1em; - flex-direction: column; - justify-content: center; - flex-grow: 1; - font-weight: 600; - margin-left: 4px; - margin-right: 16px; - pointer-events: none; - position: relative; - overflow: hidden; - } - - .bubble-name { - margin: 2px 0; - white-space: nowrap; - display: flex; - position: relative; - } - - .bubble-state { - font-size: 12px; - opacity: 0.7; - margin: 2px 0; - font-weight: normal; - white-space: nowrap; - display: flex; - position: relative; - } - - .bubble-buttons { - display: grid; - align-self: center; - grid-auto-flow: column; - grid-gap: 18px; - } - - .bubble-icon { - display: flex; - height: 24px; - width: 24px; - color: var(--primary-text-color); - } - - .bubble-button { - display: flex; - background: var(--background-color-2, var(--secondary-background-color)); - height: 42px; - border-radius: 32px; - align-items: center; - justify-content: center; - cursor: pointer; - border: none; - } - - .large .bubble-cover-card-container { - height: 64px; - display: flex; - background: var(--background-color-2, var(--secondary-background-color)); - border-radius: 32px; - } - - .large .bubble-header-container { - height: 64px; - } - - .large .bubble-header { - width: 100%; - } - - .large .bubble-icon-container { - --mdc-icon-size: 28px; - min-width: 48px !important; - min-height: 48px !important; - margin-left: 2px; - align-content: center; - } - - .large .bubble-icon { - align-items: center; - } - - .large .bubble-buttons { - display: flex; - position: relative; - right: 18px; - align-self: center; - grid-gap: 18px; - } - - .large .bubble-sub-button-container { - margin-right: 14px; - } - - .rows-2 .bubble-sub-button-container { - flex-direction: column; - gap: 4px !important; - display: grid !important; - grid-template-columns: repeat(2, min-content); - grid-template-rows: repeat(2, 1fr); - grid-auto-flow: column; - width: auto; - padding-right: 14px; - } - - .rows-2 .bubble-sub-button { - height: 20px !important; - } -` \ No newline at end of file diff --git a/src/cards/empty-column/create.ts b/src/cards/empty-column/create.ts deleted file mode 100644 index 16d9b932..00000000 --- a/src/cards/empty-column/create.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createElement } from "../../tools/utils.ts"; -import styles from "./styles.ts"; - -export function createStructure(context) { - context.elements = {}; - context.elements.emptyColumnCard = createElement('div', 'bubble-empty-column empty-column'); - - context.elements.style = createElement('style'); - context.elements.style.innerText = styles; - context.elements.customStyle = createElement('style'); - - context.content.innerHTML = ''; - context.content.appendChild(context.elements.emptyColumnCard); - context.content.appendChild(context.elements.style); - context.content.appendChild(context.elements.customStyle); - - context.cardType = "empty-column"; -} diff --git a/src/cards/empty-column/index.ts b/src/cards/empty-column/index.ts deleted file mode 100644 index 46be1328..00000000 --- a/src/cards/empty-column/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createStructure } from './create.ts'; - -export function handleEmptyColumn(context) { - if (context.cardType !== "empty-column") { - createStructure(context); - } -} diff --git a/src/cards/empty-column/styles.ts b/src/cards/empty-column/styles.ts deleted file mode 100644 index 08bc488e..00000000 --- a/src/cards/empty-column/styles.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default ` - .empty-column { - display: flex; - width: 100%; - } -`; \ No newline at end of file diff --git a/src/cards/horizontal-buttons-stack/changes.ts b/src/cards/horizontal-buttons-stack/changes.ts deleted file mode 100644 index 8ef71de1..00000000 --- a/src/cards/horizontal-buttons-stack/changes.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { isColorCloseToWhite } from "../../tools/style.ts"; -import { getState } from "../../tools/utils.ts"; -import { createButton } from './create.ts'; -import { initializesubButtonIcon } from '../../tools/global-changes.ts'; - -const BUTTON_MARGIN = 12; - -export function sortButtons(context) { - if (!context.config.auto_order) return; - - const states = context._hass.states; - - context.elements.buttons.sort((a, b) => { - if (!states[a.pirSensor]) return 1; - if (!states[a.pirSensor]) return -1; - - const aTime = states[a.pirSensor]?.last_updated; - const bTime = states[b.pirSensor]?.last_updated; - - if (states[a.pirSensor]?.state === "on" && states[b.pirSensor]?.state === "on") { - return aTime > bTime ? -1 : aTime === bTime ? 0 : 1; - } - - // If only a.pirSensor is "on", place a before b - if (states[a.pirSensor]?.state === "on") return -1; - - // If only b.pirSensor is "on", place b before a - if (states[b.pirSensor]?.state === "on") return 1; - - // If neither PIR sensor is "on", arrangement based only on the state of last updated even if off - return aTime > bTime ? -1 : aTime === bTime ? 0 : 1; - }); -} -export function placeButtons(context) { - let position = 0; - for (let i = 0; i < context.elements.buttons.length; ++i) { - let buttonWidth = localStorage.getItem(`bubbleButtonWidth-${context.elements.buttons[i].link}`); - - context.elements.buttons[i].style.width = ''; - const newWidth = context.elements.buttons[i].offsetWidth; - context.elements.buttons[i].style.width = `${newWidth}px`; - - if (newWidth > 0) { - buttonWidth = newWidth; - localStorage.setItem(`bubbleButtonWidth-${context.elements.buttons[i].link}`, `${newWidth}`); - } - - if (buttonWidth !== null) { - context.elements.buttons[i].style.transform = `translateX(${position}px)`; - context.elements.buttons[i].style.width = ''; - position += +buttonWidth + BUTTON_MARGIN; - } - } - context.elements.cardContainer.style.width = `${position - BUTTON_MARGIN}px`; -} -export function changeEditor(context) { - const detectedEditor = context.shadowRoot.host.closest('hui-card-preview, hui-card-options'); - - if (context.editor || detectedEditor !== null) { - context.elements.cardContainer.classList.add('editor'); - context.card.classList.add('editor'); - } else { - context.elements.cardContainer.classList.remove('editor'); - context.card.classList.remove('editor'); - } -} -export function changeLight(context) { - context.elements.buttons.forEach((button) => { - const entityData = context._hass.states[button.lightEntity]; - const rgbColor = entityData?.attributes.rgb_color; - const state = entityData?.state; - - if (rgbColor) { - const rgbColorOpacity = (isColorCloseToWhite(rgbColor) ? 'rgba(255, 220, 200, 0.5)' : `rgba(${rgbColor}, 0.5)`); - button.backgroundColor.style.backgroundColor = rgbColorOpacity; - button.backgroundColor.style.borderColor = 'rgba(0, 0, 0, 0)'; - } else if (state == 'on') { - button.backgroundColor.style.backgroundColor = 'rgba(255, 255, 255, 0.5)'; - button.backgroundColor.style.borderColor = 'rgba(0, 0, 0, 0)'; - } else { - button.backgroundColor.style.backgroundColor = 'rgba(0, 0, 0, 0)'; - button.backgroundColor.style.borderColor = 'var(--primary-text-color)'; - } - }); -} -export function changeConfig(context) { - context.elements.buttons.forEach((button) => { - const index = button.index; - const name = context.config[`${index}_name`] ?? ''; - const icon = context.config[`${index}_icon`] ?? ''; - const sensor = context.config[`${index}_pir_sensor`]; - const link = context.config[`${index}_link`]; - const entity = context.config[`${index}_entity`]; - - button.pirSensor = sensor; - button.lightEntity = entity; - button.link = link; - - if (name) { - button.name.innerText = name; - button.name.style.display = ''; - } else { - button.name.style.display = 'none'; - } - if (icon) { - button.icon.icon = icon; - button.icon.style.display = ''; - } else { - button.icon.style.display = 'none'; - } - - if (link === undefined) { - button.remove(); - context.elements.buttons = context.elements.buttons.filter((btn) => btn !== button); - context.elements.buttons.forEach((btn, idx) => { - btn.index = idx + 1; - }); - } - }); - - // Create a new button if necessary - let index = context.elements.buttons.length + 1; - while (context.config[`${index}_link`] !== undefined) { - const existingButton = context.elements.buttons.find(button => button.index === index); - if (!existingButton) { - const newButton = createButton(context, index); - context.elements.buttons.push(newButton); - } - index++; - } -} -export function changeStatus(context) { - if (context.content.scrollWidth > context.content.offsetWidth) { - context.content.classList.add('is-scrollable'); - } else { - context.content.classList.remove('is-scrollable'); - } -} -export function changeStyle(context) { - const state = getState(context); - - const customStyle = context.config.styles - ? Function('hass', 'entityId', 'state', 'return `' + context.config.styles + '`;')(context._hass, context.config.entity, state) - : ''; - - context.elements.customStyle.innerText = customStyle; -} \ No newline at end of file diff --git a/src/cards/horizontal-buttons-stack/create.ts b/src/cards/horizontal-buttons-stack/create.ts deleted file mode 100644 index 031f1bf8..00000000 --- a/src/cards/horizontal-buttons-stack/create.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { createElement, forwardHaptic } from "../../tools/utils.ts"; -import { addHash, removeHash } from "../pop-up/helpers.ts"; -import styles from "./styles.ts"; - -let isOpen = false; - -export function createButton(context, index) { - const name = context.config[`${index}_name`] ?? ''; - const icon = context.config[`${index}_icon`] ?? ''; - const sensor = context.config[`${index}_pir_sensor`]; - const link = context.config[`${index}_link`]; - const entity = context.config[`${index}_entity`]; - isOpen = isOpen || location.hash === link; - - const iconElement = createElement('ha-icon', 'bubble-icon icon'); - iconElement.icon = icon; - const nameElement = createElement('div', 'bubble-name name'); - nameElement.innerText = name; - const backgroundColorElement = createElement('div', 'bubble-background-color background-color'); - const backgroundElement = createElement('div', 'bubble-background background'); - const button = createElement('div', `bubble-button bubble-button-${index} button ${link.substring(1)}`); - let buttonWidth = localStorage.getItem(`bubbleButtonWidth-${link}`); - button.style.width = `${buttonWidth}px`; - - button.appendChild(iconElement); - button.appendChild(nameElement); - button.appendChild(backgroundColorElement); - button.appendChild(backgroundElement); - button.addEventListener('click', () => { - if (location.hash !== link) { - isOpen = false; - } - - if (isOpen) { - removeHash() - } else { - addHash(link); - } - isOpen = !isOpen; - - forwardHaptic("light"); - }); - - button.icon = iconElement; - button.name = nameElement; - button.backgroundColor = backgroundColorElement; - button.background = backgroundElement; - button.pirSensor = sensor; - button.lightEntity = entity; - button.link = link; - button.index = index; - - function handleUrlChange() { - if (!context.config.highlight_current_view) return; - - const isShown = location.pathname === link || location.hash === link; - if (isShown) { - button.classList.add("highlight"); - } else { - button.classList.remove("highlight"); - } - } - - window.addEventListener('urlChanged', handleUrlChange); - - context.elements.buttons.push(button); - - return button; -} - -export function createStructure(context) { - context.elements = {}; - context.elements.buttons = []; - context.elements.cardContainer = createElement('div', 'bubble-horizontal-buttons-stack-card-container horizontal-buttons-stack-container'); - - let index = 1; - while (context.config[index + '_link']) { - context.elements.cardContainer.appendChild(createButton(context, index)) - index++; - } - - context.elements.style = createElement('style'); - context.elements.style.innerText = styles; - context.elements.customStyle = createElement('style'); - - context.card.classList.add('horizontal-buttons-stack-card'); - context.card.style.marginLeft = context.config.margin ?? ''; - if (!context.config.hide_gradient) { - context.card.classList.add('has-gradient'); - } - context.card.style.setProperty('--desktop-width', context.config.width_desktop ?? '500px'); - context.elements.cardContainer.appendChild(context.elements.style); - context.elements.cardContainer.appendChild(context.elements.customStyle); - context.content.appendChild(context.elements.cardContainer); - - context.content.addEventListener('scroll', () => { - if (context.content.scrollLeft > 0) { - context.content.classList.add('is-scrolled'); - } else { - context.content.classList.remove('is-scrolled'); - } - - if (context.content.scrollWidth === context.content.offsetWidth + context.content.scrollLeft) { - context.content.classList.add('is-maxed-scroll'); - } else { - context.content.classList.remove('is-maxed-scroll'); - } - }); - - const riseAnimation = context.config.rise_animation ?? true; - - if (riseAnimation) { - context.content.style.animation = 'from-bottom .6s forwards'; - setTimeout(() => { - context.content.style.animation = 'none'; - }, 1500); - } - - // Fix for the last cards that are hidden by the HBS - let parentElement = context.card.parentNode.host; - if (parentElement && !context.editor) { - parentElement.style.padding = '0 0 80px'; - } - - context.cardType = "horizontal-buttons-stack"; -} \ No newline at end of file diff --git a/src/cards/horizontal-buttons-stack/index.ts b/src/cards/horizontal-buttons-stack/index.ts deleted file mode 100644 index 6138162f..00000000 --- a/src/cards/horizontal-buttons-stack/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { changeConfig, changeEditor, changeLight, changeStatus, changeStyle, placeButtons, sortButtons } from './changes.ts'; -import { createStructure } from './create.ts'; -import { configChanged } from "../../tools/utils.ts"; - -export function handleHorizontalButtonsStack(context) { - if (context.cardType !== "horizontal-buttons-stack") { - createStructure(context); - } - - changeStyle(context); - sortButtons(context); - changeConfig(context); - changeEditor(context); - placeButtons(context); - changeLight(context); - changeStatus(context); -} \ No newline at end of file diff --git a/src/cards/horizontal-buttons-stack/styles.ts b/src/cards/horizontal-buttons-stack/styles.ts deleted file mode 100644 index 34ab18f1..00000000 --- a/src/cards/horizontal-buttons-stack/styles.ts +++ /dev/null @@ -1,163 +0,0 @@ -export default ` - @keyframes from-bottom { - 0% { transform: translate(-50%, 100px); } - 26% { transform: translate(-50%, -8px); } - 46% { transform: translate(-50%, 1px); } - 62% { transform: translate(-50%, -2px); } - 70% { transform: translate(-50%, 0); } - 100% { transform: translate(-50%, 0); } - } - @keyframes pulse { - 0% { filter: brightness(0.7); } - 100% { filter: brightness(1.3); } - } - ha-card { - border-radius: 0; - } - .horizontal-buttons-stack-card { - bottom: 16px; - height: 51px; - margin-top: 0; - position: fixed; - width: calc(100% - var(--mdc-drawer-width, 0px) - 8px); - left: calc(var(--mdc-drawer-width, 0px) + 4px); - z-index: 6; /* Higher value hide the more-info panel */ - } - @media only screen and (max-width: 870px) { - .horizontal-buttons-stack-card { - width: calc(100% - 16px); - left: 8px; - } - - .horizontal-buttons-stack-card::before { - left: -10px; - } - } - .horizontal-buttons-stack-card::before { - content: ''; - position: absolute; - top: -32px; - display: none; - background: linear-gradient(0deg, var(--background-color, var(--primary-background-color)) 50%, rgba(79, 69, 87, 0)); - width: 200%; - height: 100px; - pointer-events: none; - } - .has-gradient.horizontal-buttons-stack-card::before { - display: block; - } - - .card-content { - width: calc(100% + 36px); - padding: 0 !important; - max-width: calc(var(--desktop-width) - 8px); - box-sizing: border-box; - overflow: scroll; - position: absolute; - left: 50%; - transform: translateX(-50%); - -ms-overflow-style: none; - scrollbar-width: none; - -webkit-mask-image: linear-gradient( - 90deg, - #000000 0%, - #000000 calc(0% + 28px), - #000000 calc(100% - 28px), - transparent 100% - ); - } - .is-scrollable.card-content { - padding: 0 !important; - width: 100%; - } - .is-scrolled.card-content { - padding: 0 !important; - width: 100%; - -webkit-mask-image: linear-gradient( - 90deg, - transparent 0%, - #000000 calc(0% + 28px), - #000000 calc(100% - 28px), - transparent 100% - ); - } - .is-maxed-scroll.card-content { - -webkit-mask-image: linear-gradient( - 90deg, - transparent 0%, - #000000 calc(0% + 28px), - #000000 calc(100% - 28px), - #000000 100% - ); - } - .card-content::-webkit-scrollbar { - display: none; - } - - .bubble-horizontal-buttons-stack-card-container { - height: 51px; - position: relative; - margin: auto; - } - - .bubble-button { - align-items: center; - border-radius: 25px; - color: var(--primary-text-color); - cursor: pointer; - display: inline-flex; - height: 50px; - left: 0; - padding: 0 16px; - position: absolute; - white-space: nowrap; - z-index: 1; - transition: transform 1s; - box-sizing: border-box; - } - .bubble-button.highlight { - animation: pulse 1.4s infinite alternate; - } - .bubble-background-color { - border: 1px solid var(--primary-text-color); - border-radius: 24px; - box-sizing: border-box; - height: 100%; - left: 0; - position: absolute; - top: 0; - transition: background-color 1s; - width: 100%; - z-index: -1; - } - .bubble-background { - opacity: 0.8; - border-radius: 24px; - width: 100%; - height: 100%; - box-sizing: border-box !important; - position: absolute; - left: 0; - z-index: -2; - background-color: var(--background-color,var(--primary-background-color)); - } - .bubble-icon { - height: 24px; - width: 24px; - } - .bubble-icon + .bubble-name { - margin-left: 8px; - } - - - .horizontal-buttons-stack-card.editor { - position: relative; - width: 100%; - left: 0; - bottom: 0; - } - .horizontal-buttons-stack-card.editor::before { - background: none; - } - -`; \ No newline at end of file diff --git a/src/cards/media-player/changes.ts b/src/cards/media-player/changes.ts deleted file mode 100644 index e8853e5c..00000000 --- a/src/cards/media-player/changes.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { initializesubButtonIcon } from '../../tools/global-changes.ts'; -import { - applyScrollingEffect, - getBrightness, - getIcon, - getIconColor, - getImage, - getName, - getState, - getAttribute, - isEntityType, - isStateOn, - getWeatherIcon, - setLayout -} from '../../tools/utils.ts'; - -export function changeIcon(context) { - const isOn = isStateOn(context); - const icon = getIcon(context); - const image = getImage(context); - - if (image !== '') { - context.elements.image.style.backgroundImage = 'url(' + image + ')'; - context.elements.icon.style.display = 'none'; - context.elements.image.style.display = ''; - } else if (icon !== '') { - context.elements.icon.icon = icon; - context.elements.icon.style.color = isOn ? 'var(--accent-color)' : 'inherit'; - context.elements.icon.style.display = ''; - context.elements.image.style.display = 'none'; - } else { - context.elements.icon.style.display = 'none'; - context.elements.image.style.display = 'none'; - } -} - -export function changeName(context) { - const name = getName(context); - - if (name !== context.previousName) { - context.elements.name.innerText = name; - context.previousName = name; - applyScrollingEffect(context, context.elements.name, name); - } -} - -export function changeMediaInfo(context) { - const title = getAttribute(context, "media_title"); - const artist = getAttribute(context, "media_artist"); - const state = title + artist; - - if (state !== context.previousState) { - if (artist === '') { - context.elements.artist.style.display = 'none'; - } else { - context.elements.artist.style.display = 'flex'; - } - - applyScrollingEffect(context, context.elements.title, title); - applyScrollingEffect(context, context.elements.artist, artist); - context.previousState = state; - } -} - -export function changeDisplayedInfo(context) { - const title = getAttribute(context, "media_title"); - const artist = getAttribute(context, "media_artist"); - const noMediaInfo = (title, artist) === ''; - - context.elements.mediaInfoContainer.style.display = noMediaInfo ? 'none' : ''; - context.elements.nameContainer.style.display = noMediaInfo ? '' : 'none'; -} - -export function changeSlider(context) { - if (isEntityType(context, "media_player") && context.dragging === false && context.elements.rangeFill) { - const percentage = 100 * getAttribute(context, "volume_level"); - context.elements.rangeFill.style.transform =`translateX(${percentage}%)`; - } -} - -export function changeStatus(context) { - const state = getState(context); - - if (state === 'unavailable') { - context.card.classList.add('is-unavailable'); - } else { - context.card.classList.remove('is-unavailable'); - } - - if (isStateOn(context)) { - context.card.classList.add('is-on'); - } else { - context.card.classList.remove('is-on'); - } -} - -export function changePlayPauseIcon(context) { - const isPlaying = getState(context) === 'playing'; - const clicked = context.elements.playPauseButton.clicked; - - if (isPlaying) { - context.elements.playPauseButton.setAttribute("icon", clicked ? "mdi:play" : "mdi:pause"); - } else { - context.elements.playPauseButton.setAttribute("icon", clicked ? "mdi:pause" : "mdi:play"); - } - - context.elements.playPauseButton.clicked = false; -} - -export function changePowerIcon(context) { - const isOn = isStateOn(context); - - if (!isOn) { - context.elements.powerButton.style.color = ""; - } else { - context.elements.powerButton.style.color = "var(--accent-color)"; - } -} - -export function changeVolumeIcon(context) { - if (context.elements.volumeButton.isHidden) { - context.elements.volumeButton.setAttribute("icon", "mdi:volume-high"); - context.elements.volumeButton.isHidden = false; - } else { - context.elements.volumeButton.setAttribute("icon", "mdi:close"); - context.elements.volumeButton.isHidden = true; - } -} - -export function changeMuteIcon(context) { - const isVolumeMuted = getAttribute(context, "is_volume_muted") === true; - const clicked = context.elements.muteButton.clicked; - - if (isVolumeMuted) { - context.elements.muteButton.style.color = clicked ? "" : "var(--accent-color)"; - } else { - context.elements.muteButton.style.color = clicked ? "var(--accent-color)" : ""; - } - - context.elements.muteButton.clicked = false; -} - -export function changeStyle(context) { - initializesubButtonIcon(context); - setLayout(context); - - const state = getState(context); - - const customStyle = context.config.styles - ? Function('hass', 'entityId', 'state', 'icon', 'subButtonIcon', 'getWeatherIcon', `return \`${context.config.styles}\`;`) - (context._hass, context.config.entity, state, context.elements.icon.icon, context.subButtonIcon, getWeatherIcon) - : ''; - - context.elements.customStyle.innerText = customStyle; -} \ No newline at end of file diff --git a/src/cards/media-player/create.ts b/src/cards/media-player/create.ts deleted file mode 100644 index 58bb749a..00000000 --- a/src/cards/media-player/create.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { addActions, addFeedback } from "../../tools/tap-actions.ts"; -import { createElement, toggleEntity, getAttribute, isStateOn } from "../../tools/utils.ts"; -import { onSliderChange } from "./helpers.ts"; -import { changeVolumeIcon } from "./changes.ts"; -import styles from "./styles.ts"; - -export function createStructure(context) { - context.dragging = false; - - context.elements = {}; - context.elements.mediaPlayerContainer = createElement('div', 'bubble-media-player-container'); - context.elements.mediaPlayerCard = createElement('div', 'bubble-media-player'); - context.elements.mediaInfoContainer = createElement('div', 'bubble-media-info-container'); - context.elements.nameContainer = createElement('div', 'bubble-name-container'); - context.elements.buttonContainer = createElement('div', 'bubble-button-container'); - context.elements.iconContainer = createElement('div', 'bubble-icon-container'); - context.elements.playPauseButton = createElement('ha-icon', 'bubble-play-pause-button'); - context.elements.previousButton = createElement('ha-icon', 'bubble-previous-button'); - context.elements.previousButton.setAttribute("icon", "mdi:skip-previous"); - context.elements.nextButton = createElement('ha-icon', 'bubble-next-button'); - context.elements.nextButton.setAttribute("icon", "mdi:skip-next"); - context.elements.volumeButton = createElement('ha-icon', 'bubble-volume-button'); - context.elements.volumeButton.setAttribute("icon", "mdi:volume-high"); - context.elements.powerButton = createElement('ha-icon', 'bubble-power-button'); - context.elements.powerButton.setAttribute("icon", "mdi:power-standby"); - context.elements.muteButton = createElement('ha-icon', 'bubble-mute-button is-hidden'); - context.elements.muteButton.setAttribute("icon", "mdi:volume-off"); - context.elements.title = createElement('div', 'bubble-title'); - context.elements.artist = createElement('div', 'bubble-artist'); - context.elements.name = createElement('div', 'bubble-name'); - context.elements.state = createElement('div', 'bubble-state'); - context.elements.icon = createElement('ha-icon', 'bubble-icon'); - context.elements.image = createElement('div', 'bubble-entity-picture'); - context.elements.style = createElement('style'); - context.elements.customStyle = createElement('style'); - - context.elements.style.innerText = styles; - - context.elements.iconContainer.appendChild(context.elements.icon); - context.elements.iconContainer.appendChild(context.elements.image); - context.elements.iconContainer.appendChild(context.elements.muteButton); - - context.elements.nameContainer.appendChild(context.elements.name); - context.elements.nameContainer.appendChild(context.elements.state); - - context.elements.mediaInfoContainer.appendChild(context.elements.title); - context.elements.mediaInfoContainer.appendChild(context.elements.artist); - - if (!context.config.hide?.power_button) context.elements.buttonContainer.appendChild(context.elements.powerButton); - if (!context.config.hide?.previous_button) context.elements.buttonContainer.appendChild(context.elements.previousButton); - if (!context.config.hide?.next_button) context.elements.buttonContainer.appendChild(context.elements.nextButton); - if (!context.config.hide?.volume_button) context.elements.buttonContainer.appendChild(context.elements.volumeButton); - if (!context.config.hide?.play_pause_button) context.elements.buttonContainer.appendChild(context.elements.playPauseButton); - - context.elements.mediaPlayerCard.appendChild(context.elements.iconContainer); - context.elements.mediaPlayerCard.appendChild(context.elements.mediaInfoContainer); - context.elements.mediaPlayerCard.appendChild(context.elements.nameContainer); - context.elements.mediaPlayerCard.appendChild(context.elements.buttonContainer); - - context.content.innerHTML = ''; - - context.content.appendChild(context.elements.mediaPlayerContainer); - context.content.appendChild(context.elements.style); - context.content.appendChild(context.elements.customStyle); - - context.elements.mediaPlayerContainer.appendChild(context.elements.mediaPlayerCard); - - addActions(context.elements.icon, context.config); - addActions(context.elements.image, context.config); - - // Volume slider - - context.elements.volumeSliderContainer = createElement('div', 'bubble-volume-slider is-hidden'); - createSlider(context, context.elements.volumeSliderContainer); - context.elements.mediaPlayerCard.appendChild(context.elements.volumeSliderContainer); - - context.elements.volumeButton.addEventListener('click', () => { - context.elements.volumeSliderContainer.classList.toggle('is-hidden'); - context.elements.muteButton.classList.toggle('is-hidden'); - context.elements.icon.classList.toggle('is-hidden'); - context.elements.image.classList.toggle('is-hidden'); - changeVolumeIcon(context); - }); - - // Power button event - - context.elements.powerButton.addEventListener('click', () => { - const isOn = isStateOn(context); - - context._hass.callService('media_player', isOn ? 'turn_off' : 'turn_on', { - entity_id: context.config.entity - }); - }); - - // Mute button event - - context.elements.muteButton.addEventListener('click', () => { - const isVolumeMuted = getAttribute(context, "is_volume_muted") === true; - - context._hass.callService('media_player', 'volume_mute', { - entity_id: context.config.entity, - is_volume_muted: isVolumeMuted ? false : true - }); - - context.elements.muteButton.clicked = true; - }); - - // Previous button event - - context.elements.previousButton.addEventListener('click', () => { - context._hass.callService('media_player', 'media_previous_track', { - entity_id: context.config.entity - }); - }); - - // Next button event - - context.elements.nextButton.addEventListener('click', () => { - context._hass.callService('media_player', 'media_next_track', { - entity_id: context.config.entity - }); - }); - - // Play button event - - context.elements.playPauseButton.addEventListener('click', () => { - context._hass.callService('media_player', 'media_play_pause', { - entity_id: context.config.entity - }); - - context.elements.playPauseButton.clicked = true; - }); - - context.cardType = `media-player`; -} - -function createSlider(context, sliderContainer) { - let initialX = 0; - let volumeLevel = Math.round(getAttribute(context, 'volume_level') * 100) + '%'; - - context.elements.rangeFill = createElement('div', 'bubble-range-fill range-fill'); - context.elements.rangeSlider = createElement('div', 'bubble-range-slider range-slider'); - context.elements.rangeValue = createElement('div', 'bubble-range-value'); - context.elements.rangeSlider.appendChild(context.elements.rangeValue); - context.elements.rangeSlider.appendChild(context.elements.rangeFill); - sliderContainer.appendChild(context.elements.rangeSlider); - - sliderContainer.addEventListener('pointercancel', onPointerCancel); - sliderContainer.addEventListener('pointerdown', (e) => { - sliderContainer.setPointerCapture(e.pointerId); - - if (context.card.classList.contains('is-unavailable')) { - return; - } - - context.dragging = true; - initialX = e.pageX || (e.touches ? e.touches[0].pageX : 0); - - sliderContainer.classList.add('is-dragging'); - sliderContainer.addEventListener('pointermove', onPointerMove); - sliderContainer.addEventListener('pointerup', onPointerUp); - }); - - function onPointerCancel() { - context.dragging = false; - - sliderContainer.classList.remove('is-dragging'); - sliderContainer.removeEventListener('pointermove', onPointerMove); - sliderContainer.removeEventListener('pointerup', onPointerUp); - } - - function onPointerMove(e) { - e.stopPropagation(); - - const moveX = e.pageX || (e.touches ? e.touches[0].pageX : 0); - if (Math.abs(initialX-moveX) > 10) { - onSliderChange(context, moveX, true); - } - } - - function onPointerUp(e) { - e.stopPropagation(); - - context.dragging = false; - - const moveX = e.pageX || (e.touches ? e.touches[0].pageX : 0); - onSliderChange(context, moveX); - - sliderContainer.classList.remove('is-dragging'); - sliderContainer.removeEventListener('pointermove', onPointerMove); - sliderContainer.removeEventListener('pointerup', onPointerUp); - } - - context.elements.rangeValue.innerText = volumeLevel; -} diff --git a/src/cards/media-player/helpers.ts b/src/cards/media-player/helpers.ts deleted file mode 100644 index 3750f136..00000000 --- a/src/cards/media-player/helpers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - throttle, - getAttribute, - isEntityType -} from "../../tools/utils.ts"; - -export function updateEntity(context, value) { - if (isEntityType(context, "media_player")) { - context._hass.callService('media_player', 'volume_set', { - entity_id: context.config.entity, - volume_level: value / 100 - }); - } -}; - -export const throttledUpdateEntity = throttle(updateEntity); - -export function onSliderChange(context, leftDistance, throttle = false) { - const rect = context.elements.rangeSlider.getBoundingClientRect(); - const percentage = 100 * (leftDistance - rect.left) / rect.width; - const rangedPercentage = Math.round(Math.min(100, Math.max(0, percentage))); - - context.elements.rangeFill.style.transform =`translateX(${rangedPercentage}%)`; - if (throttle) { - throttledUpdateEntity(context, rangedPercentage); - } else { - updateEntity(context, rangedPercentage); - } - - context.elements.rangeValue.innerText = rangedPercentage + '%'; -} diff --git a/src/cards/media-player/index.ts b/src/cards/media-player/index.ts deleted file mode 100644 index e942edc0..00000000 --- a/src/cards/media-player/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { changeState, changeSubButtonState } from "../../tools/global-changes.ts"; -import { - changeStatus, - changeName, - changeMediaInfo, - changeDisplayedInfo, - changeIcon, - changeSlider, - changePlayPauseIcon, - changeMuteIcon, - changePowerIcon, - changeStyle -} from './changes.ts'; -import { - createSmallStructure, - createLargeStructure, - createStructure -} from './create.ts'; - -export function handleMediaPlayer(context) { - if (context.cardType !== `media-player`) { - createStructure(context); - } - - changeStatus(context); - changeName(context); - changeMediaInfo(context); - changeDisplayedInfo(context); - changeIcon(context); - changeState(context); - changeSlider(context); - changePlayPauseIcon(context); - changeMuteIcon(context); - changePowerIcon(context); - changeSubButtonState(context, context.content, context.elements.buttonContainer, true); - changeStyle(context); -} diff --git a/src/cards/media-player/styles.ts b/src/cards/media-player/styles.ts deleted file mode 100644 index 99f95a88..00000000 --- a/src/cards/media-player/styles.ts +++ /dev/null @@ -1,309 +0,0 @@ -export default ` - * { - -webkit-tap-highlight-color: transparent !important; - } - - ha-card { - margin-top: 0; - background: none; - opacity: 1; - } - .is-unavailable { - opacity: 0.5; - } - - .bubble-media-player-container { - position: relative; - width: 100%; - height: 50px; - background-color: var(--background-color-2,var(--secondary-background-color)); - border-radius: 25px; - mask-image: radial-gradient(white, black); - -webkit-transform: translateZ(0); - overflow: hidden; - touch-action: pan-y; - } - - .bubble-media-player { - display: flex; - position: absolute; - justify-content: space-between; - align-items: center; - height: 100%; - width: 100%; - transition: background-color 1.5s; - background-color: rgba(0,0,0,0); - } - - .bubble-button-container { - display: inline-grid; - grid-auto-flow: column; - gap: 10px; - align-self: center; - margin-right: 8px; - } - - .bubble-play-pause-button, - .bubble-previous-button, - .bubble-next-button, - .bubble-volume-button, - .bubble-power-button { - background: none; - border: none; - cursor: pointer; - border-radius: 100%; - padding: 6px; - height: 24px; - width: 24px; - transition: background 0.3s ease; - align-self: center; - } - - .bubble-play-pause-button { - background-color: var(--accent-color); - } - - .bubble-volume-slider { - position: absolute; - width: calc(100% - 150px); - height: 38px; - left: 50px; - overflow: hidden; - border-radius: 20px; - z-index: 1; - border: 2px solid var(--background-color-2, var(--secondary-background-color)); - background-color: var(--card-background-color, var(--ha-card-background)); - opacity: 1; - transition: opacity .2s, transform .2s; - transform: translateX(0); - } - - .bubble-range-value { - display: flex; - justify-content: flex-end; - height: 38px; - align-items: center; - padding-right: 14px; - font-size: 12px; - opacity: 0.8; - } - - .bubble-mute-button { - opacity: 1; - transition: opacity .2s, transform .2s; - transform: translateX(0); - } - - .is-hidden { - opacity: 0 !important; - pointer-events: none; - transform: translateX(14px); - } - - .bubble-range-fill { - z-index: -1; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 100%; - left: -100%; - transition: all .3s; - background-color: var(--accent-color); - } - - .is-dragging .bubble-range-fill { - transition: none; - } - - .is-light .bubble-range-fill { - opacity: 0.5; - } - - .is-unavailable .bubble-button-card { - cursor: not-allowed; - } - - .bubble-range-slider { - cursor: ew-resize; - } - .is-unavailable .bubble-range-slider { - cursor: not-allowed; - } - - .bubble-icon-container { - display: flex; - flex-wrap: wrap; - width: 38px; - height: 38px; - min-width: 38px; - min-height: 38px; - align-items: center; - justify-content: center; - margin: 6px; - border-radius: 50%; - background-color: var(--card-background-color, var(--ha-card-background)); - overflow: hidden; - z-index: 1; - position: relative; - cursor: pointer; - } - - .bubble-icon { - opacity: 0.6; - } - - .is-on .bubble-icon { - filter: brightness(1.1); - opacity: 1; - } - - .bubble-icon, - .bubble-mute-button { - display: flex; - position: absolute; - height: 38px; - width: 38px; - justify-content: center; - align-items: center; - } - - .bubble-entity-picture { - background-size: cover; - background-position: center; - height: 100%; - width: 100%; - position: absolute; - } - - .bubble-media-info-container { - display: flex; - line-height: 14px; - font-size: 12px; - flex-direction: column; - justify-content: center; - flex-grow: 1; - margin-left: 4px; - pointer-events: none; - position: relative; - overflow: hidden; - } - - .bubble-title, - .bubble-name, - .bubble-state, - .bubble-artist { - display: flex; - margin: 2px 0; - position: relative; - white-space: nowrap; - } - - .bubble-title { - font-weight: 600; - } - - .bubble-name-container { - display: flex; - line-height: 1em; - flex-direction: column; - justify-content: center; - flex-grow: 1; - font-weight: 600; - margin-left: 4px; - pointer-events: none; - position: relative; - overflow: hidden; - } - - .bubble-name { - margin: 2px 0; - } - - .bubble-state { - font-size: 12px; - opacity: 0.7; - margin: 2px 0; - font-weight: normal; - } - - .bubble-sub-button-container { - right: 0 !important; - } - - @media screen and (max-width: 250px) { - .bubble-previous-button { - display: none; - } - } - - @media screen and (max-width: 206px) { - .bubble-next-button { - display: none; - } - } - - @media screen and (max-width: 160px) { - .bubble-volume-button { - display: none; - } - } - - @keyframes tap-feedback { - 0% {transform: translateX(-100%); opacity: 0;} - 64% {transform: translateX(0); opacity: 0.1;} - 100% {transform: translateX(100%); opacity: 0;} - } - - .large .bubble-media-player-container { - height: 64px; - border-radius: 34px; - } - - .large .bubble-icon-container { - --mdc-icon-size: 28px; - min-width: 48px !important; - min-height: 48px !important; - margin-left: 8px; - } - - .large .bubble-play-pause-button { - display: flex; - height: 48px; - width: 48px; - padding: 0; - align-items: center; - justify-content: center; - } - - .large .bubble-volume-slider { - height: 48px !important; - border-radius: 24px; - left: 66px !important; - width: calc(100% - 190px) !important; - } - - .large .bubble-range-value { - place-items: center; - height: 48px; - } - - .large .bubble-button-container { - align-items: center; - gap: 14px; - } - - .rows-2 .bubble-sub-button-container { - flex-direction: column; - gap: 4px !important; - display: grid !important; - grid-template-columns: repeat(2, 1fr); - grid-template-rows: repeat(2, minmax(auto, max-content)); - grid-auto-flow: column; - width: auto; - } - - .rows-2 .bubble-sub-button { - height: 20px !important; - } -`; \ No newline at end of file diff --git a/src/cards/pop-up/changes.ts b/src/cards/pop-up/changes.ts deleted file mode 100644 index 40b762d6..00000000 --- a/src/cards/pop-up/changes.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { isColorCloseToWhite } from "../../tools/style.ts"; -import { getIcon, getIconColor, getImage, getName, getState, isEntityType, isStateOn, getWeatherIcon } from "../../tools/utils.ts"; -import { getBackdrop } from "./create.ts"; -import { addHash, onEditorChange, removeHash } from "./helpers.ts"; -import { initializesubButtonIcon } from '../../tools/global-changes.ts'; - -export function changeEditor(context) { - const detectedEditor = context.verticalStack.host.closest('hui-card-preview'); - -if (context.sectionRow.classList.contains('card')) { - // Fix the empty space caused by the pop-ups in the section view - if (!context.editor && context.sectionRow.style.position !== 'absolute') { - context.sectionRow.style.position = 'absolute'; - } else if (context.editor && context.sectionRow.style.position !== '') { - context.sectionRow.style.position = ''; - } -} - - if (context.editor || detectedEditor !== null) { - context.popUp.classList.add('editor'); - - if (detectedEditor !== null) { - context.elements.popUpContainer.classList.remove('hidden'); - } else { - context.elements.popUpContainer.classList.add('hidden'); - } - } else { - context.popUp.classList.remove('editor'); - context.elements.popUpContainer.classList.remove('hidden'); - } - onEditorChange(context); -} - -export function changeStyle(context) { - initializesubButtonIcon(context); - - const state = getState(context); - const { backdropCustomStyle } = getBackdrop(context); - - const customStyle = context.config.styles - ? Function('hass', 'entityId', 'state', 'icon', 'subButtonIcon', 'getWeatherIcon', `return \`${context.config.styles}\`;`) - (context._hass, context.config.entity, state, context.elements.icon, context.subButtonIcon, getWeatherIcon) - : ''; - - if (context.elements.customStyle) { - context.elements.customStyle.innerText = customStyle; - } - backdropCustomStyle.innerText = customStyle; -} - -export function changeTriggered(context) { - let triggerEntity = context.config.trigger_entity ?? ''; - let triggerState = context.config.trigger_state ?? ''; - let triggerClose = context.config.trigger_close ?? false; - let triggerEntityState = context._hass.states[triggerEntity]?.state; - - if (!triggerEntity) return; - if (!triggerState) return; - if (context.oldTriggerEntityState === triggerEntityState) return; - - if (context.config.hash === location.hash) { - // Popup is opened: should we close it? - if (triggerClose && triggerState !== triggerEntityState) { - removeHash(); - } - } else { - // Popup is closed: should we open it? - if (triggerEntityState === triggerState) { - addHash(context.config.hash); - } - } - - context.oldTriggerEntityState = triggerEntityState; -} \ No newline at end of file diff --git a/src/cards/pop-up/create.ts b/src/cards/pop-up/create.ts deleted file mode 100644 index 4627395a..00000000 --- a/src/cards/pop-up/create.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { convertToRGBA } from "../../tools/style.ts"; -import { addActions } from "../../tools/tap-actions.ts"; -import { createElement, toggleEntity, configChanged } from "../../tools/utils.ts"; -import { onUrlChange, removeHash } from "./helpers.ts"; -import styles, { backdropStyles } from "./styles.ts"; - -let backdrop; -let hideBackdrop = false; -let startTouchY; -let lastTouchY; - -export function getBackdrop(context) { - if (backdrop) { - return backdrop; - } - - const themeColorBackground = - getComputedStyle(document.body).getPropertyValue('--ha-card-background') || - getComputedStyle(document.body).getPropertyValue('--card-background-color'); - - const backdropStyle = createElement('style'); - backdropStyle.innerHTML = ` - ${backdropStyles} - .bubble-backdrop { - background-color: ${convertToRGBA(themeColorBackground, 0.7, 0.7)}; - } - `; - document.head.appendChild(backdropStyle); - - const backdropCustomStyle = createElement('style'); - document.head.appendChild(backdropCustomStyle); - - const backdropElement = createElement('div', 'bubble-backdrop backdrop is-hidden'); - if (context.config.hide_backdrop) { - backdropElement.style.display = 'none'; - backdropElement.style.pointerEvents = 'none'; - } - document.body.appendChild(backdropElement); - - function showBackdrop() { - backdropElement.classList.add('is-visible'); - backdropElement.classList.remove('is-hidden'); - } - - function hideBackdrop() { - backdropElement.classList.add('is-hidden'); - backdropElement.classList.remove('is-visible'); - } - - backdrop = { hideBackdrop, showBackdrop, backdropElement, backdropCustomStyle }; - - return backdrop; -} - -export function createHeader(context) { - context.elements = {}; - context.elements.closeIcon = createElement('ha-icon', 'bubble-close-icon'); - context.elements.closeIcon.icon = 'mdi:close'; - context.elements.closeButton = createElement("button", "bubble-close-button close-pop-up"); - context.elements.closeButton.addEventListener('click', removeHash); - context.elements.closeButton.appendChild(context.elements.closeIcon); - - context.elements.buttonContainer = createElement('div', 'bubble-button-container'); - context.elements.header = createElement('div', 'bubble-header'); - - const existingHeader = context.popUp.querySelector('.bubble-header-container'); - if (existingHeader === null) { - context.elements.headerContainer = createElement("div", 'bubble-header-container'); - context.elements.headerContainer.setAttribute("id", "header-container"); - context.elements.headerContainer.appendChild(context.elements.header); - context.elements.headerContainer.appendChild(context.elements.closeButton); - context.elements.header.appendChild(context.elements.buttonContainer); - } else { - context.elements.headerContainer = existingHeader; - context.elements.closeIcon = existingHeader.querySelector('.bubble-close-icon'); - context.elements.closeButton = existingHeader.querySelector('.bubble-close-button'); - context.elements.buttonContainer = existingHeader.querySelector('.bubble-button-container'); - context.elements.header = existingHeader.querySelector('.bubble-header'); - } - - context.popUp.addEventListener('touchstart', (event) => { - startTouchY = event.touches[0].clientY; - }, { passive: true }); - - context.elements.header.addEventListener('touchmove', (event) => { - const touchY = event.touches[0].clientY; - const offset = touchY - startTouchY; - if (offset > 0) { - context.popUp.style.transform = `translateY(${offset}px)`; - } - }, { passive: true }); - - context.elements.header.addEventListener('touchend', (event) => { - const touchY = event.changedTouches[0].clientY; - const offset = touchY - startTouchY; - if (offset > 50) { - removeHash(); - } else { - context.popUp.style.transform = ''; - } - }, { passive: true }); -} - -export function createStructure(context) { - try { - context.elements.style = createElement('style'); - context.elements.customStyle = createElement('style'); - - context.content.appendChild(context.elements.style); - context.content.appendChild(context.elements.customStyle); - - const themeColorBackground = - getComputedStyle(document.body).getPropertyValue('--ha-card-background') || - getComputedStyle(document.body).getPropertyValue('--card-background-color'); - - const color = context.config.bg_color ? context.config.bg_color : themeColorBackground; - const opacity = context.config.bg_opacity ?? 88; - const rgbaColor = convertToRGBA(color, (opacity / 100), 1.02); - context.popUp.style.backgroundColor = rgbaColor; - context.popUp.style.setProperty('--desktop-width', context.config.width_desktop ?? '540px'); - - if (context.config.close_on_click) { - context.popUp.addEventListener('touchend', removeHash); - } - - const contextOnUrlChange = onUrlChange(context); - - setTimeout(() => { - contextOnUrlChange(); - }, 0); - - window.addEventListener('location-changed', contextOnUrlChange); - window.addEventListener('popstate', contextOnUrlChange); - window.addEventListener('keydown', (event) => { - if (event.key === 'Escape' && context.config.hash === location.hash) { - removeHash(); - } - }, { passive: true }); - - context.popUp.addEventListener('touchmove', (event) => { - // Calculate the distance the finger has traveled - let touchMoveDistance = event.touches[0].clientY - startTouchY; - - // If the distance is positive (i.e., the finger is moving downward) and exceeds a certain threshold, close the pop-up - if (touchMoveDistance > 300 && event.touches[0].clientY > lastTouchY) { - removeHash(); - } - - // Update the Y position of the last touch - lastTouchY = event.touches[0].clientY; - }, { passive: true }); - - const existingContainer = context.popUp.querySelector('.bubble-pop-up-container'); - if (existingContainer === null) { - - context.elements.popUpContainer = createElement('div'); - context.elements.popUpContainer.classList.add('bubble-pop-up-container'); - let child = context.popUp.firstChild; - - while (child) { - context.elements.popUpContainer.appendChild(child); - child = context.popUp.firstChild; - } - } else { - context.elements.popUpContainer = existingContainer; - } - - context.popUp.appendChild(context.elements.headerContainer); - context.popUp.appendChild(context.elements.popUpContainer); - - } catch (e) { - console.error(e) - } -} - -export function prepareStructure(context) { - try { - context.cardType = "pop-up"; - context.verticalStack = context.getRootNode(); - context.sectionRow = context.verticalStack.host.parentElement; - context.popUp = context.verticalStack.querySelector('#root'); - context.popUp.classList.add('bubble-pop-up', 'pop-up', 'is-popup-closed'); - context.verticalStack.removeChild(context.popUp); - context.elements = {}; - getBackdrop(context); - - hideBackdrop = hideBackdrop || (context.config.hide_backdrop ?? false); - - context.popUp.style.setProperty('--custom-height-offset-desktop', context.config.margin_top_desktop ?? '0px'); - context.popUp.style.setProperty('--custom-height-offset-mobile', context.config.margin_top_mobile ?? '0px'); - context.popUp.style.setProperty('--custom-margin', `-${context.config.margin ?? '7px'}`); - context.popUp.style.setProperty('--custom-backdrop-filter', hideBackdrop ? 'none' : `blur(${context.config.bg_blur ?? 10}px)`); - context.popUp.style.setProperty('--custom-popup-filter', hideBackdrop ? `blur(${context.config.bg_blur ?? 10}px)` : 'none'); - context.popUp.style.setProperty('--custom-shadow-opacity', (context.config.shadow_opacity ?? 0) / 100); - - - const style = createElement('style'); - context.elements.customStyle = createElement('style'); - style.innerText = styles; - context.popUp.appendChild(style); - context.popUp.appendChild(context.elements.customStyle); - - } catch (e) { - console.error(e) - } -} \ No newline at end of file diff --git a/src/cards/pop-up/helpers.ts b/src/cards/pop-up/helpers.ts deleted file mode 100644 index 050545e4..00000000 --- a/src/cards/pop-up/helpers.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { getBackdrop } from "./create.ts"; - -let popupCount = 0; - -export function clickOutside(event) { - const targets = event.composedPath(); - const popupTarget = targets.find((target) => { - return ( - (target.classList && target.classList.contains('bubble-pop-up')) || - target.nodeName === 'HA-MORE-INFO-DIALOG' || - target.nodeName === 'HA-DIALOG-DATE-PICKER' - ); - }); - - if (popupTarget === undefined) { - removeHash(); - } -} - -export function removeHash() { - const newURL = window.location.href.split('#')[0]; - - history.replaceState(null, "", newURL); - window.dispatchEvent(new Event('location-changed')); -} -export function addHash(hash) { - const newURL = hash.startsWith('#') ? window.location.href.split('#')[0] + hash : hash; - - history.pushState(null, "", newURL); - window.dispatchEvent(new Event('location-changed')); -} -export function closePopup(context) { - if (context.popUp.classList.contains('is-popup-opened') === false) { - return; - } - - popupCount--; - document.body.style.overflow = ''; - context.popUp.classList.add('is-popup-closed'); - context.popUp.classList.remove('is-popup-opened'); - context.hideContentTimeout = setTimeout(function() { - context.popUp.style.display = 'none'; - }, 380); - context.resetCloseTimeout = () => { - clearTimeout(context.closeTimeout); - } - context.popUp.removeEventListener('touchstart', context.resetCloseTimeout); - window.removeEventListener('click', clickOutside); - - const closeOnClick = context.config.close_on_click ?? false; - if (closeOnClick) { - context.popUp.removeEventListener('mouseup', removeHash); - context.popUp.removeEventListener('touchend', removeHash); - } - - context.removeDomTimeout = window.setTimeout(() => { - if (context.popUp.parentNode === context.verticalStack) { - context.verticalStack.removeChild(context.popUp); - } - }, 360); -} -export function openPopup(context) { - if (context.popUp.classList.contains('is-popup-opened')) { - return; - } - - context.popUp.classList.add('is-popup-opened'); - - window.clearTimeout(context.removeDomTimeout); - if (context.popUp.parentNode !== context.verticalStack) { - context.verticalStack.appendChild(context.popUp); - } - popupCount++; - clearTimeout(context.closeTimeout); - clearTimeout(context.hideContentTimeout); - document.body.style.overflow = 'hidden'; - context.popUp.style.display = ''; - context.popUp.style.transform = ''; - context.popUp.addEventListener('touchstart', context.resetCloseTimeout, { passive: true }); - - const closeOnClick = context.config.close_on_click ?? false; - if (closeOnClick) { - context.popUp.addEventListener('mouseup', removeHash, { passive: true }); - context.popUp.addEventListener('touchend', removeHash, { passive: true }); - } - - requestAnimationFrame(() => { - context.popUp.classList.remove('is-popup-closed'); - window.addEventListener('click', clickOutside, { passive: true }); - }); - - if (context.config.auto_close > 0) { - context.closeTimeout = setTimeout(removeHash, context.config.auto_close); - } -} -export function onUrlChange(context) { - const { hideBackdrop, showBackdrop } = getBackdrop(context); - - return function() { - if (context.config.hash === location.hash) { - openPopup(context); - } else { - closePopup(context); - } - - if (popupCount === 0 || context.editor) { - hideBackdrop(); - } else { - showBackdrop(); - } - } -} -export function onEditorChange(context) { - const { hideBackdrop, showBackdrop } = getBackdrop(context); - const detectedEditor = context.verticalStack.host.closest('hui-card-preview'); - - if (context.editor || detectedEditor !== null) { - hideBackdrop(); - window.clearTimeout(context.removeDomTimeout); - if (context.popUp.parentNode !== context.verticalStack) { - context.verticalStack.appendChild(context.popUp); - } - } else { - if (context.config.hash === location.hash) { - openPopup(context); - showBackdrop(); - } else if (context.popUp.parentNode === context.verticalStack) { - context.verticalStack.removeChild(context.popUp); - } - } -} \ No newline at end of file diff --git a/src/cards/pop-up/index.ts b/src/cards/pop-up/index.ts deleted file mode 100644 index e4e1f368..00000000 --- a/src/cards/pop-up/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { changeEditor, changeIcon, changeLight, changeName, changeState, changeStatus, changeStyle, changeTriggered } from './changes.ts'; -import { createHeader, createStructure, prepareStructure } from './create.ts'; -import { configChanged } from "../../tools/utils.ts"; -import { handleButton } from "../../cards/button/index.ts"; -import { getButtonType } from "../../cards/button/helpers.ts"; - -export async function handlePopUp(context) { - if (context.cardType !== "pop-up") { - if ((context.getRootNode() instanceof ShadowRoot) === false) { - // The card is not added in the DOM - return; - } - prepareStructure(context); - createHeader(context); - createStructure(context); - } - - if ( - context.cardType !== "pop-up" || - context.popUp.classList.contains('is-popup-opened') || - configChanged(context, context.popUp) - ){ - changeStyle(context); - - if (context.config.entity || context.config.name) { - handleButton(context, context.elements.buttonContainer, context.elements.header); - } - } - - changeTriggered(context); - changeEditor(context); -} diff --git a/src/cards/pop-up/styles.ts b/src/cards/pop-up/styles.ts deleted file mode 100644 index 791131c1..00000000 --- a/src/cards/pop-up/styles.ts +++ /dev/null @@ -1,170 +0,0 @@ -export default ` - .bubble-pop-up-container { - display: flex; - flex-direction: column; - overflow: scroll; - height: calc(100% + 50px); - margin-top: -50px; - max-width: 100%; - padding-top: 50px; - padding-bottom: 80px; - grid-gap: 14px !important; - gap: 14px !important; - column-gap: 14px !important; - --grid-gap: 14px; - --vertical-stack-card-gap: 14px; - --horizontal-stack-card-gap: 14px; - --stack-card-gap: 14px; - -ms-overflow-style: none; /* for Internet Explorer, Edge */ - scrollbar-width: none; /* for Firefox */ - overflow-y: auto; - overflow-x: hidden; - grid-auto-rows: min-content; - mask-image: linear-gradient(to bottom, transparent 0px, black 40px, black calc(100% - 40px), transparent 100%); - -webkit-mask-image: linear-gradient(to bottom, transparent 0px, black 40px, black calc(100% - 40px), transparent 100%); - } - .bubble-pop-up.card-content { - width: 100% !important; - padding: 0 !important; - } - #root { - display: flex !important; - gap: 0 !important; - } - .bubble-pop-up { - display: flex !important; - transition: transform .36s; - position: fixed; - width: 100%; - max-width: 100%; - border-radius: 42px 42px 0 0; - box-sizing: border-box; - margin-left: var(--custom-margin); - padding: 18px 18px calc(50px + var(--custom-height-offset-mobile)) 18px; - left: 8px; - z-index: 5 !important; - bottom: calc(-50px - var(--custom-height-offset-mobile)); - } - .bubble-pop-up-container::-webkit-scrollbar { - display: none; /* for Chrome, Safari, and Opera */ - } - .bubble-pop-up > :first-child { - position: sticky; - top: 0; - z-index: 1; - background: none !important; - overflow: visible; - } - .is-popup-opened { - transform: translateY(0); - box-shadow: 0px 0px 50px rgba(0, 0, 0, var(--custom-shadow-opacity)); - backdrop-filter: var(--custom-popup-filter); - -webkit-backdrop-filter: var(--custom-popup-filter); - } - .is-popup-closed { - transform: translateY(100%) !important; - box-shadow: none !important; - backdrop-filter: none !important; - -webkit-backdrop-filter: none !important; - } - @media only screen and (min-width: 600px) { - .pop-up { - margin-left: 0 !important; - bottom: calc(-50px - var(--custom-height-offset-desktop)) !important; - min-width: var(--desktop-width, 540px); - max-width: var(--desktop-width, 540px); - left: calc(50% - (var(--desktop-width) / 2)); - padding: 18px 18px calc(50px + var(--custom-height-offset-desktop)) 18px; - } - } - @media only screen and (min-width: 870px) { - .pop-up { - left: calc(var(--mdc-drawer-width, 0px) / 2 + 50% - (var(--desktop-width) / 2)); - } - } - .bubble-pop-up.editor { - position: inherit !important; - width: 100% !important; - padding: 18px !important; - backdrop-filter: none !important; - display: block !important; - transform: none !important; - height: auto !important; - min-width: auto; - border-radius: 42px; - } - .bubble-header-container { - display: inline-flex; - height: 50px; - width: 100%; - margin: 0; - padding: 0; - } - .bubble-range-fill { - opacity: .5; - } - .bubble-header { - display: inline-flex; - position: relative; - flex-grow: 1; - margin-right: 14px; - } - .bubble-name { - font-size: 16px; - font-weight: heavy; - } - .bubble-close-button { - height: 50px; - width: 50px; - border: none; - border-radius: 50%; - z-index: 1; - background: var(--background-color,var(--secondary-background-color)); - color: var(--primary-text-color); - flex-shrink: 0; - cursor: pointer; - } - .bubble-button-card-container { - background: var(--background-color,var(--secondary-background-color)) !important; - backdrop-filter: blur(14px); - -webkit-backdrop-filter: blur(14px); - } - .bubble-pop-up-container.hidden { - height: 140px !important; - mask-image: linear-gradient(to bottom, transparent 0px, black 40px, black calc(100% - 40px), transparent 100%) !important; - -webkit-mask-image: linear-gradient(to bottom, transparent 0px, black 40px, black calc(100% - 40px), transparent 100%) !important; - } - .bubble-pop-up.editor > .bubble-pop-up-container { - padding-bottom: 0 !important; - mask-image: none; - -webkit-mask-image: none; - } -`; - -export const backdropStyles = ` - .bubble-backdrop { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 4; - opacity: 0; - transition: all 0.3s; - transition-delay: .1s; - display: flex; - } - - .bubble-backdrop.is-visible { - opacity: 1; - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - } - - .bubble-backdrop.is-hidden { - opacity: 0; - backdrop-filter: none; - -webkit-backdrop-filter: none; - pointer-events: none; - } -` \ No newline at end of file diff --git a/src/cards/separator/changes.ts b/src/cards/separator/changes.ts deleted file mode 100644 index 086e2a6d..00000000 --- a/src/cards/separator/changes.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getIcon, getName, getState, getWeatherIcon, setLayout } from "../../tools/utils.ts"; -import { initializesubButtonIcon } from '../../tools/global-changes.ts'; - -export function changeIcon(context) { - context.elements.icon.icon = getIcon(context); -} -export function changeName(context) { - const name = getName(context); - if (name !== context.elements.name.innerText) { - context.elements.name.innerText = name; - } -} -export function changeStyle(context) { - initializesubButtonIcon(context); - setLayout(context); - - const state = getState(context); - - const customStyle = context.config.styles - ? Function('hass', 'entityId', 'state', 'icon', 'subButtonIcon', 'getWeatherIcon', `return \`${context.config.styles}\`;`) - (context._hass, context.config.entity, state, context.elements.icon.icon, context.subButtonIcon, getWeatherIcon) - : ''; - - context.elements.customStyle.innerText = customStyle; -} \ No newline at end of file diff --git a/src/cards/separator/create.ts b/src/cards/separator/create.ts deleted file mode 100644 index 3baf26a7..00000000 --- a/src/cards/separator/create.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createElement } from "../../tools/utils.ts"; -import { addActions } from "../../tools/tap-actions.ts"; -import styles from "./styles.ts"; - -export function createStructure(context) { - context.elements = {}; - context.elements.separatorCard = createElement('div', 'bubble-separator separator-container'); - - context.elements.icon = createElement('ha-icon', 'bubble-icon'); - context.elements.name = createElement('h4', 'bubble-name'); - context.elements.line = createElement('div', 'bubble-line'); - - context.elements.style = createElement('style'); - context.elements.style.innerText = styles; - context.elements.customStyle = createElement('style'); - - context.elements.separatorCard.appendChild(context.elements.icon); - context.elements.separatorCard.appendChild(context.elements.name); - context.elements.separatorCard.appendChild(context.elements.line); - - context.content.innerHTML = ''; - context.content.appendChild(context.elements.separatorCard); - context.content.appendChild(context.elements.style); - context.content.appendChild(context.elements.customStyle); - - context.cardType = "separator"; -} \ No newline at end of file diff --git a/src/cards/separator/index.ts b/src/cards/separator/index.ts deleted file mode 100644 index 876cdf28..00000000 --- a/src/cards/separator/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { changeSubButtonState } from "../../tools/global-changes.ts"; -import { changeState } from "../../tools/global-changes.ts"; -import { - changeIcon, - changeName, - changeStyle -} from './changes.ts' -import { createStructure } from './create.ts'; - -export function handleSeparator(context) { - if (context.cardType !== "separator") { - createStructure(context); - } - - changeIcon(context); - changeName(context); - changeSubButtonState(context, context.content, context.elements.separatorCard); - changeStyle(context); -} diff --git a/src/cards/separator/styles.ts b/src/cards/separator/styles.ts deleted file mode 100644 index 66c05a72..00000000 --- a/src/cards/separator/styles.ts +++ /dev/null @@ -1,54 +0,0 @@ -export default ` - .bubble-separator { - display: flex; - width: 100%; - padding: 4px 0; - align-items: center; - } - .bubble-icon { - display: inline-flex; - height: 24px; - width: 24px; - margin: 0 22px 0 8px; - } - .bubble-name { - margin: 0 30px 0 0; - font-size: 16px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .bubble-name:empty { - display: none; - } - .bubble-line { - border-radius: 6px; - opacity: 0.5; - flex-grow: 1; - height: 6px; - background-color: var(--background-color, var(--secondary-background-color)); - } - .bubble-sub-button-container { - margin-left: 24px; - } - - .large .bubble-separator { - height: 58px; - } - - .rows-2 .bubble-sub-button-container { - flex-direction: column; - gap: 4px !important; - display: grid !important; - grid-template-columns: repeat(2, 1fr); - grid-template-rows: repeat(2, minmax(auto, max-content)); - grid-auto-rows: none; - grid-auto-flow: column; - width: auto; - padding-right: 14px; - } - - .rows-2 .bubble-sub-button { - height: 20px !important; - } -`; \ No newline at end of file diff --git a/src/editor/bubble-card-editor.ts b/src/editor/bubble-card-editor.ts deleted file mode 100644 index ae8fbb54..00000000 --- a/src/editor/bubble-card-editor.ts +++ /dev/null @@ -1,1790 +0,0 @@ -import { version } from '../var/version.ts'; -import { - fireEvent, - isStateOn, - getState, - getAttribute, - getIcon -} from '../tools/utils.ts'; - -const LitElement = Object.getPrototypeOf(customElements.get("ha-panel-lovelace")); -const html = LitElement?.prototype.html; -const css = LitElement?.prototype.css; - -export function createBubbleCardEditor() { - if (!LitElement) { - console.error("The Bubble Card editor needs LitElement to be defined"); - return; - } - - class BubbleCardEditor extends LitElement { - - setConfig(config) { - this._config = { - ...config - }; - } - - static get properties() { - return { - hass: {}, - _config: {} - }; - } - - get _card_type() { - return this._config.card_type || ''; - } - - get _button_type() { - return this._config.button_type || - (this._config.card_type === 'pop-up' ? '' : 'switch'); - } - - get _entity() { - return this._config.entity || ''; - } - - get _name() { - return this._config.name || ''; - } - - get _icon() { - return this._config.icon || ''; - } - - get _state() { - return this._config.state || ''; - } - - get _text() { - return this._config.text || ''; - } - - get _hash() { - return this._config.hash || '#pop-up-name'; - } - - get _trigger_entity() { - return this._config.trigger_entity || ''; - } - - get _trigger_state() { - return this._config.trigger_state || ''; - } - - get _trigger_close() { - return this._config.trigger_close || false; - } - - get _margin() { - return this._config.margin || '7px'; - } - - get _margin_top_mobile() { - return this._config.margin_top_mobile || '0px'; - } - - get _margin_top_desktop() { - return this._config.margin_top_desktop || '0px'; - } - - get _width_desktop() { - return this._config.width_desktop || '540px'; - } - - get _bg_color() { - return this._config.bg_color || ''; - } - - get _bg_opacity() { - return this._config.bg_opacity !== undefined ? this._config.bg_opacity : '88'; - } - - get _bg_blur() { - return this._config.bg_blur !== undefined ? this._config.bg_blur : '14'; - } - - get _shadow_opacity() { - return this._config.shadow_opacity !== undefined ? this._config.shadow_opacity : '0'; - } - - get _rise_animation() { - return this._config.rise_animation !== undefined ? this._config.rise_animation : true; - } - - get _auto_close() { - return this._config.auto_close || ''; - } - - get _close_on_click() { - return this._config.close_on_click || false; - } - - get _background_update() { - return this._config.background_update || false; - } - - get _icon_open() { - return this._config.icon_open || ''; - } - - get _icon_close() { - return this._config.icon_close || ''; - } - - get _icon_down() { - return this._config.icon_down || ''; - } - - get _icon_up() { - return this._config.icon_up || ''; - } - - get _open_service() { - return this._config.open_service || 'cover.open_cover'; - } - - get _close_service() { - return this._config.open_service || 'cover.close_cover'; - } - - get _stop_service() { - return this._config.open_service || 'cover.stop_cover'; - } - - get _auto_order() { - return this._config.auto_order || false; - } - - get _highlight_current_view() { - return this._config.highlight_current_view || false; - } - - get _show_state() { - const defaultState = this._config.card_type === 'state' ? true : false; - return this._config.show_state || defaultState; - } - - get _show_attribute() { - const defaultState = this._config.card_type === 'state' ? true : false; - return this._config.show_attribute || defaultState; - } - - get _show_last_changed() { - const defaultState = this._config.card_type === 'state' ? true : false; - return this._config.show_last_changed || this._config.show_last_updated || defaultState; - } - - get _attribute() { - return this._config.attribute || false; - } - - get _hide_backdrop() { - return this._config.hide_backdrop || false; - } - - get _hide_gradient() { - return this._config.hide_gradient || false; - } - - get _hide_play_pause_button() { - return this._config.hide?.play_pause_button || false; - } - - get _hide_next_button() { - return this._config.hide?.next_button || false; - } - - get _hide_previous_button() { - return this._config.hide?.previous_button || false; - } - - get _hide_volume_button() { - return this._config.hide?.volume_button || false; - } - - get _hide_power_button() { - return this._config.hide?.power_button || false; - } - - get _sub_button() { - return this._config.sub_button || ''; - } - - get _button_action() { - return this._config.button_action || ''; - } - - get _tap_action() { - return { - action: this._config.tap_action?.action || "more-info", - navigation_path: this._config.tap_action?.navigation_path || "", - url_path: this._config.tap_action?.url_path || "", - service: this._config.tap_action?.service || "", - target_entity: this._config.tap_action?.target?.entity_id || "", - data: this._config.tap_action?.data || "" - }; - } - - get _double_tap_action() { - return { - action: this._config.double_tap_action?.action || "toggle", - navigation_path: this._config.double_tap_action?.navigation_path || "", - url_path: this._config.double_tap_action?.url_path || "", - service: this._config.double_tap_action?.service || "", - target_entity: this._config.double_tap_action?.target?.entity_id || "", - data: this._config.double_tap_action?.data || "" - }; - } - - get _hold_action() { - return { - action: this._config.hold_action?.action || "toggle", - navigation_path: this._config.hold_action?.navigation_path || "", - url_path: this._config.hold_action?.url_path || "", - service: this._config.hold_action?.service || "", - target_entity: this._config.hold_action?.target?.entity_id || "", - data: this._config.hold_action?.data || "" - }; - } - - render() { - if (!this.hass) { - return html``; - } - - const root = document.querySelector("body > home-assistant").shadowRoot; - const dialog = root.querySelector("hui-dialog-edit-card").shadowRoot; - const previewElement = dialog.querySelector("ha-dialog > div.content > div.element-preview"); - - // Change the default preview element to be sticky - if (previewElement.style.position !== 'sticky') { - previewElement.style.position = 'sticky'; - previewElement.style.top = '0'; - } - - if (!this.listsUpdated) { - const formateList = item => ({ - label: item, - value: item - }); - - this.allEntitiesList = Object.keys(this.hass.states).map(formateList); - - this.lightList = Object.keys(this.hass.states).filter( - (eid) => eid.substr(0, eid.indexOf(".")) === "light" - ).map(formateList); - - this.sensorList = Object.keys(this.hass.states).filter( - (eid) => eid.substr(0, eid.indexOf(".")) === "sensor" - ).map(formateList); - - this.binarySensorList = Object.keys(this.hass.states).filter( - (eid) => eid.substr(0, eid.indexOf(".")) === "binary_sensor" - ).map(formateList); - - this.coverList = Object.keys(this.hass.states).filter( - (eid) => eid.substr(0, eid.indexOf(".")) === "cover" - ).map(formateList); - - this.mediaPlayerList = Object.keys(this.hass.states).filter( - (eid) => eid.substr(0, eid.indexOf(".")) === "media_player" - ).map(formateList); - - this.attributeList = Object.keys(this.hass.states[this._entity]?.attributes || {}).map((attributeName) => { - let entity = this.hass.states[this._entity]; - let formattedName = this.hass.formatEntityAttributeName(entity, attributeName); - return { label: formattedName, value: attributeName }; - }); - - this.cardTypeList = [{ - 'label': 'Button', - 'value': 'button' - }, - { - 'label': 'Cover', - 'value': 'cover' - }, - { - 'label': 'Empty column', - 'value': 'empty-column' - }, - { - 'label': 'Horizontal buttons stack', - 'value': 'horizontal-buttons-stack' - }, - { - 'label': 'Media player', - 'value': 'media-player' - }, - { - 'label': 'Pop-up', - 'value': 'pop-up' - }, - { - 'label': 'Separator', - 'value': 'separator' - } - ]; - - this.buttonTypeList = [{ - 'label': 'Switch', - 'value': 'switch' - }, - { - 'label': 'Slider', - 'value': 'slider' - }, - { - 'label': 'State', - 'value': 'state' - }, - { - 'label': 'Name / Text', - 'value': 'name' - } - ]; - - this.tapActionTypeList = [{ - 'label': 'More info', - 'value': 'more-info' - }, - { - 'label': 'Toggle', - 'value': 'toggle' - }, - { - 'label': 'Navigate', - 'value': 'navigate' - }, - { - 'label': 'URL', - 'value': 'url' - }, - { - 'label': 'Call service', - 'value': 'call-service' - }, - { - 'label': 'No action', - 'value': 'none' - } - ]; - - this.listsUpdated = true; - } - - const allEntitiesList = this.allEntitiesList; - const lightList = this.lightList; - const sensorList = this.sensorList; - const coverList = this.coverList; - const cardTypeList = this.cardTypeList; - const buttonTypeList = this.buttonTypeList; - const nameButton = this._config.button_type === 'name'; - - if (this._config.card_type === 'pop-up') { - return html` -
- ${this.makeDropdown("Card type", "card_type", cardTypeList)} - - -

- - Header settings -

-
- ${this.makeDropdown("Button type", "button_type", buttonTypeList)} - ${this.makeDropdown("Optional - Entity", "entity", allEntitiesList, nameButton)} - - ${this.makeDropdown("Optional - Icon", "icon")} - ${this.makeShowState()} -
-
- -

- - Pop-up settings -

-
- - - -
- -
-
- - -
- -
-
- Background updates are only recommended if you encounter issues with certain cards within your pop-up. -
-
- -

- - Pop-up trigger -

-
- This allows you to open this pop-up based on the state of any entity, for example you can open a "Security" pop-up with a camera when a person is in front of your house. You can also create a toggle helper (input_boolean) and trigger its opening/closing in an automation. - ${this.makeDropdown("Optional - Entity to open the pop-up based on its state", "trigger_entity", allEntitiesList)} - - - -
- -
-
-
-
- -

- - Styling options -

-
- ${this.makeLayoutOptions()} - -

- - Pop-up styling -

-
- - - - - - - - - - -
- -
-
- Set this toggle to true on the first pop-up of your main dashboard to disable the backdrop on all pop-ups. -
-
- ${this.makeStyleEditor()} -
-
- -

- - Tap action on icon -

-
- ${this.makeTapActionPanel("Tap action")} - ${this.makeTapActionPanel("Double tap action")} - ${this.makeTapActionPanel("Hold action")} -
-
- ${this.makeSubButtonPanel()} - - This card allows you to convert any vertical stack into a pop-up. Each pop-up is hidden by default and can be opened by targeting its link (e.g., '#pop-up-name'), with any card that support navigation_path, or with the horizontal buttons stack that is included. -

Important: This card must be placed within a vertical-stack card at the topmost position to function properly. To avoid misalignment with your layout, please place all your vertical stacks/pop-ups before any other cards on your dashboard. -
- Since v1.7.0, the optimized mode has been removed to ensure stability and to simplify updates for everyone. However, if your pop-up content still appears on the screen during page loading, you can install this similar fix. - ${this.makeVersion()} -
- `; - } else if (this._config.card_type === 'button') { - return html` -
- ${this.makeDropdown("Card type", "card_type", cardTypeList)} - ${this.makeDropdown("Button type", "button_type", buttonTypeList)} - ${this.makeDropdown(this._button_type !== 'slider' ? "Entity (toggle)" : "Entity (light or media_player)", "entity", allEntitiesList, nameButton)} - -

- - Button settings -

-
- - ${this.makeDropdown("Optional - Icon", "icon")} - ${this.makeShowState()} -
-
- -

- - Tap action on icon -

-
- ${this.makeTapActionPanel("Tap action")} - ${this.makeTapActionPanel("Double tap action")} - ${this.makeTapActionPanel("Hold action")} -
-
- -

- - Tap action on button -

-
- ${this.makeTapActionPanel("Tap action", this._button_action, 'toggle', 'button_action')} - ${this.makeTapActionPanel("Double tap action", this._button_action, 'toggle', 'button_action')} - ${this.makeTapActionPanel("Hold action", this._button_action, 'more-info', 'button_action')} -
-
- -

- - Styling options -

-
- ${this.makeLayoutOptions()} - ${this.makeStyleEditor()} -
-
- ${this.makeSubButtonPanel()} - This card allows you to control your entities. When used as a slider, it can control the brightness of a light, the volume of a media player, the position of a cover, and it also supports input number. To access color / control of an entity, simply tap on the icon. - ${this.makeVersion()} -
- `; - } else if (this._config.card_type === 'separator') { - return html` -
- ${this.makeDropdown("Card type", "card_type", cardTypeList)} - - ${this.makeDropdown("Icon", "icon")} - -

- - Styling options -

-
- ${this.makeLayoutOptions()} - ${this.makeStyleEditor()} -
-
- ${this.makeSubButtonPanel()} - This card is a simple separator for dividing your pop-up into categories / sections. e.g. Lights, Devices, Covers, Settings, Automations... - ${this.makeVersion()} -
- `; - } else if (this._config.card_type === 'horizontal-buttons-stack') { - if (!this.buttonAdded) { - this.buttonAdded = true; - this.buttonIndex = 0; - - while (this._config[(this.buttonIndex + 1) + '_link']) { - this.buttonIndex++; - } - } - - function addButton() { - this.buttonIndex++; - this.requestUpdate(); - } - - return html` -
- ${this.makeDropdown("Card type", "card_type", cardTypeList)} -
- ${this.makeButton()} -
- - - -
- -
-
- -

- - Styling options -

-
- -

- - Horizontal buttons stack styling -

-
- - - - -
- -
-
- - -
- -
-
- - -
- -
-
-
-
- ${this.makeStyleEditor()} -
-
- This card is the companion to the pop-up card, allowing you to open the corresponding pop-ups. It also allows you to open any page of your dashboard. In addition, you can add your motion sensors so that the order of the buttons adapts according to the room you just entered. This card is scrollable, remains visible and acts as a footer. - ${this.makeVersion()} -
- `; - } else if (this._config.card_type === 'cover') { - return html` -
- ${this.makeDropdown("Card type", "card_type", cardTypeList)} - ${this.makeDropdown("Entity", "entity", coverList)} - -

- - Cover settings -

-
- - ${this.makeDropdown("Optional - Open icon", "icon_open")} - ${this.makeDropdown("Optional - Closed icon", "icon_close")} - ${this.makeShowState()} -
-
- -

- - Custom services -

-
- - - -
-
- -

- - Styling options -

-
- ${this.makeLayoutOptions()} - -

- - Cover styling -

-
- ${this.makeDropdown("Optional - Arrow down icon", "icon_down")} - ${this.makeDropdown("Optional - Arrow up icon", "icon_up")} -
-
- ${this.makeStyleEditor()} -
-
- -

- - Tap action on icon -

-
- ${this.makeTapActionPanel("Tap action")} - ${this.makeTapActionPanel("Double tap action")} - ${this.makeTapActionPanel("Hold action")} -
-
- ${this.makeSubButtonPanel()} - This card allows you to control your covers. - ${this.makeVersion()} -
- `; - } else if (this._config.card_type === 'media-player') { - return html` -
- ${this.makeDropdown("Card type", "card_type", cardTypeList)} - ${this.makeDropdown("Entity", "entity", this.mediaPlayerList)} - -

- - Media player settings -

-
- - ${this.makeDropdown("Optional - Icon", "icon")} - ${this.makeShowState()} -
-
- -

- - Display/hide buttons -

-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
-
- -

- - Styling options -

-
- ${this.makeLayoutOptions()} - ${this.makeStyleEditor()} -
-
- -

- - Tap action on icon -

-
- ${this.makeTapActionPanel("Tap action")} - ${this.makeTapActionPanel("Double tap action")} - ${this.makeTapActionPanel("Hold action")} -
-
- ${this.makeSubButtonPanel()} - This card allows you to control a media player. You can tap on the icon to get more control. - ${this.makeVersion()} -
- `; - } else if (this._config.card_type === 'empty-column') { - return html` -
- ${this.makeDropdown("Card type", "card_type", cardTypeList)} - -

- - Styling options -

-
- ${this.makeStyleEditor()} -
-
- Just an empty card to fill any empty column. - ${this.makeVersion()} -
- `; - } else if (!this._config.card_type) { - return html` -
- ${this.makeDropdown("Card type", "card_type", cardTypeList)} - You need to add a card type first. - -

The Bubble Card ${version} changelog is available here. -


-

If you have an issue or a question you can find more details on my GitHub page.

- -
-

And if you like my project and want to support me, please consider making a donation. Any amount is welcome and very much appreciated! 🍻

-
- - -
- ${this.makeVersion()} -
- `; - } - } - - makeLayoutOptions() { - return html` - - -

- - Layout options for sections -

-
- - -
-
- `; - } - - makeShowState(context = this._config, config = '', array = false, index) { - const entity = context?.entity ?? this._config.entity ?? ''; - const nameButton = this._config.button_type === 'name'; - - - const attributeList = Object.keys(this.hass.states[entity]?.attributes || {}).map((attributeName) => { - let state = this.hass.states[entity]; - let formattedName = this.hass.formatEntityAttributeName(state, attributeName); - return { label: formattedName, value: attributeName }; - }); - - return html` - ${array !== 'sub_button' ? html` - - -
- -
-
- ` : ''} - ${array === 'sub_button' ? html` - - -
- -
-
- ` : ''} - - -
- -
-
- ${!array === 'sub_button' ? html` - - -
- -
-
- ` : ''} - - -
- -
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
- ${context?.show_attribute ? html` -
- -
- ` : ''} - `; - } - - makeDropdown(label, configValue, items, disabled) { - if (label.includes('icon') || label.includes('Icon')) { - return html` -
- -
- `; - } else { - return html` -
- -
- `; - } - } - - makeTapActionPanel(label, context = this._config, defaultAction, array, index = this._config) { - const hass = this.hass; - const icon = label === "Tap action" - ? "mdi:gesture-tap" - : label === "Double tap action" - ? "mdi:gesture-double-tap" - : "mdi:gesture-tap-hold"; - const valueType = label === "Tap action" - ? context.tap_action - : label === "Double tap action" - ? context.double_tap_action - : context.hold_action; - const configValueType = label === "Tap action" - ? "tap_action" - : label === "Double tap action" - ? "double_tap_action" - : "hold_action"; - const isDefault = context === this._config; - - if (!defaultAction) { - defaultAction = isDefault && label === "Tap action" - ? "more-info" - : isDefault - ? "toggle" - : ''; - } - - return html` - -

- - ${label} -

-
-
- -
- ${valueType?.action === 'navigate' ? html` -
- -
- ` : ''} - ${valueType?.action === 'url' ? html` -
- -
- ` : ''} - ${valueType?.action === 'call-service' ? html` -
- -
-
- -
- ` : ''} - ${valueType?.action === 'call-service' && valueType?.service ? html` - For now, you still need to switch to the YAML editor if you want to add data: to your service. - ` : ''} -
-
- `; - } - - makeSubButtonPanel() { - const subButtonElements = this._config?.sub_button?.map((subButton, index) => { - if (!subButton) { - return; - } - - const subButtonIndex = 'sub_button.' + index + '.'; - - const removeSubButton = (event) => { - event.stopPropagation(); - let subButtons = [...this._config.sub_button]; // Créer une copie - subButtons.splice(index, 1); - this._config.sub_button = subButtons; // Remplacer le tableau original - - this._valueChanged({ target: { configValue: 'sub_button.' + (index - 1), value: subButton } }); - this.requestUpdate(); - }; - - const moveSubButtonLeft = (event) => { - event.stopPropagation(); - if (index > 0) { - let subButtons = [...this._config.sub_button]; // Créer une copie - [subButtons[index], subButtons[index - 1]] = [subButtons[index - 1], subButtons[index]]; - this._config.sub_button = subButtons; // Remplacer le tableau original - - this._valueChanged({ target: { configValue: 'sub_button.' + index, value: this._config.sub_button[index] } }); - } - this.requestUpdate(); - }; - - const moveSubButtonRight = (event) => { - event.stopPropagation(); - if (index < this._config.sub_button.length - 1) { - let subButtons = [...this._config.sub_button]; // Créer une copie - [subButtons[index], subButtons[index + 1]] = [subButtons[index + 1], subButtons[index]]; - this._config.sub_button = subButtons; // Remplacer le tableau original - - this._valueChanged({ target: { configValue: 'sub_button.' + index, value: this._config.sub_button[index] } }); - } - this.requestUpdate(); - }; - - return html` - -

- - ${this._config.sub_button[index] ? "Button " + (index + 1) + (subButton.name ? " - " + subButton.name : "") : "New button"} - - ${index > 0 ? html`` : ''} - ${index < this._config.sub_button.length - 1 ? html`` : ''} -

-
- -

- - Button settings -

-
-
- -
-
- -
-
- -
- ${this.makeShowState(subButton, subButtonIndex, 'sub_button', index)} -
-
- -

- - Tap action on button -

-
- ${this.makeTapActionPanel("Tap action", subButton, 'more-info', 'sub_button', index)} - ${this.makeTapActionPanel("Double tap action", subButton, 'none', 'sub_button', index)} - ${this.makeTapActionPanel("Hold action", subButton, 'none', 'sub_button', index)} -
-
-
-
- `; - }); - - const addSubButton = () => { - if (!this._config.sub_button) { - this._config.sub_button = []; - } - - let newSubButton = { - entity: this._config.entity - }; - this._config.sub_button = [...this._config.sub_button]; - this._config.sub_button.push(newSubButton); - fireEvent(this, "config-changed", { config: this._config }); - this.requestUpdate(); - }; - - // Return full panel for all sub-buttons - return html` - -

- - Sub-buttons editor -

-
- ${subButtonElements} - - Add new customized buttons fixed to the right. -
-
- `; - } - - makeButton() { - let buttons = []; - for (let i = 1; i <= this.buttonIndex; i++) { - buttons.push(html` -
- -

- - Button ${i} ${this._config[i + '_name'] ? ("- " + this._config[i + '_name']) : ""} - -

-
- - - - - -
-
-
- `); - } - - return buttons; - } - - makeVersion() { - return html` -

- Bubble Card - - ${version} - -

- `; - } - - removeButton(index) { - // Removing button fields - delete this._config[index + '_name']; - delete this._config[index + '_icon']; - delete this._config[index + '_link']; - delete this._config[index + '_entity']; - delete this._config[index + '_pir_sensor']; - - // Updating indexes of following buttons - for (let i = index; i < this.buttonIndex; i++) { - this._config[i + '_name'] = this._config[(i + 1) + '_name']; - this._config[i + '_icon'] = this._config[(i + 1) + '_icon']; - this._config[i + '_link'] = this._config[(i + 1) + '_link']; - this._config[i + '_entity'] = this._config[(i + 1) + '_entity']; - this._config[i + '_pir_sensor'] = this._config[(i + 1) + '_pir_sensor']; - } - - // Removing fields of the last button - delete this._config[this.buttonIndex + '_name']; - delete this._config[this.buttonIndex + '_icon']; - delete this._config[this.buttonIndex + '_link']; - delete this._config[this.buttonIndex + '_entity']; - delete this._config[this.buttonIndex + '_pir_sensor']; - - // Updating index of the last button - this.buttonIndex--; - - fireEvent(this, "config-changed", { - config: this._config - }); - } - - makeStyleEditor() { - return html` - -

- - Custom styles -

-
-
- -
- For advanced users, you can edit the CSS style of this card in this editor. For more information, you can go here. You don't need to add styles: |, it will be added automatically. -
-
- `; - } - - _valueChanged(ev) { - const target = ev.target; - const detail = ev.detail; - let rawValue; - - // Check if the target is a ha-switch - if (target.tagName === 'HA-SWITCH') { - rawValue = target.checked; - } else if (target.value !== undefined) { - rawValue = typeof target.value === 'string' ? target.value.replace(",", ".") : target.value; - } - - if (typeof rawValue === 'string' && (rawValue.endsWith(".") || rawValue === "-")) { - return; - } - - const { configValue, checked } = target; - if (configValue) { - const value = checked !== undefined ? checked : rawValue; - const configKeys = configValue.split('.'); - let obj = this._config; - - for (let i = 0; i < configKeys.length - 1; i++) { - obj[configKeys[i]] = obj[configKeys[i]] || {}; - obj = obj[configKeys[i]]; - } - - // If the event is of type 'input', update the configuration directly with the input's value - if (ev.type === 'input') { - obj[configKeys[configKeys.length - 1]] = rawValue; - } else if (detail && obj[configKeys[configKeys.length - 1]] !== detail.value) { - obj[configKeys[configKeys.length - 1]] = detail.value; - } else if (target.tagName === 'HA-SWITCH') { - obj[configKeys[configKeys.length - 1]] = rawValue; - } - } - - fireEvent(this, "config-changed", { config: this._config }); - this.requestUpdate(); - } - - _arrayValueChange(index, value, array) { - this._config[array] = this._config[array] || []; - - let arrayCopy = [...this._config[array]]; - - arrayCopy[index] = arrayCopy[index] || {}; - arrayCopy[index] = { ...arrayCopy[index], ...value }; - - this._config[array] = arrayCopy; - - fireEvent(this, "config-changed", { config: this._config }); - - this.requestUpdate(); - } - - _tapActionValueChange(index, value, array) { - if (array === undefined) { - for (let key in value) { - this._config[key] = { ...this._config[key], ...value[key] }; - } - } else { - this._config[array] = this._config[array] || (array ? {} : []); - - let copy = Array.isArray(this._config[array]) ? [...this._config[array]] : {...this._config[array]}; - - if (Array.isArray(copy)) { - copy[index] = copy[index] || {}; - - let objCopy = { ...copy[index] }; - - for (let key in value) { - if (key in objCopy) { - objCopy[key] = { ...objCopy[key], ...value[key] }; - } else { - objCopy[key] = value[key]; - } - } - - copy[index] = objCopy; - } else { - for (let key in value) { - if (!copy.hasOwnProperty(key)) { - copy[key] = value[key]; - } else { - copy[key] = { ...copy[key], ...value[key] }; - } - } - } - - this._config[array] = copy; - } - - fireEvent(this, "config-changed", { config: this._config }); - this.requestUpdate(); - } - - static get styles() { - return css` - div { - display: grid; - grid-gap: 12px; - } - - ha-combo-box[label="Card type"]::after { - content: ""; - position: relative; - background-color: var(--background-color, var(--secondary-background-color)); - display: block; - width: 100%; - height: 1px; - top: 12px; - margin-bottom: 12px !important; - opacity: 0.6; - } - - #add-button { - margin: 0 0 14px 0; - color: var(--text-primary-color); - width: 100%; - height: 32px; - border-radius: 16px; - border: none; - background-color: var(--accent-color); - cursor: pointer; - } - - p { - margin-bottom: 4px; - } - - ha-icon, a, p, button, h4 { - color: var(--primary-text-color) !important; - } - - hr { - display: inline-block; - width: 100%; - background-color: var(--text-primary-color); - opacity: .15; - margin: 8px 0 0 0; - } - - code { - background: var(--accent-color); - background-blend-mode: darken; - padding: 2px 4px; - border-radius: 6px; - } - - .button-header { - height: auto; - width: 100%; - display: inline-flex; - align-items: center; - margin: 0 8px; - } - - .button-number { - display: inline-flex; - width: auto; - } - - .remove-button { - display: inline-flex; - border-radius: 50%; - width: 24px; - height: 24px; - text-align: center; - line-height: 24px; - vertical-align: middle; - cursor: pointer; - } - - .content { - margin: 12px 4px 14px 4px; - } - - h4 > ha-icon { - margin: 8px; - } - - ha-textfield { - width: 100%; - } - - h3 { - margin: 4px 0; - } - - .code-editor { - overflow: scroll; - } - - .icon-button { - background: var(--accent-color); - border: none; - cursor: pointer; - padding: 8px; - margin: 0; - border-radius: 32px; - font-weight: bold; - } - - .icon-button.header { - background: none; - float: right; - padding: 0; - margin: 0 8px; - } - `; - } - } - - if (!customElements.get('bubble-card-editor')) customElements.define('bubble-card-editor', BubbleCardEditor); - - return BubbleCardEditor; -} \ No newline at end of file diff --git a/src/editor/bubble-pop-up-editor.ts b/src/editor/bubble-pop-up-editor.ts deleted file mode 100644 index d5c613dd..00000000 --- a/src/editor/bubble-pop-up-editor.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { version } from '../var/version.ts'; -import { fireEvent } from '../tools/utils.ts'; - -let bubblePopUpEditor = new MutationObserver((mutationsList, observer) => { - if (customElements.get("ha-panel-lovelace")) { - - const LitElement = Object.getPrototypeOf(customElements.get("ha-panel-lovelace")); - const html = LitElement.prototype.html; - const css = LitElement.prototype.css; - - class BubblePopUpEditor extends LitElement { - - setConfig(config) { - this._config = { - ...config - }; - } - - static get properties() { - return { - hass: {}, - _config: {} - }; - } - - get _entity() { - return this._config.entity || ''; - } - - get _name() { - return this._config.name || ''; - } - - get _icon() { - return this._config.icon || ''; - } - - get _state() { - return this._config.state || ''; - } - - get _text() { - return this._config.text || ''; - } - - get _hash() { - return this._config.hash || '#pop-up-name'; - } - - get _trigger_entity() { - return this._config.trigger_entity || ''; - } - - get _trigger_state() { - return this._config.trigger_state || ''; - } - - get _trigger_close() { - return this._config.trigger_close || false; - } - - get _margin() { - return this._config.margin || '7px'; - } - - get _margin_top_mobile() { - return this._config.margin_top_mobile || '0px'; - } - - get _margin_top_desktop() { - return this._config.margin_top_desktop || '0px'; - } - - get _width_desktop() { - return this._config.width_desktop || '500px'; - } - - get _bg_color() { - return this._config.bg_color || window.color; - } - - get _bg_opacity() { - return this._config.bg_opacity !== undefined ? this._config.bg_opacity : '88'; - } - - get _bg_blur() { - return this._config.bg_blur !== undefined ? this._config.bg_blur : '14'; - } - - get _shadow_opacity() { - return this._config.shadow_opacity !== undefined ? this._config.shadow_opacity : '0'; - } - - get _is_sidebar_hidden() { - return this._config.is_sidebar_hidden || false; - } - - get _auto_close() { - return this._config.auto_close || ''; - } - - get _back_open() { - return this._config.back_open || false; - } - - render() { - if (!this.hass) { - return html``; - } - - if (!this.listsUpdated) { - const formateList = item => ({ - label: item, - value: item - }); - - this.allEntitiesList = Object.keys(this.hass.states).map(formateList); - this.listsUpdated = true; - } - - const allEntitiesList = this.allEntitiesList; - const lightList = this.lightList; - const cardTypeList = this.cardTypeList; - const buttonTypeList = this.buttonTypeList; - - return html` -
-

Pop-up - - Optimized mode - -

- Since the recent updates of Home Assistant, the optimized mode has become obsolete. The regular mode is now exactly as efficient, which greatly simplifies things for both new and current users. So if you are on the latest Home Assistant release you can remove it from your extra modules and in your pop-ups in YAML just replace custom:bubble-pop-up with custom:bubble-card then add card_type: pop-up. This mode will be removed around the v2.0.0. - - - ${this.makeDropdown("Optional - Icon", "icon")} - ${this.makeDropdown("Optional - Entity to toggle (e.g. room light group)", "entity", allEntitiesList)} - ${this.makeDropdown("Optional - Entity state to display (e.g. room temperature)", "state", allEntitiesList)} - - -

Pop-up trigger

- This allows you to open this pop-up based on the state of any entity, for example you can open a "Security" pop-up with a camera when a person is in front of your house. You can also create a toggle helper (input_boolean) and trigger its opening/closing in an automation. - ${this.makeDropdown("Optional - Entity to open the pop-up based on its state", "trigger_entity", allEntitiesList)} - - - -
- -
-
-

Styling options

- - - - - - -
- -
-
- - - - - Set ‘Background blur’ to 0 if your pop-up animations are rendering at low FPS. - ${this.makeVersion()} -
- `; - } - - makeDropdown(label, configValue, items) { - const hass = this.hass; - - if (label.includes('icon') || label.includes('Icon')) { - return html` -
- -
- `; - } else { - return html` -
- -
- `; - } - } - - makeVersion() { - return html` -

- Bubble Card - Pop-up - - ${version} - -

- `; - } - - _valueChanged(ev) { - if (!this._config || !this.hass) { - return; - } - const target = ev.target; - const detail = ev.detail; - let rawValue = typeof target.value === 'string' ? target.value.replace(",", ".") : target.value; - let value; - - if (typeof rawValue === 'string' && (rawValue.endsWith(".") || rawValue === "-")) { - return; - } - - if (target.configValue) { - if (target.type === 'ha-switch') { - value = target.checked; - } else { - if (rawValue !== "") { - if (!isNaN(parseFloat(rawValue)) && isFinite(rawValue)) { - value = parseFloat(rawValue); - if (isNaN(value)) { - value = undefined; - } - } else { - value = rawValue; - } - } - value = value !== undefined ? value : (target.checked !== undefined || !detail.value ? target.value || target.checked : target.checked || detail.value); - } - - // Check if the value has changed - if (this._config[target.configValue] !== value) { - this._config = { - ...this._config, - [target.configValue]: value, - }; - - fireEvent(this, "config-changed", { - config: this._config - }); - } - } - - // Handle ha-combo-box value changes - if (target.tagName === 'HA-COMBO-BOX') { - if (detail.value) { - this._config = { - ...this._config, - [target.configValue]: detail.value, - }; - } else { - // Handle the case when detail.value is undefined or false - this._config = { - ...this._config, - [target.configValue]: undefined, - }; - } - - fireEvent(this, "config-changed", { - config: this._config - }); - } - } - - static get styles() { - return css` - div { - display: grid; - grid-gap: 12px; - } - code { - background: var(--accent-color); - background-blend-mode: darken; - padding: 2px 4px; - border-radius: 6px; - } - `; - } - } - - customElements.define("bubble-pop-up-editor", BubblePopUpEditor); - observer.disconnect(); - } -}); - -bubblePopUpEditor.observe(document, { childList: true, subtree: true }); \ No newline at end of file diff --git a/src/tools/global-changes.ts b/src/tools/global-changes.ts deleted file mode 100644 index aacceecd..00000000 --- a/src/tools/global-changes.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { addActions, addFeedback } from "../tools/tap-actions.ts"; -import { - createElement, - applyScrollingEffect, - formatDateTime, - isStateOn, - isEntityType, - getState, - getAttribute, - getIcon -} from './utils.ts'; - -export function changeState(context) { - const buttonType = context.config.button_type; - const state = context._hass.states[context.config.entity]; - const attribute = context.config.attribute ?? ''; - - const defaultShowState = buttonType === 'state'; - const showName = context.config.show_name ?? true; - const showIcon = context.config.show_icon ?? true; - const showState = context.config.show_state ?? defaultShowState; - const showAttribute = context.config.show_attribute ?? defaultShowState; - const showLastChanged = (context.config.show_last_updated || context.config.show_last_changed) ?? ''; - - // Format state and attributes based on button type - let formattedState = state && showState ? context._hass.formatEntityState(state) : ''; - let formattedAttribute; - let formattedLastChanged; - - if (showAttribute) { - if (!attribute) { - switch (buttonType) { - case "switch": - case "state": - formattedLastChanged = state ? formatDateTime(state.last_changed, context._hass.locale.language) : ''; - break; - case "slider": - const attributeKey = isEntityType(context, "cover") ? "current_subButton" : - isEntityType(context, "light") && state.state !== 'off' ? "brightness" : - isEntityType(context, "media_player") && state.state !== 'off' ? "volume_level" : - null; - formattedAttribute = state && attributeKey ? context._hass.formatEntityAttributeValue(state, attributeKey) : ''; - break; - default: - formattedAttribute = ""; - } - } else { - formattedAttribute = state ? context._hass.formatEntityAttributeValue(state, attribute) : ''; - } - } - - if (showLastChanged) { - formattedLastChanged = state ? formatDateTime(state.last_changed, context._hass.locale.language) : ''; - } - - // Check if formattedState or formattedAttribute is 'Unknown' - if (formattedState === 'Unknown') { - formattedState = ''; - } - - if (formattedAttribute === 'Unknown') { - formattedAttribute = ''; - } - - let displayedState = ''; - - if (formattedState) { - displayedState += formattedState; - } - - if (formattedLastChanged) { - if (displayedState) { - // Add a space if formattedState is not 'off', otherwise add a dash - displayedState += (formattedState.toLowerCase() !== 'off') ? ' ' : ' - '; - } - // Capitalize formattedLastChanged if formattedState is 'off' - displayedState += (formattedState.toLowerCase() === 'off') ? capitalizeFirstLetter(formattedLastChanged) : formattedLastChanged; - } - - if (formattedAttribute) { - if (displayedState) { - displayedState += ' - '; - } - displayedState += formattedAttribute; - } - - // Function to capitalize the first letter of a string - function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); - } - - // Capitalize the first letter of displayedState - displayedState = capitalizeFirstLetter(displayedState); - - // Update display - if (showName) { - context.elements.name.style.display = ''; - } else { - context.elements.name.style.display = 'none'; - context.elements.state.style.opacity = '1'; - context.elements.state.style.fontSize = '14px'; - } - - if (!showIcon) { - context.elements.iconContainer.style.display = 'none'; - context.elements.nameContainer.style.marginLeft = '16px'; - } else { - context.elements.iconContainer.style.display = ''; - context.elements.nameContainer.style.marginLeft = ''; - } - - if (showState) { - context.elements.state.style.display = 'flex'; - } - - if (displayedState === '') { - context.elements.state.style.display = 'none'; - } else if (context.previousState !== displayedState) { - context.elements.state.style.display = ''; - applyScrollingEffect(context, context.elements.state, displayedState); - context.previousState = displayedState; - } -} - -export function changeSubButtonState(context, container = context.content, appendTo = container.firstChild.firstChild, before = false) { - const subButtons = context.config.sub_button; - if (!subButtons) return; - - context.previousValues = context.previousValues || {}; - let previousSubButtons = [...(context.previousValues.subButtons || [])]; - context.elements = { ...context.elements }; - - const subButtonContainer = context.elements.subButtonContainer || createElement('div', 'bubble-sub-button-container'); - if (!context.elements.subButtonContainer) { - Object.assign(subButtonContainer.style, subButtonStyles.subButtonContainer); - if (before) appendTo.prepend(subButtonContainer); - else appendTo.appendChild(subButtonContainer); - context.elements.subButtonContainer = subButtonContainer; - } - - subButtons.forEach((subButton, i) => { - if (!subButton) return; - - const index = i + 1; - const entity = subButton.entity ?? context.config.entity; - const state = context._hass.states[entity]; - const name = subButton.name ?? getAttribute(context, "friendly_name", entity) ?? ''; - const attributeType = subButton.attribute ?? ''; - const attribute = getAttribute(context, attributeType, entity); - const icon = getIcon(context, entity, subButton.icon); - const isOn = isStateOn(context, entity); - const backgroundColor = isOn ? 'var(--accent-color)' : 'var(--card-background-color, var(--ha-card-background))'; - - const showName = subButton.show_name ?? false; - const showState = subButton.show_state ?? false; - const showAttribute = subButton.show_attribute ?? false; - const showLastChanged = (subButton.show_last_changed || subButton.show_last_updated) ?? false; - const showIcon = subButton.show_icon ?? true; - const showBackround = subButton.show_background ?? true; - - let subButtonElement = context.elements[index] || createElement('div', 'bubble-sub-button bubble-sub-button-' + index); - if (!context.elements[index]) { - Object.assign(subButtonElement.style, subButtonStyles.subButton); - subButtonElement.nameContainer = createElement('div', 'bubble-sub-button-name-container'); - Object.assign(subButtonElement.nameContainer.style, subButtonStyles.nameContainer); - subButtonElement.feedback = createElement('div', 'bubble-feedback-element feedback-element'); - - subButtonElement.appendChild(subButtonElement.feedback); - subButtonElement.appendChild(subButtonElement.nameContainer); - subButtonContainer.appendChild(subButtonElement); - context.elements[index] = subButtonElement; - } - - if (showIcon && icon) { - let iconElement = subButtonElement.icon || createElement('ha-icon', 'bubble-sub-button-icon'); - iconElement.setAttribute('icon', icon); - iconElement.style.display = 'flex'; - iconElement.style.setProperty('--mdc-icon-size', '16px'); - subButtonElement.appendChild(iconElement); - subButtonElement.icon = iconElement; - } else if (subButtonElement.icon) { - subButtonElement.icon.style.display = 'none'; - } - - subButtonElement.style.backgroundColor = showBackround ? backgroundColor : ''; - - if (subButton.tap_action?.action !== 'none' || subButton.double_tap_action?.action !== 'none' || subButton.hold_action?.action !== 'none') { - const defaultActions = { - tap_action: { action: "more-info" }, - double_tap_action: { action: "none" }, - hold_action: { action: "none" } - }; - addActions(subButtonElement, subButton, entity, defaultActions); - addFeedback(subButtonElement, subButtonElement.feedback); - } - - let displayedState = ''; - const formattedState = state && showState ? context._hass.formatEntityState(state) : ''; - const formattedAttribute = state && attribute && showAttribute ? context._hass.formatEntityAttributeValue(state, attributeType) : ''; - const formattedLastChanged = state && showLastChanged ? formatDateTime(state.last_changed, context._hass.locale.language) : ''; - - if (showName && name) displayedState += name; - if (formattedState) displayedState += (displayedState ? ' - ' : '') + formattedState; - if (formattedLastChanged) displayedState += (displayedState ? ' - ' : '') + formattedLastChanged; - if (formattedAttribute) displayedState += (displayedState ? ' - ' : '') + formattedAttribute; - - displayedState = displayedState.charAt(0).toUpperCase() + displayedState.slice(1); - - if (!displayedState && !showIcon) { - subButtonElement.style.display = 'none'; - } else { - subButtonElement.style.display = 'flex'; - subButtonElement.nameContainer.innerText = displayedState; - if (showIcon && subButtonElement.icon) { - subButtonElement.icon.style.marginRight = displayedState ? '4px' : '0'; - subButtonElement.icon.style.setProperty('--mdc-icon-size', displayedState ? '16px' : '20px'); - } - } - }); - - context.previousValues.subButtons = subButtons.slice(); - - for (let i = previousSubButtons.length; i > 0; i--) { - if (i > subButtons.length) { - let element = context.elements[i]; - if (element) { - subButtonContainer.removeChild(element); - delete context.elements[i]; - } - } - } -} - -const subButtonStyles = { - subButtonContainer: { - position: 'relative', - display: 'flex', - justifyContent: 'end', - right: '8px', - alignContent: 'center', - gap: '8px' - }, - subButton: { - flexWrap: 'nowrap', - flexDirection: 'row-reverse', - alignItems: 'center', - justifyContent: 'center', - position: 'relative', - right: '0', - boxSizing: 'border-box', - width: 'min-content', - minWidth: '36px', - height: '36px', - verticalAlign: 'middle', - fontSize: '12px', - borderRadius: '32px', - padding: '0 8px', - overflow: 'hidden', - whiteSpace: 'nowrap', - zIndex: '1', - transition: 'all 0.5s ease-in-out', - color: 'var(--primary-text-color)' - }, - nameContainer: { - display: 'flex' - } -}; - -export function initializesubButtonIcon(context) { - if (!Array.isArray(context.subButtonIcon)) { - context.subButtonIcon = []; - } - - context.content.querySelectorAll('.bubble-sub-button-icon').forEach((iconElement, index) => { - context.subButtonIcon[index] = iconElement; - }); -} \ No newline at end of file diff --git a/src/tools/init.ts b/src/tools/init.ts deleted file mode 100644 index ee981f18..00000000 --- a/src/tools/init.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { version } from '../var/version.ts'; - -// Initialize the content if it's not there yet. - -export function initializeContent(context) { - if (!context.content) { - let shadow = context.shadowRoot || context.attachShadow({ mode: 'open' }); - let card = document.createElement("ha-card"); - card.style.cssText = "background: none; border: none; box-shadow: none; border-radius: 16px;"; - let content = document.createElement("div"); - content.className = "card-content"; - content.style.padding = "0"; - card.appendChild(content); - shadow.appendChild(card); - context.card = card; - context.content = content; - } -} diff --git a/src/tools/style.ts b/src/tools/style.ts deleted file mode 100644 index b1ebf04f..00000000 --- a/src/tools/style.ts +++ /dev/null @@ -1,226 +0,0 @@ -let timeouts = {}; -let idCounter = 0; - -export const addStyles = function(hass, context, styles, customStyles, state, entityId, stateChanged, path = '', element = context.content) { - const id = idCounter++; // Generate a unique id for each function call - const key = id + styles; // Create a unique key for each id and style combination - - const executeStyles = () => { - // Evaluate customStyles if it exists, else assign an empty string - const customStylesEval = customStyles ? Function('hass', 'entityId', 'state', 'return `' + customStyles + '`;')(hass, entityId, state) : ''; - let styleAddedKey = styles + 'Added'; // Append 'Added' to the styles value - - // Check if the style has changed - if (!context[styleAddedKey] || context.previousStyle !== customStylesEval || stateChanged || context.previousConfig !== context.config) { - if (!context[styleAddedKey]) { - // Check if the style element already exists - context.styleElement = element.querySelector('style'); - if (!context.styleElement) { - // If not, create a new style element - context.styleElement = document.createElement('style'); - const parentElement = (path ? element.querySelector(path) : element); - parentElement?.appendChild(context.styleElement); - } - context[styleAddedKey] = true; - } - - // Create a new style element and update its content - const newStyleElement = document.createElement('style'); - newStyleElement.innerHTML = customStylesEval + styles; - - // Add the new style element to the DOM before removing the old one - context.styleElement.parentNode.insertBefore(newStyleElement, context.styleElement.nextSibling); - - // Remove the old style element - context.styleElement.parentNode.removeChild(context.styleElement); - - // Update the reference to the style element - context.styleElement = newStyleElement; - - context.previousStyle = customStylesEval; // Store the current style - context.previousConfig = context.config; // Store the current config - } - } - - if (timeouts[key]) { - clearTimeout(timeouts[key]); // Clear the timeout for the specific key if it exists - } else { - executeStyles(); // Execute instantly for the first call - } - - timeouts[key] = setTimeout(executeStyles, 500); // 500ms delay -} - -export function createIcon(context, entityId, icon, iconContainer, editor) { - let hass = context._hass; - - let entityAttributes = !entityId || !hass.states[entityId].attributes ? false : hass.states[entityId].attributes; - context.imageUrl = !entityAttributes.entity_picture ? false : entityAttributes.entity_picture; - updateIcon(context, hass, entityId, icon, iconContainer); - - if (editor) { - return; - } - - setInterval(() => { - hass = context._hass; - - if (!entityId.startsWith('media_player.')) { - return; - } - - if (entityId && hass.states[entityId]) { - context.currentEntityPicture = hass.states[entityId].attributes.entity_picture; - if (context.currentEntityPicture !== context.previousEntityPicture) { - context.imageUrl = context.currentEntityPicture; - updateIcon(context, hass, entityId, icon, iconContainer); - context.previousEntityPicture = context.currentEntityPicture; - } - } - }, 1000); // Check every second if the entity picture changed -} - -export function updateIcon(context, hass, entityId, icon, iconContainer) { - while (iconContainer.firstChild) { - iconContainer.removeChild(iconContainer.firstChild); - } - - let imageUrl = context.config.icon && context.config.icon.includes('/') - ? context.config.icon - : context.imageUrl - ? context.imageUrl - : ''; - - function isPlayerActive(state) { - if (entityId.startsWith("media_player.")) { - const inactiveStates = ['off', 'unknown', 'idle', undefined]; - return !inactiveStates.includes(state); - } else { - return false; - } - } - - if (imageUrl && (isPlayerActive(hass.states[entityId].state) || !entityId.startsWith("media_player."))) { - const img = document.createElement("div"); - img.setAttribute("class", "entity-picture"); - img.setAttribute("alt", "Icon"); - if (iconContainer) { - iconContainer.appendChild(img); - iconContainer.style.background = "center / cover no-repeat url(" + imageUrl + "), var(--card-background-color,var(--ha-card-background))"; - } - } else { - const haIcon = document.createElement("ha-icon"); - haIcon.setAttribute("icon", icon); - haIcon.setAttribute("class", "icon"); - if (iconContainer) { - iconContainer.appendChild(haIcon); - } - } -} - -export function isColorCloseToWhite(rgbColor) { - let whiteThreshold = [220, 220, 190]; - for(let i = 0; i < 3; i++) { - if(rgbColor[i] < whiteThreshold[i]) { - return false; - } - } - return true; -} - -let rgbaColor; - -export function convertToRGBA(color, opacity, lighten = 1) { - if (color.startsWith('#')) { - if (color.length === 4) { // Short hexadecimal color - let r = Math.min(255, parseInt(color.charAt(1).repeat(2), 16) * lighten), - g = Math.min(255, parseInt(color.charAt(2).repeat(2), 16) * lighten), - b = Math.min(255, parseInt(color.charAt(3).repeat(2), 16) * lighten); - rgbaColor = "rgba(" + r + ", " + g + ", " + b + ", " + opacity + ")"; - } else { // Regular hexadecimal color - let r = Math.min(255, parseInt(color.slice(1, 3), 16) * lighten), - g = Math.min(255, parseInt(color.slice(3, 5), 16) * lighten), - b = Math.min(255, parseInt(color.slice(5, 7), 16) * lighten); - rgbaColor = "rgba(" + r + ", " + g + ", " + b + ", " + opacity + ")"; - } - } else if (color.startsWith('rgb')) { - let rgbValues = color.match(/\d+/g); - rgbaColor = "rgba(" + Math.min(255, rgbValues[0] * lighten) + ", " + Math.min(255, rgbValues[1] * lighten) + ", " + Math.min(255, rgbValues[2] * lighten) + ", " + opacity + ")"; - } else if (color.startsWith('var(--')) { - // New code for CSS variables - let cssVar = color.slice(4, -1); - let computedColor = window.getComputedStyle(document.documentElement).getPropertyValue(cssVar); - if (computedColor.startsWith('#') || computedColor.startsWith('rgb')) { - rgbaColor = convertToRGBA(computedColor, opacity, lighten); - } - } - return rgbaColor; -} - -export function getIconColor(hass, entityId, stateOn, isColorCloseToWhite, rgbColor) { - let iconColorOpacity, iconColor, iconFilter; - - if (entityId && entityId.startsWith("light.")) { - rgbColor = hass.states[entityId].attributes.rgb_color; - iconColorOpacity = rgbColor - ? (!isColorCloseToWhite(rgbColor) ? `rgba(${rgbColor}, 0.5)` : 'rgba(255,220,200,0.5)') - : (stateOn ? 'rgba(255,220,200, 0.5)' : `rgba(255, 255, 255, 0.5)`); - iconColor = rgbColor - ? (!isColorCloseToWhite(rgbColor) ? `rgb(${rgbColor})` : 'rgb(255,220,200)') - : (stateOn ? 'rgba(255,220,200, 1)' : 'rgba(255, 255, 255, 1)'); - iconFilter = rgbColor ? - (!isColorCloseToWhite(rgbColor) ? 'brightness(1.1)' : 'none') : - 'none'; - } else { - iconColorOpacity = `var(--accent-color)`; - iconFilter = 'brightness(1.1)'; - } - - return { iconColorOpacity, iconColor, iconFilter }; -} - -export function getIconStyles(entityId, stateOn, iconColor, iconFilter) { - return ` - .icon-container { - position: relative; - display: flex; - flex-wrap: wrap; - align-content: center; - justify-content: center; - z-index: 1; - min-width: 38px; - min-height: 38px; - margin: 6px; - border-radius: 50%; - cursor: pointer !important; - background-color: var(--card-background-color,var(--ha-card-background)); - } - - .icon-container::after { - content: ''; - position: absolute; - display: block; - opacity: ${entityId.startsWith("light.") ? '0.2' : '0'}; - width: 100%; - height: 100%; - transition: all 1s; - border-radius: 50%; - background-color: ${stateOn ? (iconColor ? iconColor : 'var(--accent-color)') : 'var(--card-background-color,var(--ha-card-background))'}; - } - - .icon { - display: flex; - width: 22px; - color: ${stateOn ? (iconColor ? iconColor : 'var(--accent-color)') : 'inherit'} !important; - opacity: ${stateOn ? '1' : (entityId ? '0.6' : '1')}; - filter: ${stateOn ? (iconColor ? iconFilter : 'brightness(1.1)') : 'inherit'}; - } - - .entity-picture { - display: flex; - height: 38px; - width: 38px; - border-radius: 100%; - } - `; -} \ No newline at end of file diff --git a/src/tools/tap-actions.ts b/src/tools/tap-actions.ts deleted file mode 100644 index a2ec75d1..00000000 --- a/src/tools/tap-actions.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { tapFeedback } from "./utils.ts"; - -const maxHoldDuration = 300; -const doubleTapTimeout = 300; - -export function callAction(element, config, action) { - setTimeout(() => { - const event = new Event('hass-action', { bubbles: true, composed: true }); - event.detail = { config, action }; - element.dispatchEvent(event); - }, 1); -} - -class ActionHandler { - constructor(element, config, sendActionEvent, defaultEntity, defaultActions) { - this.element = element; - this.config = config; - this.sendActionEvent = sendActionEvent; - this.defaultEntity = defaultEntity; - this.defaultActions = defaultActions; - this.tapTimeout = null; - this.lastTap = 0; - this.startTime = null; - } - - handleStart(e) { - e.stopPropagation(); - e.stopImmediatePropagation(); - - this.startTime = Date.now(); - clearTimeout(this.tapTimeout); - } - - handleEnd() { - if (this.startTime === null) return; - - const currentTime = Date.now(); - const holdDuration = currentTime - this.startTime; - const doubleTapDuration = currentTime - this.lastTap; - - this.lastTap = currentTime; - this.startTime = null; - - if (holdDuration > maxHoldDuration) { - this.sendActionEvent(this.element, this.config, 'hold', this.defaultEntity, this.defaultActions); - } else if (doubleTapDuration < doubleTapTimeout) { - this.sendActionEvent(this.element, this.config, 'double_tap', this.defaultEntity, this.defaultActions); - } else { - this.tapTimeout = setTimeout(() => { - this.sendActionEvent(this.element, this.config, 'tap', this.defaultEntity, this.defaultActions); - }, doubleTapTimeout); - } - } -} - -export function sendActionEvent(element, config, action, defaultEntity, defaultActions) { - const tapAction = config?.tap_action || defaultActions?.tap_action || { action: "more-info" }; - const doubleTapAction = config?.double_tap_action || defaultActions?.double_tap_action || { action: "toggle" }; - const holdAction = config?.hold_action || defaultActions?.hold_action || { action: "toggle" }; - const entity = config?.entity ?? defaultEntity; - - callAction( - element, - { entity: entity, tap_action: tapAction, double_tap_action: doubleTapAction, hold_action: holdAction }, - action - ); -} - -export function addActions(element, config, defaultEntity, defaultActions) { - const handler = new ActionHandler(element, config, sendActionEvent, defaultEntity, defaultActions); - - element.addEventListener('pointerdown', handler.handleStart.bind(handler)); - element.addEventListener('pointerup', handler.handleEnd.bind(handler)); - element.addEventListener('contextmenu', (e) => e.preventDefault()); - element.style.cursor = 'pointer'; -} - -export function addFeedback(element, feedbackElement) { - element.addEventListener('click', () => tapFeedback(feedbackElement)); -} diff --git a/src/tools/url-listener.ts b/src/tools/url-listener.ts deleted file mode 100644 index f370e2ce..00000000 --- a/src/tools/url-listener.ts +++ /dev/null @@ -1,27 +0,0 @@ -// 'urlChanged' custom event for the pop-ups - -const event = new Event('urlChanged'); - -export function addUrlListener() { - if (!window.eventAdded) { - - window.eventAdded = true; - window.popUpInitialized = false; - - ['location-changed', 'connection-status', 'popstate'].forEach((eventType) => { - window.addEventListener(eventType, urlChanged); - }, { passive: true, once: true }); - - function urlChanged() { - let count = 0; - window.dispatchEvent(event); - } - - // Check url when pop-ups are initialized - const popUpInitialized = () => { - window.dispatchEvent(event); - }; - - window.addEventListener('popUpInitialized', popUpInitialized, { passive: true, once: true }); - } -} diff --git a/src/tools/utils.ts b/src/tools/utils.ts deleted file mode 100644 index 51e9e04e..00000000 --- a/src/tools/utils.ts +++ /dev/null @@ -1,506 +0,0 @@ -import { isColorCloseToWhite } from "./style.ts"; - -export function hasStateChanged(context, hass, entityId) { - context.hasState = hass.states[entityId]; - if (context.hasState) { - context.newState = [context.hasState.state, context.hasState.attributes.rgb_color]; - if (!context.oldState || context.newState[0] !== context.oldState[0] || context.newState[1] !== context.oldState[1]) { - context.oldState = context.newState; - context.stateChanged = true; - } else { - context.stateChanged = false; - } - - return context.stateChanged; - } -} - -export function configChanged(context, card) { - if ( - card.classList.contains('editor') && - context.config !== context.previousConfig - ){ - context.previousConfig = context.config; - return true; - } else { - return false; - } -} - -export const fireEvent = (node, type, detail, options) => { - options = options || {}; - detail = detail === null || detail === undefined ? {} : detail; - const event = new Event(type, { - bubbles: options.bubbles === undefined ? true : options.bubbles, - cancelable: Boolean(options.cancelable), - composed: options.composed === undefined ? true : options.composed, - }); - event.detail = detail; - node.dispatchEvent(event); - return event; -}; - -export const forwardHaptic = hapticType => { - fireEvent(window, "haptic", hapticType) -} - -export const navigate = (_node, path, replace = false) => { - if (replace) { - history.replaceState(null, "", path) - } else { - history.pushState(null, "", path) - } - fireEvent(window, "location-changed", { - replace - }) -} - -export function toggleEntity(hass, entityId) { - hass.callService('homeassistant', 'toggle', { - entity_id: entityId - }); -} - -export function tapFeedback(feedbackElement) { - if (feedbackElement === undefined) return; - - forwardHaptic("success"); - - feedbackElement.style.display = ''; - feedbackElement.style.animation = 'tap-feedback .3s'; - - setTimeout(() => { - feedbackElement.style.animation = 'none'; - feedbackElement.style.display = 'none'; - }, 500); -} - -export function getIcon(context, entity = context.config.entity, icon = context.config.icon) { - const entityType = entity?.split('.')[0]; - const entityIcon = getAttribute(context, "icon", entity); - const configIcon = icon; - const state = getState(context, entity); - - function coverIcon() { - const open = state !== "closed"; - const coverType = getAttribute(context, 'device_class', entity); - - switch (coverType) { - case 'awning': - return open ? "mdi:awning-outline" : "mdi:awning"; - case "blind": - return open ? "mdi:blinds-open" : "mdi:blinds"; - case 'curtain': - return open ? "mdi:curtains-open" : "mdi:curtains"; - case 'damper': - return open ? "mdi:window-shutter-open" : "mdi:window-shutter"; // To be defined - case "door": - return open ? "mdi:door-open" : "mdi:door-closed"; - case "garage": - return open ? "mdi:garage-open" : "mdi:garage"; - case 'gate': - return open ? "mdi:gate-open" : "mdi:gate"; - case 'shade': - return open ? "mdi:roller-shade" : "mdi:roller-shade-closed"; - case "shutter": - return open ? "mdi:window-shutter-open" : "mdi:window-shutter"; - case "window": - return open ? "mdi:window-open" : "mdi:window-closed"; - default: - return open ? "mdi:window-shutter-open" : "mdi:window-shutter"; - } - } - - function binarySensorIcon() { - const isOff = state === "off"; - const binarySensorType = getAttribute(context, 'device_class', entity); - - switch (binarySensorType) { - case "battery": - return isOff ? "mdi:battery" : "mdi:battery-outline"; - case "battery_charging": - return isOff ? "mdi:battery" : "mdi:battery-charging"; - case "cold": - return isOff ? "mdi:thermometer" : "mdi:snowflake"; - case "connectivity": - return isOff ? "mdi:server-network-off" : "mdi:server-network"; - case "door": - return isOff ? "mdi:door-closed" : "mdi:door-open"; - case "garage_door": - return isOff ? "mdi:garage" : "mdi:garage-open"; - case "power": - return isOff ? "mdi:power-plug-off" : "mdi:power-plug"; - case "tamper": - return isOff ? "mdi:check-circle" : "mdi:alert-circle"; - case "smoke": - return isOff ? "mdi:check-circle" : "mdi:smoke"; - case "heat": - return isOff ? "mdi:thermometer" : "mdi:fire"; - case "light": - return isOff ? "mdi:brightness-5" : "mdi:brightness-7"; - case "lock": - return isOff ? "mdi:lock" : "mdi:lock-open"; - case "moisture": - return isOff ? "mdi:water-off" : "mdi:water"; - case "motion": - return isOff ? "mdi:motion-sensor-off" : "mdi:motion-sensor"; - case "occupancy": - return isOff ? "mdi:home-outline" : "mdi:home"; - case "opening": - return isOff ? "mdi:square" : "mdi:square-outline"; - case "plug": - return isOff ? "mdi:power-plug-off" : "mdi:power-plug"; - case "presence": - return isOff ? "mdi:home-outline" : "mdi:home"; - case "running": - return isOff ? "mdi:stop" : "mdi:play"; - case "sound": - return isOff ? "mdi:music-note-off" : "mdi:music-note"; - case "update": - return isOff ? "mdi:package" : "mdi:package-up"; - case "vibration": - return isOff ? "mdi:crop-portrait" : "mdi:vibrate"; - case "window": - return isOff ? "mdi:window-closed" : "mdi:window-open"; - default: - return isOff ? "mdi:radiobox-blank" : "mdi:checkbox-marked-circle"; - } - } - - function weatherIcon(weatherType = getState(context, entity)) { - switch (weatherType) { - case 'cloudy': - return "mdi:weather-cloudy"; - case 'partlycloudy': - return "mdi:weather-partly-cloudy"; - case 'rainy': - return "mdi:weather-rainy"; - case 'snowy': - return "mdi:weather-snowy"; - case 'sunny': - return "mdi:weather-sunny"; - case 'clear-night': - return "mdi:weather-night"; - case 'fog': - return "mdi:weather-fog"; - case 'hail': - return "mdi:weather-hail"; - case 'lightning': - return "mdi:weather-lightning"; - case 'lightning-rainy': - return "mdi:weather-lightning-rainy"; - case 'pouring': - return "mdi:weather-pouring"; - case 'windy': - return "mdi:weather-windy"; - case 'windy-variant': - return "mdi:weather-windy-variant"; - case 'exceptional': - return "mdi:alert-circle-outline"; - default: - return "mdi:weather-cloudy"; - } - } - - const defaultIcons = { - alarm_control_panel: 'mdi:shield', - alert: "mdi:alert", - automation: "mdi:playlist-play", - binary_sensor: binarySensorIcon(), - calendar: "mdi:calendar", - camera: "mdi:video", - climate: "mdi:thermostat", - configurator: "mdi:settings", - conversation: "mdi:text-to-speech", - cover: coverIcon(), - device_tracker: "mdi:account", - fan: "mdi:fan", - group: "mdi:google-circles-communities", - history_graph: "mdi:chart-line", - homeassistant: "mdi:home-assistant", - homekit: "mdi:home-automation", - image_processing: "mdi:image-filter-frames", - input_boolean: "mdi:drawing", - input_datetime: "mdi:calendar-clock", - input_number: "mdi:ray-vertex", - input_select: "mdi:format-list-bulleted", - input_text: "mdi:textbox", - light: "mdi:lightbulb", - lock: 'mdi:lock', - mailbox: "mdi:mailbox", - media_player: 'mdi:speaker', - mower: "mdi:robot-mower", - notify: "mdi:comment-alert", - person: "mdi:account", - plant: "mdi:flower", - proximity: "mdi:apple-safari", - remote: "mdi:remote", - scene: "mdi:palette", - script: "mdi:file-document", - sensor: "mdi:eye", - simple_alarm: "mdi:bell", - sun: "mdi:white-balance-sunny", - switch: "mdi:flash", - timer: "mdi:timer", - updater: "mdi:cloud-upload", - vacuum: "mdi:robot-vacuum", - water_heater: "mdi:thermometer", - weather: weatherIcon(), - weblink: "mdi:open-in-new" - }; - - if (configIcon && entityType !== 'weather') return configIcon; - if (entityIcon) return entityIcon; - if (defaultIcons[entityType]) return defaultIcons[entityType]; - - return ''; -} - -export function getWeatherIcon(weatherType) { - switch (weatherType) { - case 'cloudy': - return "mdi:weather-cloudy"; - case 'partlycloudy': - return "mdi:weather-partly-cloudy"; - case 'rainy': - return "mdi:weather-rainy"; - case 'snowy': - return "mdi:weather-snowy"; - case 'sunny': - return "mdi:weather-sunny"; - case 'clear-night': - return "mdi:weather-night"; - case 'fog': - return "mdi:weather-fog"; - case 'hail': - return "mdi:weather-hail"; - case 'lightning': - return "mdi:weather-lightning"; - case 'lightning-rainy': - return "mdi:weather-lightning-rainy"; - case 'pouring': - return "mdi:weather-pouring"; - case 'windy': - return "mdi:weather-windy"; - case 'windy-variant': - return "mdi:weather-windy-variant"; - case 'exceptional': - return "mdi:alert-circle-outline"; - default: - return "mdi:weather-cloudy"; - } -} - -export function getIconColor(context) { - const entity = context.config.entity; - const defaultColor = `var(--accent-color)`; - const entityRgbColor = getAttribute(context, "rgb_color"); - - if (!entity) return defaultColor; - if (entity.startsWith("light.") === false) return defaultColor; - - const defaultLightOnColor = 'rgba(255, 220, 200)'; - const defaultLightOffColor = 'rgba(255, 255, 255)'; - const defaultLightColor = isStateOn(context) ? defaultLightOnColor : defaultLightOffColor; - - if (!entityRgbColor) return defaultLightColor; - - return isColorCloseToWhite(entityRgbColor) ? defaultLightOnColor : `rgba(${entityRgbColor.join(', ')})`; -} - -export function getImage(context) { - if (context.config.force_icon) return ''; - - const entityImageLocal = getAttribute(context, "entity_picture_local"); - const entityImage = getAttribute(context, "entity_picture"); - - if (entityImageLocal) return entityImageLocal; - if (entityImage) return entityImage; - - return ''; -} - -export function getName(context) { - const configName = context.config.name; - const entityName = getAttribute(context, "friendly_name"); - - if (configName) return configName; - if (entityName) return entityName; - - return ''; -} - -export function getState(context, entity = context.config.entity) { - return context._hass.states[entity]?.state ?? ''; -} - -export function getAttribute(context, attribute, entity = context.config.entity) { - return context._hass.states[entity]?.attributes[attribute] ?? ''; -} - -export function isEntityType(context, entityType) { - return context.config.entity?.startsWith(entityType + ".") ?? false; -} - -export function isStateOn(context, entity = context.config.entity) { - const state = getState(context, entity); - const numericState = Number(state); - const activeStringStates = ['on', 'open', 'opening', 'closing', 'cleaning', 'true', 'idle', 'home', 'playing', 'locked', 'occupied', 'available', 'running', 'active', 'connected', 'mowing']; - - if (activeStringStates.includes(state) || numericState > 0) { - return true; - } - - return false; -} - -export function createElement(tag, classNames = '') { - const element = document.createElement(tag); - - if (classNames !== '') { - classNames.split(' ').forEach(className => { - element.classList.add(className); - }); - } - - return element; -} - -export function applyScrollingEffect(context, element, text) { - // Add a scrolling effect on any text element that is longer than its container - // You need to manually add "overflow: hidden" to your container CSS - - const scrollingEffect = context.config.scrolling_effect ?? true; - - if (element.previousText === text || element.style.whiteSpace === 'normal') return; - - const classNames = element.className.split(' '); - const className = classNames.find(name => name.startsWith('bubble-')); - - // Remove the previous scrolling effect if it exists - element.innerHTML = text; - element.style = ''; - - // Check if the text is longer than its container - function checkIfContentIsLonger() { - if (scrollingEffect && element.scrollWidth > element.parentNode.offsetWidth) { - // Add the CSS for the scrolling effect - const separator = `|`; - element.innerHTML = `${text + separator + text + separator}`; - - const css = createScrollingEffectCSS(className); - element.styleElement = document.createElement('style'); - element.appendChild(element.styleElement); - element.styleElement.innerHTML = css; - } else { - // If the text fits without scrolling, remove the style element - if (scrollingEffect && element.styleElement) { - element.styleElement = null; - } - // If scrollingEffect is false, limit the text to two lines - if (!scrollingEffect) { - element.style.whiteSpace = 'normal'; - element.style.display = '-webkit-box'; - element.style.webkitLineClamp = '2'; - element.style.webkitBoxOrient = 'vertical'; - element.style.textOverflow = 'ellipsis'; - } - // If the condition is not met, check again at the next frame - requestAnimationFrame(checkIfContentIsLonger); - } - } - - requestAnimationFrame(checkIfContentIsLonger); - - element.previousText = text; - - function createScrollingEffectCSS(className) { - return ` - .${className} { - white-space: nowrap; - mask-image: linear-gradient(to right, transparent, black calc(0% + 8px), black calc(100% - 8px), transparent); - } - .${className} span { - display: inline-block; - animation: scroll 14s linear infinite; - } - - .bubble-scroll-separator { - opacity: .3; - margin: 0 6px 0 8px; - } - - @keyframes scroll { - from { transform: translateX(0%); } - to { transform: translateX(-50%); } - } - `; - } -} - -export function formatDateTime(datetime, locale) { - if (!datetime) return ''; - const date = new Date(datetime); - const now = new Date(); - let diffInSeconds = Math.floor((now - date) / 1000); - - if (isNaN(diffInSeconds)) { - // datetime was not a valid date - return ''; - } - - let unit; - let value; - if (diffInSeconds < 60) { - unit = 'second'; - value = diffInSeconds + 1; - } else if (diffInSeconds < 3600) { - unit = 'minute'; - value = Math.floor(diffInSeconds / 60); - } else if (diffInSeconds < 86400) { - unit = 'hour'; - value = Math.floor(diffInSeconds / 3600); - } else { - unit = 'day'; - value = Math.floor(diffInSeconds / 86400); - } - - const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }); - return rtf.format(-value, unit); -} - -export function setLayout(context) { - const cardLayout = context.config.card_layout; - - if (cardLayout === 'large') { - if (!context.content.classList.contains('large')) { - context.content.classList.add('large'); - } - if (context.content.classList.contains('rows-2')) { - context.content.classList.remove('rows-2'); - } - } else if (cardLayout === 'large-2-rows') { - if (!context.content.classList.contains('large')) { - context.content.classList.add('large'); - } - if (!context.content.classList.contains('rows-2')) { - context.content.classList.add('rows-2'); - } - } else { - context.content.classList.remove('large'); - context.content.classList.remove('rows-2'); - } -} - -export function throttle(mainFunction, delay = 300) { - let timerFlag; - - return (...args) => { - if (timerFlag === undefined) { - mainFunction(...args); - timerFlag = setTimeout(() => { - timerFlag = undefined; - }, delay); - } - }; -} \ No newline at end of file diff --git a/src/var/cards.ts b/src/var/cards.ts deleted file mode 100644 index 9407b7cf..00000000 --- a/src/var/cards.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Cards variables - -import { - isColorCloseToWhite, - convertToRGBA, - getIconColor, - getIconStyles -} from '../tools/style.ts'; -import { hasStateChanged } from '../tools/utils.ts'; - -let popUpOpen; -let rgbColor; -let rgbaColor; -let formatedState; -let haStyle; -let themeBgColor; - -export function getVariables(context, config, hass, editor) { - let customStyles = !config.styles ? '' : config.styles; - let entityId = config.entity && hass.states[config.entity] - ? config.entity - : ''; - let state = entityId ? hass.states[entityId].state : ''; - hasStateChanged(context, hass, entityId); - let stateChanged = context.stateChanged; - - let icon = !config.icon && entityId - ? hass.states[entityId].attributes.icon || hass.states[entityId].attributes.entity_picture || '' - : config.icon || ''; - let name = config.name - ? config.name - : entityId - ? hass.states[entityId].attributes.friendly_name - : ''; - let widthDesktop = config.width_desktop || '540px'; - let widthDesktopDivided = widthDesktop ? widthDesktop.match(/(\d+)(\D+)/) : ''; - let isSidebarHidden = config.is_sidebar_hidden || false; - let stateOn = ['on', 'open', 'cleaning', 'true', 'home', 'playing'].includes(state) || (Number(state) !== 0 && !isNaN(Number(state))); - let riseAnimation = config.rise_animation !== undefined ? config.rise_animation : true; - let marginCenter = config.margin - ? (config.margin !== '0' ? config.margin : '0px') - : '7px'; - let bgOpacity = config.bg_opacity !== undefined ? config.bg_opacity : '88'; - let shadowOpacity = config.shadow_opacity !== undefined ? config.shadow_opacity : '0'; - let bgBlur = config.bg_blur !== undefined ? config.bg_blur : '10'; - let { - iconColorOpacity, - iconColor, - iconFilter - } = getIconColor(hass, entityId, stateOn, isColorCloseToWhite); - let iconStyles = getIconStyles(entityId, stateOn, iconColor, iconFilter); - haStyle = !haStyle ? getComputedStyle(document.body) : ''; - themeBgColor = !themeBgColor ? haStyle.getPropertyValue('--ha-card-background') || haStyle.getPropertyValue('--card-background-color') : ''; - let color = config.bg_color ? config.bg_color : themeBgColor; - if (color && (!context.rgbaColor || context.rgbaColor !== context.color || editor)) { - const lighten = 1.02; - context.rgbaColor = convertToRGBA(color, (bgOpacity / 100), lighten); - context.color = context.rgbaColor; - rgbaColor = context.rgbaColor; - window.color = color; - } - - return { customStyles, entityId, icon, name, widthDesktop, widthDesktopDivided, isSidebarHidden, state, stateChanged, stateOn, formatedState, riseAnimation, marginCenter, popUpOpen, rgbaColor, rgbColor, bgOpacity, shadowOpacity, bgBlur, iconColorOpacity, iconColor, iconFilter, iconStyles, haStyle, themeBgColor, color }; -} diff --git a/src/var/version.ts b/src/var/version.ts deleted file mode 100644 index 3c54709e..00000000 --- a/src/var/version.ts +++ /dev/null @@ -1 +0,0 @@ -export let version = 'v2.0.0-beta.7'; \ No newline at end of file