A different take on designing a Lovelace UI

hexadecimal
{{ 0x680ae2fffe82bf8b == "0x680ae2fffe82bf8b" }}
// False

untested but try this

{% set lights = [
  states.switch['0x680ae2fffe82bf8b'],
  states.switch['0x842e14fffe7f2dd9']
] %}
2 Likes

Once again, you are my hero :slight_smile:
Thanks !

I have two things i need help with.

  1. I cannot get the cards to look right. The icons and text doesn’t line up correctly. See picture below

  2. I cannot get the packground image to show. Where do i add background image?

I have removed extra styles (extra_styles) to resolve problem.

1 Like

Have you followed all the steps outlined in the installation instructions? hass-config/INSTALL.md at 132cbbd2f8c4b29f4c100cf9a4e99327094049a9 · matt8707/hass-config · GitHub

Given just the screenshot I can’t tell what the issue is.

In the www folder see hass-config/www at master · matt8707/hass-config · GitHub

Did you have a fix for this issue? Initially my loader.svg image was missing but is is definitly in the folder now. But still have the same issue as you have or had.

Would be great to learn if you fixed it and in which way.

Or to the others, how to fix the issue that the loader.svg is not popping up in the cards.
It shows a missing picture icon, whilst loader.svg is placed in the www folder.

Missing picture

Hi. Did you find how to fix this ? I have the same problem

Did you reload the dashboard page after loading the loader.svg?
I had the same issue and I just copied all the images in the www folder from the GitHub to my HA folder. After reloading the page (and closing and reopening the app on the phone) the load animation started working correctly on both.

Somehow it started working today out of the blue, I restared home assistant, reload the dashboard on the browser aswell. But at that time didn’t work.

So it is fixed now :slight_smile:

No. Sorry didn’t have enough time ;-(

Has someone added a progressbar for the duration of a actually playing movie in the Media card?

Can anyone tell me why I can’t go further to the left? Or what I have to change so that I can get further to the left?

- type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: Kueche
                    card:
                      type: custom:button-card
                      template:
                        - conditional_media
                        - icon_apple_tv
                      entity: media_player.kueche
                      custom_fields:
                        progress:
                          card:
                            type: custom:bar-card
                            height: 0.4em
                            entities:
                              - entity: sensor.appletv_kueche_progress
                            positions:
                              icon: 'off'
                              indicator: 'off'
                              name: 'off'
                              value: 'off'
                            color: >
                              [[[ return 'var(--primary-color)'; ]]]
                            max: '100'
                            min: '0'
                            target: '0'
                            decimal: '2'
                            unit_of_measurement: ' '
                            severity:
                              - hide: true
                               
                      styles:
                       custom_fields:
                          progress:
                            - position: absolute
                            - width: 100%
                            - height: 100%
                            - margin: 70% 10% 0 0
                            - display: initial
                            - opacity: 1
                            - justify-self: end          
                      ```
1 Like

I’ve been trying to figure out the PS5 integration! Would you mind sharing your code?

Add sensor.ps5_activity as if it was a media player in packages/tv_media.yaml

To get the other info you’ll need to add some checks in button_card_templates/media.yaml for entity.attributes.title_image and entity.attributes.title_name.

I edited my file pretty heavily so I just applied the edits to the original one. It should work, but let me know if it doesn’t.

button_card_templates/media.yaml

#################################################
#                                               #
#                  BASE MEDIA                   #
#                                               #
#################################################

base_media:
  variables:
    media_on: >
      [[[ return !entity || ['playing', 'paused'].indexOf(entity.state) !== -1; ]]]
    media_off: >
      [[[ return !entity || ['off', 'idle', 'standby', 'unknown', 'unavailable', 'none'].indexOf(entity.state) !== -1; ]]]
  tap_action:
    action: >
      [[[
        return variables.media_on
            ? 'call-service'
            : 'none';
      ]]]
    service: media_player.media_play_pause
    service_data:
      entity_id: >
        [[[
          return variables.entity_id;
        ]]]
  double_tap_action:
    action: more-info
  styles:
    card:
      - color: >
          [[[
            let game_image = entity.attributes.title_image;
            if (variables.is_youtube) {
                return `#efefef`;
            } else {
                return entity && game_image !== undefined
                  ? '#efefef'
                  : variables.media_on && variables.entity_picture === undefined
                      ? 'rgba(0, 0, 0, 0.6)'
                      : variables.media_off
                          ? '#97989c'
                          : '#efefef';
            }
          ]]]
      - text-shadow: >
          [[[
            let game_image = entity.attributes.title_image;
            if (variables.is_youtube) {
                return `1px 1px 5px rgba(18, 22, 23, 0.9)`;
            } else {
                return entity && game_image !== undefined
                  ? '1px 1px 5px rgba(18, 22, 23, 0.9)'
                  : variables.media_off || variables.entity_picture === undefined
                      ? 'none'
                      : '1px 1px 5px rgba(18, 22, 23, 0.9)';
            }
          ]]]

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

