31 |
32 | #### Compact card
33 |
34 |
35 | #### Card with media shortcuts
36 |
37 |
38 | #### Grouped cards
39 |
40 |
41 | #### Stacked cards
42 |
43 |
44 | #### Speaker group management
45 |
46 |
47 | **Check the repository for card options & example configurations**
--------------------------------------------------------------------------------
/src/utils/colorGenerator.js:
--------------------------------------------------------------------------------
1 | import Vibrant from 'node-vibrant/dist/vibrant';
2 |
3 | import { CONTRAST_RATIO, COLOR_SIMILARITY_THRESHOLD } from '../const';
4 |
5 | const luminance = (r, g, b) => {
6 | const a = [r, g, b].map((v) => {
7 | let w = v;
8 | w /= 255;
9 | return w <= 0.03928 ? w / 12.92 : ((w + 0.055) / 1.055) ** 2.4;
10 | });
11 | return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
12 | };
13 |
14 | const contrast = (rgb1, rgb2) => {
15 | const lum1 = luminance(...rgb1);
16 | const lum2 = luminance(...rgb2);
17 | const brightest = Math.max(lum1, lum2);
18 | const darkest = Math.min(lum1, lum2);
19 | return (brightest + 0.05) / (darkest + 0.05);
20 | };
21 |
22 | const getContrastRatio = (rgb1, rgb2) => Math.round((contrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
23 |
24 | const colorGenerator = (colors) => {
25 | colors.sort((colorA, colorB) => colorB.population - colorA.population);
26 |
27 | const backgroundColor = colors[0];
28 | let foregroundColor;
29 |
30 | const contrastRatios = new Map();
31 | const approvedContrastRatio = (hex, rgb) => {
32 | if (!contrastRatios.has(hex)) {
33 | contrastRatios.set(hex, getContrastRatio(backgroundColor.rgb, rgb));
34 | }
35 |
36 | return contrastRatios.get(hex) > CONTRAST_RATIO;
37 | };
38 |
39 | // We take each next color and find one that has better contrast.
40 | for (let i = 1; i < colors.length && foregroundColor === undefined; i++) {
41 | // If this color matches, score, take it.
42 | if (approvedContrastRatio(colors[i].hex, colors[i].rgb)) {
43 | foregroundColor = colors[i].rgb;
44 | break;
45 | }
46 |
47 | // This color has the wrong contrast ratio, but it is the right color.
48 | // Let's find similar colors that might have the right contrast ratio
49 |
50 | const currentColor = colors[i];
51 |
52 | for (let j = i + 1; j < colors.length; j++) {
53 | const compareColor = colors[j];
54 |
55 | // difference. 0 is same, 765 max difference
56 | const diffScore =
57 | Math.abs(currentColor.rgb[0] - compareColor.rgb[0]) +
58 | Math.abs(currentColor.rgb[1] - compareColor.rgb[1]) +
59 | Math.abs(currentColor.rgb[2] - compareColor.rgb[2]);
60 |
61 | if (diffScore > COLOR_SIMILARITY_THRESHOLD) {
62 | continue;
63 | }
64 |
65 | if (approvedContrastRatio(compareColor.hex, compareColor.rgb)) {
66 | foregroundColor = compareColor.rgb;
67 | break;
68 | }
69 | }
70 | }
71 |
72 | if (foregroundColor === undefined) {
73 | foregroundColor = backgroundColor.getYiq() < 200 ? [255, 255, 255] : [0, 0, 0];
74 | }
75 |
76 | return [new backgroundColor.constructor(foregroundColor, 0).hex, backgroundColor.hex];
77 | };
78 |
79 | Vibrant._pipeline.generator.register('default', colorGenerator);
80 | export default async (picture) => new Vibrant(picture, { colorCount: 16 }).getPalette();
81 |
--------------------------------------------------------------------------------
/src/const.ts:
--------------------------------------------------------------------------------
1 | const DEFAULT_HIDE = {
2 | repeat: true,
3 | shuffle: true,
4 | power_state: true,
5 | artwork_border: true,
6 | icon_state: true,
7 | sound_mode: true,
8 | group_button: false,
9 | runtime: true,
10 | runtime_remaining: true,
11 | volume: false,
12 | volume_level: true,
13 | controls: false,
14 | play_pause: false,
15 | play_stop: true,
16 | prev: false,
17 | next: false,
18 | jump: true,
19 | state_label: false,
20 | progress: false,
21 | icon: false,
22 | name: false,
23 | info: false,
24 | };
25 |
26 | const REPEAT_STATE = {
27 | OFF: 'off',
28 | ALL: 'all',
29 | ONE: 'one',
30 | };
31 |
32 | const ICON = {
33 | DROPDOWN: 'mdi:chevron-down',
34 | GROUP: 'mdi:speaker-multiple',
35 | MENU: 'mdi:menu-down',
36 | MUTE: {
37 | true: 'mdi:volume-off',
38 | false: 'mdi:volume-high',
39 | },
40 | NEXT: 'mdi:skip-next',
41 | PLAY: {
42 | true: 'mdi:pause',
43 | false: 'mdi:play',
44 | },
45 | POWER: 'mdi:power',
46 | PREV: 'mdi:skip-previous',
47 | SEND: 'mdi:send',
48 | SHUFFLE: 'mdi:shuffle',
49 | REPEAT: {
50 | [REPEAT_STATE.OFF]: 'mdi:repeat-off',
51 | [REPEAT_STATE.ONE]: 'mdi:repeat-once',
52 | [REPEAT_STATE.ALL]: 'mdi:repeat',
53 | },
54 | STOP: {
55 | true: 'mdi:stop',
56 | false: 'mdi:play',
57 | },
58 | VOL_DOWN: 'mdi:volume-minus',
59 | VOL_UP: 'mdi:volume-plus',
60 | FAST_FORWARD: 'mdi:fast-forward',
61 | REWIND: 'mdi:rewind',
62 | };
63 |
64 | const UPDATE_PROPS = [
65 | 'entity',
66 | 'groupMgmtEntity',
67 | '_overflow',
68 | 'break',
69 | 'thumbnail',
70 | 'prevThumbnail',
71 | 'edit',
72 | 'idle',
73 | 'cardHeight',
74 | 'backgroundColor',
75 | 'foregroundColor',
76 | ];
77 |
78 | const MEDIA_DURATION_PROP = 'media_duration';
79 |
80 | const PROGRESS_PROPS = [MEDIA_DURATION_PROP, 'media_position', 'media_position_updated_at'];
81 |
82 | const BREAKPOINT = 390;
83 |
84 | const LABEL_SHORTCUT = 'Shortcuts...';
85 |
86 | const MEDIA_INFO = [
87 | { attr: 'media_title' },
88 | { attr: 'media_artist' },
89 | { attr: 'media_series_title' },
90 | { attr: 'media_season', prefix: 'S' },
91 | { attr: 'media_episode', prefix: 'E' },
92 | { attr: 'media_channel' },
93 | { attr: 'app_name' },
94 | ];
95 |
96 | const PLATFORM = {
97 | SONOS: 'sonos',
98 | SQUEEZEBOX: 'squeezebox',
99 | BLUESOUND: 'bluesound',
100 | SOUNDTOUCH: 'soundtouch',
101 | MEDIAPLAYER: 'media_player',
102 | HEOS: 'heos',
103 | };
104 |
105 | const CONTRAST_RATIO = 4.5;
106 |
107 | const COLOR_SIMILARITY_THRESHOLD = 150;
108 |
109 | export {
110 | DEFAULT_HIDE,
111 | ICON,
112 | UPDATE_PROPS,
113 | MEDIA_DURATION_PROP,
114 | PROGRESS_PROPS,
115 | BREAKPOINT,
116 | LABEL_SHORTCUT,
117 | MEDIA_INFO,
118 | PLATFORM,
119 | CONTRAST_RATIO,
120 | COLOR_SIMILARITY_THRESHOLD,
121 | REPEAT_STATE,
122 | };
123 |
--------------------------------------------------------------------------------
/src/components/tts.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit-element';
2 |
3 | import t from '../utils/translation';
4 |
5 | class MiniMediaPlayerTts extends LitElement {
6 | static get properties() {
7 | return {
8 | hass: {},
9 | config: {},
10 | player: {},
11 | };
12 | }
13 |
14 | get label() {
15 | return t(
16 | this.hass,
17 | 'placeholder.tts',
18 | 'ui.card.media_player.text_to_speak',
19 | 'Say',
20 | );
21 | }
22 |
23 | get input() {
24 | return this.shadowRoot.getElementById('tts-input');
25 | }
26 |
27 | get message() {
28 | return this.input.value;
29 | }
30 |
31 | render() {
32 | return html`
33 |
269 |
270 | ```yaml
271 | type: custom:mini-media-player
272 | entity: media_player.kitchen_speakers
273 | ```
274 |
275 | #### Compact card
276 | Setting either `volume` and/or `controls` to `true` in the `hide` option object will render the player as a single row.
277 |
278 |
279 |
280 | ```yaml
281 | type: custom:mini-media-player
282 | entity: media_player.example
283 | icon: mdi:spotify
284 | artwork: cover
285 | hide:
286 | volume: true
287 | source: true
288 | power_state: false
289 | ```
290 |
291 | #### Card with media shortcuts
292 | You can specify media shortcuts through the `shortcuts` option, either as a list or as buttons or why not both?
293 |
294 |
295 |
296 | ```yaml
297 | - entity: media_player.spotify
298 | type: custom:mini-media-player
299 | artwork: cover
300 | source: icon
301 | hide:
302 | volume: true
303 | shortcuts:
304 | columns: 4 # Max buttons per row
305 | buttons:
306 | # Start predefined playlist
307 | - icon: mdi:cat
308 | type: playlist
309 | id: spotify:user:XXXXXXX:playlist:37i9dQZF1DZ06evO2O09Hg # Where XXXXXXX is your User ID
310 | # Change the source to bathroom
311 | - icon: mdi:dog
312 | type: source
313 | id: Bathroom
314 | # Trigger script
315 | - icon: mdi:dog
316 | type: script
317 | id: script.script_name
318 | # Trigger custom service
319 | - name: Crooners Playlist
320 | type: service
321 | id: spotcast.start
322 | data:
323 | entity_id: media_player.googlehome1234
324 | uri: spotify:playlist:37i9dQZF1DX9XiAcF7t1s5
325 |
326 | ... # etc.
327 | ```
328 | **Tip**:
329 | If you don't have Sonos, but want just a bit more control over playlists and so, a simple solution is to use the `type: service`-option, to trigger the`spotcast.start`-service.
330 |
331 | Remember to add the [required data](https://github.com/fondberg/spotcast#call-the-service), for spotcast to work. Also, kindly note that the [spotcast](https://github.com/fondberg/spotcast) custom component is required, for this to work. It's available in HACS.
332 |
333 | #### Grouped cards
334 | Set the `group` option to `true` when nesting mini media player cards inside other cards that already have margins/paddings.
335 |
336 |
337 |
338 | ```yaml
339 | type: entities
340 | entities:
341 | - type: custom:mini-media-player
342 | entity: media_player.multiroom_player
343 | group: true
344 | source: icon
345 | info: short
346 | hide:
347 | volume: true
348 | power: true
349 | - type: custom:mini-media-player
350 | entity: media_player.kitchen_speakers
351 | group: true
352 | hide:
353 | controls: true
354 | - type: custom:mini-media-player
355 | entity: media_player.bathroom_speakers
356 | group: true
357 | hide:
358 | controls: true
359 | - type: custom:mini-media-player
360 | entity: media_player.bedroom_speakers
361 | group: true
362 | hide:
363 | controls: true
364 | - type: custom:mini-media-player
365 | entity: media_player.patio_speakers
366 | group: true
367 | hide:
368 | controls: true
369 | ```
370 |
371 | #### Stacked cards
372 | By using vertical and horizontal stacks, you can achieve many different setups.
373 |
374 |
375 |
376 | ```yaml
377 | - type: horizontal-stack
378 | cards:
379 | - entity: media_player.tv_livingroom
380 | type: custom:mini-media-player
381 | info: short
382 | artwork: cover
383 | hide:
384 | mute: true
385 | icon: true
386 | power_state: false
387 | - entity: media_player.tv_bedroom
388 | type: custom:mini-media-player
389 | info: short
390 | artwork: cover
391 | hide:
392 | mute: true
393 | icon: true
394 | power_state: false
395 | - type: horizontal-stack
396 | cards:
397 | - entity: media_player.cc_patio
398 | type: custom:mini-media-player
399 | hide:
400 | icon: true
401 | - entity: media_player.cc_kitchen
402 | type: custom:mini-media-player
403 | hide:
404 | icon: true
405 | - entity: media_player.cc_bath
406 | type: custom:mini-media-player
407 | hide:
408 | icon: true
409 | ```
410 |
411 | #### Speaker group management
412 | Specify all your speaker entities in a list under the option `speaker_group` -> `entities`. They obviously need to be of the same platform.
413 |
414 | * The card does only allow changes to be made to groups where the entity of the card is the coordinator/master speaker.
415 | * Checking a speakers in the list will make it join the group of the card entity. (*`media_player.sonos_office`* in the example below).
416 | * Unchecking a speaker in the list will remove it from any group it's a part of.
417 |
418 |
419 |
420 | ```yaml
421 | type: custom:mini-media-player
422 | entity: media_player.sonos_office
423 | hide:
424 | power: true
425 | icon: true
426 | source: true
427 | speaker_group:
428 | platform: sonos
429 | show_group_count: true
430 | entities:
431 | - entity_id: media_player.sonos_office
432 | name: Sonos Office
433 | - entity_id: media_player.sonos_livingroom
434 | name: Sonos Livingroom
435 | - entity_id: media_player.sonos_kitchen
436 | name: Sonos Kitchen
437 | - entity_id: media_player.sonos_bathroom
438 | name: Sonos Bathroom
439 | - entity_id: media_player.sonos_bedroom
440 | name: Sonos Bedroom
441 | ```
442 |
443 | If you are planning to use the `speaker_group` option in several cards, creating a separate yaml file for the list is highly recommended, this will result in a less cluttered `ui-lovelace.yaml` and also make the list easier to manage and maintain.
444 | You then simply reference the file containing the list using `entities: !include filename.yaml` for every occurrence of `entities` in your `ui-lovelace.yaml`.
445 |
446 | This is however only possible when you have lovelace mode set to yaml in your HA configuration, see [Lovelace YAML mode](https://www.home-assistant.io/lovelace/yaml-mode/) for more info.
447 |
448 | ## Development
449 | *If you plan to contribute back to this repo, please fork & create the PR against the [dev](https://github.com/kalkih/mini-media-player/tree/dev) branch.*
450 |
451 | **Clone this repository into your `config/www` folder using git.**
452 |
453 | ```console
454 | $ git clone https://github.com/kalkih/mini-media-player.git
455 | ```
456 |
457 | **Add a reference to the card in your `ui-lovelace.yaml`.**
458 |
459 | ```yaml
460 | resources:
461 | - url: /local/mini-media-player/dist/mini-media-player-bundle.js
462 | type: module
463 | ```
464 |
465 | ### Instructions
466 |
467 | *Requires `nodejs` & `npm`*
468 |
469 | 1. Move into the `mini-media-player` repo, checkout the *dev* branch & install dependencies.
470 | ```console
471 | $ cd mini-media-player && git checkout dev && npm install
472 | ```
473 |
474 | 2. Make changes to the source
475 |
476 | 3. Build the source by running
477 | ```console
478 | $ npm run build
479 | ```
480 |
481 | 4. Refresh the browser to see changes
482 |
483 | *Make sure cache is cleared or disabled*
484 |
485 | 5. *(Optional)* Watch the source and automatically rebuild on save
486 | ```console
487 | $ npm run watch
488 | ```
489 |
490 | *The new `mini-media-player-bundle.js` will be build and ready inside `/dist`.*
491 |
492 |
493 | ## Getting errors?
494 | Make sure you have `javascript_version: latest` in your `configuration.yaml` under `frontend:`.
495 |
496 | Make sure you have the latest version of `mini-media-player-bundle.js`.
497 |
498 | If you have issues after updating the card, try clearing your browsers cache or restart Home Assistant.
499 |
500 | If you are getting "Custom element doesn't exist: mini-media-player" or running older browsers try replacing `type: module` with `type: js` in your resource reference, like below.
501 |
502 | ```yaml
503 | resources:
504 | - url: ...
505 | type: js
506 | ```
507 |
508 | ## Inspiration
509 | - [@ciotlosm](https://github.com/ciotlosm) - [custom-lovelace](https://github.com/ciotlosm/custom-lovelace)
510 | - [@c727](https://github.com/c727) - [Custom UI: Mini media player](https://community.home-assistant.io/t/custom-ui-mini-media-player/40135)
511 |
512 | ## License
513 | This project is under the MIT license.
514 |
--------------------------------------------------------------------------------