A different take on designing a Lovelace UI

I have searched and tested, the problem is browser, backdrop-filter. Try other browser. I use chrome, when I resize to “phone screen” then it blurs, otherwise not.

hey, can you share your config? :slight_smile: thanks in advance!

Hi, everything is on my repo :wink: ngocjohn (Viet Ngoc) · GitHub

hello, when i turn my tv off, im allways getting this error.

Anyone can help?

Thanks in advance
Capture

onthe base template i have:`

base:
  template:
    - settings
    - tilt
    - extra_styles
  variables:
    state_on: >
      [[[ return ['on', 'home', 'cool', 'fan_only', 'playing', 'unlocked'].indexOf(!entity || entity.state) !== -1; ]]]
    state: >
      [[[ return !entity || entity.state; ]]]
    entity_id: >
      [[[ return !entity || entity.entity_id; ]]]
    entity_picture: >
      [[[ return !entity || entity.attributes.entity_picture; ]]]
    timeout: >
      [[[ return !entity || Date.now() - Date.parse(entity.last_changed); ]]]
    is_youtube: >
      [[[
        let is_youtube = entity?.attributes?.app_id === 'com.google.ios.youtube',
            sensor = this?._config?.triggers_update,
            media_title = entity?.attributes?.media_title,
            watching_title = states[sensor]?.attributes?.title;
        if (is_youtube && media_title === watching_title) {
            return true;
        }
      ]]]
  aspect_ratio: 1/1
  show_state: true
  show_icon: false
  state_display: >
    [[[
      const stateDict = {
        'on': variables.translate_on,
        'off': variables.translate_off,
        'cool': variables.translate_cool,
        'fan_only': variables.translate_fan_only,
      };
      if (variables.state === true) return variables.translate_unknown;
      return stateDict[variables.state];
    ]]]
  tap_action:
    ui_sound_tablet: |
      [[[
        let screensaver = states[variables.entity_tablet] === undefined ||
            states[variables.entity_tablet].state;

        if (variables.state === 'off' && screensaver === 'off') {
            hass.callService('media_player', 'play_media', {
                entity_id: variables.entity_browser_mod,
                media_content_id: '/local/sound/on.m4a',
                media_content_type: 'music'
            });
        }
        if (variables.state_on && screensaver === 'off') {
            hass.callService('media_player', 'play_media', {
                entity_id: variables.entity_browser_mod,
                media_content_id: '/local/sound/off.m4a',
                media_content_type: 'music'
            });
        }
      ]]]
    card_bounce: |
      [[[
        // add animation
        if (this.getElementsByTagName("style").length === 0) {

            // phone condition
            let mq = window.matchMedia('(max-width: 800px)').matches;

            let style = document.createElement('style');

            style.innerHTML = `
                @keyframes card_bounce {
                    0%   { transform: scale(1); }
                    10%  { transform: scale(${ mq ? '0.92' : '0.94' }); }
                    25%  { transform: scale(1); }
                    30%  { transform: scale(${ mq ? '0.96' : '0.98' }); }
                    100% { transform: scale(1); }
                }
            `;

            this.appendChild(style);
        }

        // duration
        let duration = 800;

        // animate
        this.style.animation = `card_bounce ${duration}ms cubic-bezier(0.22, 1, 0.36, 1)`;

        // reset
        window.setTimeout(() => { this.style.animation = "none"; }, duration + 100)
      ]]]
    action: toggle
    haptic: medium
  double_tap_action:
    haptic: success
  hold_action:
    action: block
  styles:
    grid:
      - grid-template-areas: |
          "icon  circle"
          "n     n"
          "s     s"
      - grid-template-columns: repeat(2, 1fr)
      - grid-template-rows: auto repeat(2, min-content)
      - gap: 1.3%
      - align-items: start
      - will-change: transform
    name:
      - justify-self: start
      - line-height: 121%
    state:
      - justify-self: start
      - line-height: 115%
    card:
      - border-radius: var(--button-card-border-radius)
      - border-width: 0
      - -webkit-tap-highlight-color: rgba(0,0,0,0)
      - transition: none
      - --mdc-ripple-color: >
          [[[
            return variables.state_on
                ? 'rgb(0, 0, 0)'
                : '#97989c';
          ]]]
      - color: >
          [[[
            return variables.state_on
                ? '#4b5254'
                : '#97989c';
          ]]]
      - background-color: >
          [[[
            return variables.state_on
                ? 'rgba(255, 255, 255, 0.85)'
                : 'rgba(115, 115, 115, 0.25)';
          ]]]

and int the button:

`          - type: custom:button-card
            entity: media_player.lg_webos_smart_tv
            name: Tv
            state_display: >
              [[[
                if (variables.state === 'playing') {
                    return 'Playing';
                }
                if (variables.state === true) {
                    return variables.translate_unknown;
                }
              ]]]
  #          double_tap_action: !include popup/sala_tv.yaml
            template:
              - base
              - icon_tv`

Will you share your “gallery” function when no media played? Thx

sure. Install gallery_image over hacs.
ui-lovelace.yaml

     #################################################
     #                                               #
     #                     MEDIA                     #
     #                                               #
     #################################################

      - type: grid
        title: Medien
        view_layout:
          grid-area: media
        columns: 1
        cards:

          - type: custom:swipe-card
            parameters:
              touchStartPreventDefault: false
              roundLengths: true
              speed: 550
              spaceBetween: 40
              threshold: 5
              autoHeight: false
              initialSlide: 0
              centeredSlides: true
              slidesPerView: auto
#              effect: cube              loop: false
              preventClicksPropagation: true
              preventClicks: true
              pagination:
                type: bullets
            cards:

              - type: horizontal-stack
                cards:

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: gallery
                    card: 
                      type: custom:gallery-card
                      entities:
                        - sensor.gallery_images
                      menu_alignment: hidden
                      slideshow_timer: 5
                      card_mod:
                        style: |
                          ha-card {
                            border-radius: calc(var(--custom-button-card-border-radius) / 2);  /* card - rounded corners */
                            aspect-ratio: 1/1;  /* card - square */
                          }
                          figure {
                            margin: 0px !important;  /* remove card margins to line up with rest of dashboard */
                          }
                          figcaption {
                            display: none;  /* hide image caption */
                          }
                          img {
                            object-fit: cover !important;  /* fill the whole card */
                            aspect-ratio: 1/1;  /* needed for object-fit */
                          }
                          .btn {
                            top: 50% !important;  /* center buttons */
                          }
                          .modal-content {
                            aspect-ratio: unset;  /* undo image aspect-ratio when clicked */
                          }

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: TV
                    card:
                      type: custom:button-card
                      entity: media_player.tv_wohnzimmer_2
                      template: [conditional_media, progress_bar]

configuration.yaml

  - platform: files
    folder: /config/www/img
    filter: '**/*.jpg'
    name: gallery_images
    sort: date
    recursive: True

tv_media.yaml

template:
  - select:
      - name: conditional_media
        state: >
          {% set recently_added = 'gallery' %}
          {% set recently_added_backup = 'Recently Added (Offline)' %}
          {% set paused_timeout_minutes = 15 %}
          {% set media_players = [
            states.media_player.tv_wohnzimmer_2,
            states.media_player.tv_schlafzimmer_2,
            states.media_player.vardagsrum,
            states.media_player.lea,
            states.sensor.ps5_101_activity,
            states.media_player.wohnzimmer_oben,
            states.media_player.schlafzimmer,
            states.media_player.buro,
            states.media_player.maja,
            states.media_player.kuche,
            states.media_player.fernseher_maja_2 ] %}

          {% macro media(state) %}
          {% set state = media_players | selectattr('state','eq',state) | list %}
          {% set last_changed = recently_added if state | length == 0 else state | map(attribute='last_changed') | list | max %}
            {{ state | selectattr('last_changed','eq', last_changed) | map(attribute='name') | list | join }}
          {% endmacro %}

          {% set recently_added = recently_added_backup if is_state('sensor.recently_added_offline','Active') else recently_added %}

          {% set playing = media_players | selectattr('state','eq','playing') | list %}
          {% set timeout_playing = False if on | length == 0 else
            (as_timestamp(now()) - as_timestamp(on | map(attribute='last_changed') | list | max)) < paused_timeout_minutes * 60 %}
                
          {% set paused = media_players | selectattr('state','eq','paused') | list %}
          {% set timeout_paused = False if paused | length == 0 else
            (as_timestamp(now()) - as_timestamp(paused | map(attribute='last_changed') | list | max)) < paused_timeout_minutes * 60 %}

          {% if playing %}
            {{ media('playing') if timeout_playing else media('paused') if timeout_paused else media('playing') }}
          {% elif paused %}
            {{ media('paused') if timeout_paused else recently_added }}
          {% else %}
            {{ recently_added }}
          {% endif %}
        options: >
          {% set recently_added = ['gallery'] %}
          {% set recently_added_backup = ['Recently Added (Offline)'] %}
          {% set media_players = [
            states.media_player.tv_wohnzimmer_2,
            states.media_player.vardagsrum,
            states.media_player.lea,
            states.media_player.tv_schlafzimmer_2,
            states.sensor.ps5_101_activity,
            states.media_player.wohnzimmer_oben,
            states.media_player.schlafzimmer,
            states.media_player.buro,
            states.media_player.maja,
            states.media_player.kuche,
            states.media_player.fernseher_maja_2 ] %}
          {{ recently_added + recently_added_backup + media_players | map(attribute='name') | list }}
        select_option:
          service: select.select_option
          target:
            entity_id: select.conditional_media
          data:
            option: >
              {{ option }}

I hope that I don´t forget something

1 Like

How do you get that green glow around the buttons?
Thanks

Its just some box-shadow styling, based on state, that I added to the button card template that I use for those particular button cards:

card:
      - border-radius: var(--button-card-border-radius)
      - border-width: 0
      - -webkit-tap-highlight-color: rgba(0,0,0,0)
      - transition: none
      - --mdc-ripple-color: >
          [[[
            return variables.state_on
                ? 'rgb(0, 0, 0)'
                : '#97989c';
          ]]]
      - color: >
          [[[
            return variables.state_on
                ? '#4b5254'
                : '#97989c';
          ]]]
      - background-color: >
          [[[
            return variables.state_on
                ? 'rgba(255, 255, 255, 0.85)'
                : 'rgba(115, 115, 115, 0.25)';
          ]]]
      - box-shadow: >
          [[[
            return variables.state_on
                ? '0px 0px 10px 3px red'
                : '0px 0px 10px 3px green';
          ]]]
1 Like

This is great!
Unfortunately, I’m unable to make it work in several grids at the same time; only the first one is showing the values.
E.g., below an example of my dashboard where I have one grid per room and I’d like to have the temperature and humidity in each title. However, only the first one shows. Is this expected or is there any way to make it work like this?


The goal is to eliminate those 4 cards and avoid having to swipe whenever I need to turn on a light using the wallpanel

1 Like

Hi,

I adapt the code template from VietNgoc to the following:
wz3

Font Size and Icon Size can adapt to your needs.
Now you can show on all grifs: Temperature, Humidity, Position of a thermostat valve (or your lux sensor) and motion.

For example:

                      tempsensor: sensor.buero2_temperature
                      humidsensor: sensor.buero_position
                      positionSensor: sensor.buero_position
                      motionSensor: binary_sensor.bewegungsmelder_treppe
                      fontSize: 0.8rem
                      motionIconSize: 15px

when there is no sensor defined then nothing will be shown. I hope that works.

change_grid_title:
  state_display: >
    [[[
      function findFirstH1Above(element) {
        let currentElement = element;
        while (currentElement) {
          if (currentElement.shadowRoot) {
            const shadowH1 = currentElement.shadowRoot.querySelector('h1');
            if (shadowH1) return shadowH1;
          }
          const lightH1 = currentElement.querySelector('h1');
          if (lightH1) return lightH1;
          currentElement = currentElement.getRootNode().host;
          if (!currentElement) break;
        }
        return null;
      }
    
      let parentElement = this.getRootNode().host;
      let headerTitle = findFirstH1Above(parentElement);
    
      if (headerTitle && !headerTitle.dataset.processed) {
        const tempSensor = parseFloat(states[variables.tempsensor]?.state);
        const humidSensor = parseFloat(states[variables.humidsensor]?.state);
        const positionSensor = parseFloat(states[variables.positionSensor]?.state);
        const motionState = states[variables.motionSensor]?.state;
        const motionIcon = motionState === 'on' ? 'mdi:motion-sensor' : 'mdi:motion-sensor-off';
        const fontSize = variables.fontSize || '14px';
        const motionIconSize = variables.motionIconSize || '20px';
        let currentTitle = headerTitle.innerText;
        headerTitle.style.display = 'flex';
        headerTitle.style.width = '100%';
        headerTitle.style.justifyContent = 'space-between';
        headerTitle.style.alignItems = 'center';
        headerTitle.style.paddingInline = 'inherit';
        const formattedTemp = !isNaN(tempSensor) ? 
            (tempSensor % 1 === 0 ? tempSensor.toFixed(0) : tempSensor.toFixed(1)).replace('.', ',') + '°C' : '';
        const formattedHumid = !isNaN(humidSensor) ? humidSensor.toFixed(0) + '%' : '';
        const formattedPosition = !isNaN(positionSensor) ? positionSensor.toFixed(0) + '%' : '';
        let sensorValues = [];
        if (formattedTemp) sensorValues.push(formattedTemp);
        if (formattedHumid) sensorValues.push(formattedHumid);
        if (formattedPosition) sensorValues.push(formattedPosition);
        const sensorDisplay = sensorValues.join(' / ');
        const motionIconHTML = states[variables.motionSensor] 
          ? `<ha-icon icon="${motionIcon}" style="margin-left: 5px; --mdc-icon-size: ${motionIconSize};"></ha-icon>`
          : '';
        headerTitle.innerHTML = `
          <div>${currentTitle}</div>
          <div style="font-size: ${fontSize}; display: flex; align-items: center;">
            ${sensorDisplay}
            ${motionIconHTML}
          </div>
        `;
        headerTitle.dataset.processed = true;
      }
    ]]]


best, Michael

1 Like

Hi,

I have an issue with padding value of the grid header and I don´t find where I can change it without installing car-mod. For faster page visivility on older tablets I would like to use it without card-mod:

1h

The green marked padding are should be smaller.

Thanks,
Michael

I think there is no other way to interfere with css without card-mod. Because this is a default lovelace grid card from HA, not custom

thanks for your answer. that’s a pity. Is there any chance to change the source code of lovelace or whereever these .card-header element comes from. Even under the risk that I need to do it again after any update.

Does anyone have an idea how to scale the sidebar relative to the right side? Now the sidebar is too big and that is at the expense of the rest of the display.