media:
  template:
    - base
    - base_media
  styles:
    custom_fields:
      icon:
        - width: 70%
        - margin-left: 2%
        - fill: '#9da0a2'
        - display: >
            [[[
              let game_image = entity.attributes.title_image;
              if (variables.is_youtube) {
                  return `none`;
              }
              else {
                  return entity && game_image !== undefined
                    ? 'none'
                    : variables.media_off || variables.entity_picture === undefined
                        ? 'initial'
                        : 'none';
              }
            ]]]
    card:
      - background-color: none
      - background-size: cover
      - background-position: center
      - background-image: >
          [[[
            let game_image = entity.attributes.title_image;
            if (variables.is_youtube) {
                return `linear-gradient(0deg, rgba(0,0,0,.8) 0%, rgba(0,0,0,0) 100%), url(${states[this._config?.triggers_update].state})`;
            } else {
                return entity && game_image !== undefined
                  ? `linear-gradient(0deg, rgba(0,0,0,.8) 0%, rgba(0,0,0,0) 100%), url(${game_image})`
                  : variables.media_on && variables.entity_picture === undefined
                      ? 'linear-gradient(0deg, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0.8) 100%)'
                      : variables.media_off
                          ? 'linear-gradient(0deg, rgba(115, 115, 115, 0.2) 0%, rgba(115, 115, 115, 0.2) 100%)'
                          : `linear-gradient(0deg, rgba(0,0,0,.8) 0%, rgba(0,0,0,0) 100%), url(${variables.entity_picture})`;
            }
          ]]]

#################################################
#                                               #
#               CONDITIONAL MEDIA               #
#                                               #
#################################################

conditional_media:
  aspect_ratio: 1000/996
  template:
    - base
    - base_media
    - icon_play_pause
  variables:
    i: >
      [[[
        if (entity) {
            let data = entity.attributes.data;
            return data === undefined || Math.floor(Math.random() * (data.length - 1)) + 1;
        }
      ]]]
  state_display: >
    [[[
      let horizontalStack = this.getRootNode().host,
          swipeCard = horizontalStack.getRootNode().host,
          gridTitle = swipeCard.getRootNode().querySelector("h1");

      swipeCard.swiper.on("slideChange", () => {
          if (swipeCard.swiper.realIndex === 0) {
              gridTitle.textContent = "Media";
          }
          else if (swipeCard.swiper.realIndex === 1) {
              gridTitle.textContent = "Spelare";
          }
      });

      if (entity) {
          let elt = this.shadowRoot,
              await = setTimeout(marquee, 100),
              data = entity.attributes.data,
              artist = entity.attributes.media_artist,
              title = entity.attributes.media_title,
              game_title = entity.attributes.title_name;

            if (data !== undefined) {
                var number = data[variables.i].number === undefined && data[variables.i].aired !== undefined
                    ? `(${data[variables.i].aired.split("-")[0]})`
                    : data[variables.i].number === undefined && data[variables.i].aired === undefined
                        ? ' '
                        : data[variables.i].number,
                output = `${data[variables.i].title} ${number}`;
            } else {
                var output = artist === undefined && title !== undefined
                    ? title
                    : game_title !== undefined
                      ? `${game_title}`
                      : title === undefined && artist !== undefined
                          ? artist
                          : title !== undefined && artist !== undefined
                              ? `${artist} - ${title}`
                              : variables.translate_idle;
            }

          function marquee() {
              let state = elt.getElementById("state"),
                  container = elt.getElementById("container");

              if (state && container) {
                  state.innerHTML = output;
                  let ro = new ResizeObserver(entries => {
                      let spacer = " ".repeat(3),
                          s = entries[0],
                          c = entries[1],
                          r = s && s.contentRect &&
                              c && c.contentRect &&
                              s.contentRect.width !== 0 &&
                              c.contentRect.width !== 0;

                      if (r && s.contentRect.width < c.contentRect.width) {
                          state.classList.remove("marquee");
                      }
                      else if (r && s.contentRect.width >= c.contentRect.width) {
                          state.innerHTML = `${output} ${spacer} ${output} ${spacer}&nbsp;`;
                          state.classList.add("marquee");
                      }
                  });
                  ro.observe(state);
                  ro.observe(container);
              }
          }
          return output;
      }
      return variables.translate_unknown;
    ]]]
  tap_action:
    action: call-service
    service: media_player.media_play_pause
    service_data:
      entity_id: >
        [[[ return variables.entity_id; ]]]
  styles:
    grid:
      - gap: 0.65%
    name:
      - padding: 0.2vw
      - margin: -0.2vw
    state:
      - padding-bottom: 5.25%
      - max-width: unset
      - overflow: visible
    card:
      - padding: 5.75% 5.25% 0 5.75%
      - border-radius: calc(var(--button-card-border-radius) / 2)
      - background: rgba(115, 115, 115, 0.2) center center/cover no-repeat
      - background-image: &media_background_image >
          [[[
            if (entity) {
              if (variables.is_youtube) {
                  return `url(${states[this._config?.triggers_update].state})`;
              } else {
                let data = entity.attributes.data,
                game_image = entity.attributes.title_image;
                return game_image !== undefined
                  ? `url("${game_image}")`
                  : data && (data[variables.i].fanart || data[variables.i].poster)
                      ? `url("${data[variables.i].fanart}"), url("${data[variables.i].poster}")`
                      : `url("${variables.entity_picture}")`;
              }
            }
          ]]]
      - color: >
          [[[
            return entity === undefined
                ? '#97989c'
                : '#efefef';
          ]]]
      - text-shadow: >
          [[[
            return entity === undefined
                ? 'none'
                : '1px 1px 5px rgba(18, 22, 23, 0.9)';
          ]]]
    custom_fields:
      icon:
        - width: 30%
        - fill: >
            [[[
              return entity && variables.media_on
                  ? 'rgba(255, 255, 255, 0.8)'
                  : '#9da0a2';
            ]]]
      blur_overlay:
        - display: block
        - position: absolute
        - width: 103.1%
        - height: 103.1%
        - filter: var(--blur-intensity)
        - clip-path: >
            inset(74.5% 1.45% 1.45% 1.45% round 0 0 calc(var(--button-card-border-radius) / 2) calc(var(--button-card-border-radius) / 2))
        - background: center center/cover no-repeat
        - background-image: *media_background_image
        - left: -1.5%
        - bottom: -1.6%
  custom_fields:
    blur_overlay: >
      [[[
        setTimeout(() => {
            let elt = this.shadowRoot,
                card = elt.getElementById('card'),
                container = elt.getElementById('container'),
                blur_overlay = elt.getElementById('blur_overlay');

            if (elt && card && container && blur_overlay) {
                card.insertBefore(blur_overlay, container);
            }
          }, 0);
        return ' ';
      ]]]

Then you’ll also need to add sensor.ps5_activity as if it was a media player in ui-lovelace.yaml (this is the media section only and you only need to add the PlayStation bits)

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

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

          - type: custom:swipe-card
            parameters:
              speed: 550
              spaceBetween: 40
              threshold: 5
            cards:

              - type: horizontal-stack
                cards:

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state_not: Shield TV

                      - entity: select.conditional_media
                        state_not: Master Bedroom TV

                      - entity: select.conditional_media
                        state_not: PS5 Activity

                      - entity: select.conditional_media
                        state_not: Garage TV

                      - entity: select.conditional_media
                        state_not: Upstairs TV

                    card:
                      type: custom:button-card
                      entity: sensor.plex_recently_added
                      name: Recently Added
                      tap_action:
                        action: none
                      template:
                        - conditional_media
                        - icon_plex

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: Shield TV
                    card:
                      type: custom:button-card
                      entity: media_player.shield_tv_plex
                      triggers_update: sensor.youtube_watching
                      template:
                        - conditional_media
                        - icon_shield_tv

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: Master Bedroom TV
                    card:
                      type: custom:button-card
                      entity: media_player.master_bedroom_tv_plex
                      triggers_update: sensor.youtube_watching
                      template:
                        - conditional_media
                        - icon_google_tv

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: Upstairs TV
                    card:
                      type: custom:button-card
                      entity: media_player.upstairs_tv_plex
                      triggers_update: sensor.youtube_watching
                      template:
                        - conditional_media
                        - icon_google_tv

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: PS5 Activity
                    card:
                      type: custom:button-card
                      entity: sensor.ps5_activity
                      template:
                        - conditional_media
                        - icon_playstation

                  - type: conditional
                    conditions:
                      - entity: select.conditional_media
                        state: Garage TV
                    card:
                      type: custom:button-card
                      entity: media_player.garage_tv_plex
                      template:
                        - conditional_media
                        - icon_google_tv

              - type: grid
                columns: 2
                cards:

                  - type: custom:button-card
                    entity: media_player.shield_tv_plex
                    triggers_update: sensor.youtube_watching
                    name: Shield TV
                    template:
                      - media
                      - icon_shield_tv

                  - type: custom:button-card
                    entity: media_player.master_bedroom_tv_plex
                    triggers_update: sensor.youtube_watching
                    name: Master Bedroom TV
                    template:
                      - media
                      - icon_google_tv

                  - type: custom:button-card
                    entity: sensor.ps5_activity
                    name: PS5 Activity
                    template:
                      - media
                      - icon_playstation

                  - type: custom:button-card
                    entity: media_player.garage_tv_plex
                    name: Garage TV
                    template:
                      - media
                      - icon_google_tv

              - type: grid
                columns: 2
                cards:

                  - type: custom:button-card
                    entity: media_player.upstairs_tv_plex
                    triggers_update: sensor.youtube_watching
                    name: Upstairs TV
                    template:
                      - media
                      - icon_google_tv

and I made a PlayStation icon to match the other media player icons.
button_card_templates/icons.yaml

icon_playstation:
  custom_fields:
    icon: >
      <svg viewBox="0 0 50 50">
        <path d="M49.5,11.8l-0.2-2.4c-0.5-2.7-2.1-4.9-4.2-6.6c-1.9-1.6-4.1-2.4-6.7-2.4L27.5,0.3L11,0.4c-0.5,0-0.9,0-1.4,0.1 C7.8,0.8,6.2,1.6,4.8,2.7C1.9,5,0.5,7.9,0.4,11.5v26.7c0.1,1.1,0.1,2,0.3,3C1.4,43.8,3,45.8,5,47.4c1.9,1.4,4.1,2.2,6.5,2.2 l13.7,0.1c0,0,13.3-0.1,13.4-0.2c0.7,0,1.3-0.1,1.9-0.2c2.8-0.6,5.1-2.2,6.8-4.4c1.3-1.7,2.1-3.7,2.2-5.9l0.1-6.7L49.5,11.8z M19.2,29.1l-6.2,2.2c-1.1,0.4-1.3,0.9-0.4,1.2c0.9,0.3,2.5,0.2,3.6-0.2l3-1.1v3.1c-0.2,0-0.4,0.1-0.6,0.1c-3,0.5-6.1,0.3-9.2-0.7 c-2.9-0.8-3.4-2.5-2.1-3.5c1.2-0.9,3.3-1.6,3.3-1.6l8.5-3V29.1z M26.3,15.9V38l-6-1.9V9.7c2.5,0.5,6.2,1.6,8.2,2.3 c5,1.7,6.8,3.9,6.8,8.8c0,4.7-2.9,6.5-6.6,4.7v-8.8c0-1-0.2-2-1.2-2.3C26.8,14.1,26.3,14.8,26.3,15.9z M43,31.3 c-0.7,0.9-2.5,1.5-2.5,1.5l-13,4.7v-3.5l9.6-3.4c1.1-0.4,1.3-0.9,0.4-1.2c-0.9-0.3-2.5-0.2-3.6,0.2l-6.4,2.3v-3.6l0.4-0.1 c0,0,1.8-0.7,4.4-0.9c2.6-0.3,5.8,0,8.3,1C43.4,29.1,43.7,30.4,43,31.3z"/>
      </svg>

It should look something like this
image

EDIT: edited base media variables to include ‘none’ in media_off

    media_off: >
      [[[ return !entity || ['off', 'idle', 'standby', 'unknown', 'unavailable', 'none'].indexOf(entity.state) !== -1; ]]]

EDIT2: line 95 media.yaml

            let game_image = entity.attributes.title_image;
7 Likes

okay i get it to work,

but when i use my mobile or tablet it the progress moves a little bit to the right and top. But i use % as margin values.

here on my phone

Unbenannt1

i updatet the grid

styles:
                        grid:
                          - grid-template-areas: |
                              "icon  circle"
                              "progress  progress"
                              "n     n"
                              "s     s"
                          - grid-template-columns: repeat(2, 1fr)
                          - grid-template-rows: auto repeat(3, min-content)
                          - gap: 1.3%
                          - align-items: start

and added the #progress to the extra_styles

extra_styles: >
      [[[
        if (entity) {
          let hs = entity.attributes.hs_color === undefined,
            h = hs || entity.attributes.hs_color[0],
            s = hs || entity.attributes.hs_color[1],
            l_min = 28,
            l_max = 48,
            l_calc = entity.attributes.brightness / 2.54 * (l_max - l_min) / 100 + l_min;
          var light_color = entity.attributes.color_mode === 'color_temp' || entity.attributes.color_mode === 'brightness'
            ? `hsl(204, 58%, ${l_calc}%);`
            : `hsl(${h}, ${s}%, ${l_calc}%);`;
        }

        return `
          svg {
            --light-color:
            ${ variables.state_on && entity.attributes.brightness !== undefined
                ? light_color
                : variables.state_on && entity.attributes.brightness === undefined
                  ? 'var(--state-icon-active-color);'
                  : 'var(--state-icon-color);'
            }
          }
          #container {
            text-align: left !important;
          }
          #name, #state {
            font-size: 1.32vw;
            letter-spacing: -0.02vw;
          }
          #state::first-letter {
            text-transform: uppercase;
          }
          /* portrait */
          @media screen and (max-width: 1200px) {
            #name, #state {
              font-size: 2vw;
            }
          }
          /* phone */
          @media screen and (max-width: 800px) {
            #name, #state {
              font-size: 3vw;
            }
          }

          /* tilt */
          #ripple, .js-tilt-glare {
            clip-path: inset(0 round var(--custom-button-card-border-radius));
            overflow: hidden;
          }
          .js-tilt-glare {
            z-index: 1;
          }
          .js-tilt-glare-inner {
            background-color: rgba(0,0,0,0.9);
          }
          #container {
            transform: translateZ(${variables.tilt_options.parallax});
          }
          #card {
            transform-style: preserve-3d;
            overflow: visible;
          }

          ${this._config.template.includes('conditional_media') ? `
            :host {
              --blur-intensity: blur(4.5px) brightness(0.8);
            }
            /* phone */
            @media screen and (max-width: 800px) {
              :host {
                --blur-intensity: blur(2.5px) brightness(0.8);
              }
            }
            #ripple, .js-tilt-glare {
              clip-path: inset(0 round calc(var(--custom-button-card-border-radius) / 2));
            }
            #container {
              overflow: hidden;
            }
            .marquee {
              animation: marquee 20s linear infinite;
            }
            @keyframes marquee {
              from {
                transform: translateX(0%);
              }
              to {
                transform: translateX(-50%);
              }
            }
          `:''}

          ${this._config.template.includes('footer') ? `
            :host {
              --name-font-size: 1.22vw;
              --name-icon-size: 1.5vw;
              --notify-font-size: 0.9vw;
              --notify-box-size: 1.8vw;
              --name-padding-v: 0.7vw;
              --name-padding-h: 1.1vw;
              --progress-padding-v: 0.7vw;
              --progress-padding-h: 1.1vw;
              --card-border-radius: 0.6vw;
            }
            #ripple, .js-tilt-glare {
              border-radius: calc(var(--card-border-radius) - 0.1vw);
              clip-path: inset(0 round calc( var(--custom-button-card-border-radius) - 0.1vw ));
            }
            #progress {
              padding: var(--progress-padding-v) var(--progress-padding-h);
            }
            #name {
              font-size: var(--name-font-size);
              padding: var(--name-padding-v) var(--name-padding-h);
              letter-spacing: 0.012vw;
            }
            ha-icon {
              width: var(--name-icon-size);
              vertical-align: 7%;
              padding-right: 0.1vw;
              opacity: 0.4;
            }
            #card {
              border-radius: var(--card-border-radius);
              background: rgba(115, 115, 115, 0.04);
            }
            #notify {
              font-size: var(--notify-font-size);
              width: var(--notify-box-size);
              height: var(--notify-box-size);
              line-height: var(--notify-box-size);
              padding-right: 0.5px;
              padding-top: 0.5px;
            }
            /* portrait */
            @media screen and (max-width: 1200px) {
              #name {
                font-size: calc(var(--name-font-size) * 1.4);
                padding: calc(var(--name-padding-v) * 1.4) calc(var(--name-padding-h) * 1.4);
              }
              #progress {
                padding: calc(var(--progress-padding-v) * 1.4) calc(var(--progress-padding-h) * 1.4);
              }
              ha-icon {
                width: calc(var(--name-icon-size) * 1.4);
              }
              #card {
                border-radius: calc(var(--card-border-radius) * 1.4);
                margin: 0 0.5vw;
              }
              #notify {
                font-size: calc(var(--notify-font-size) * 1.4);
                width: calc(var(--notify-box-size) * 1.4);
                height: calc(var(--notify-box-size) * 1.4);
                line-height: calc(var(--notify-box-size) * 1.4);
              }
            }
            /* phone */
            @media screen and (max-width: 800px) {
              #name {
                font-size: calc(var(--name-font-size) * 2.7);
                padding: calc(var(--name-padding-v) * 2.7) calc(var(--name-padding-h) * 2.7);
                letter-spacing: 0.12vw;
              }
              #progress {
                margin: calc(var(--progress-padding-v) * 2.7) calc(var(--progress-padding-h) * 2.7);
              }
              ha-icon {
                width: calc(var(--name-icon-size) * 2.7);
              }
              #card {
                border-radius: calc(var(--card-border-radius) * 2.7);
                background: rgba(115, 115, 115, 0.08);
                margin: 0 0.5vw;
              }
              #notify {
                font-size: calc(var(--notify-font-size) * 2.7);
                width: calc(var(--notify-box-size) * 2.7);
                height: calc(var(--notify-box-size) * 2.7);
                line-height: calc(var(--notify-box-size) * 2.7);
                padding: 0;
              }
            }
          `:''}
        `
      ]]]

but still no change, any ideas perhaps`?

I updated browser-mod today now double tap isn’t working. Is that a known issue - I didn’t see anything in the docs?

Edit: required a cache clear to fix.

Thanks for sharing your code! I copied your code over to mine exactly, but now I am now seeing the below javascript error on all media button cards:

ButtonCardJSTemplateError: ReferenceError: game_image is not defined in ‘game_image = entity.attributes.title_image;
if (variables.is_youtube) {
return `linear-gra…’

I must be missing something. Also verified that the PS5 integration is active:

1 Like

My bad. media.yaml line 95 add let in front of game_image = entity.attributes.title_image;

...
      - background-image: >
          [[[
            let game_image = entity.attributes.title_image;
            if (variables.is_youtube) {
                return `linear-gradient(0deg, rgba(0,0,0,.8) 0%, rgba(0,0,0,0) 100%), url(${states[this._config?.triggers_update].state})`;
            }
...
2 Likes

how did you create this sensor?