diff --git a/README.md b/README.md
index 4d0080c..daf09e3 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@
- Supports [ollo69's SamsungTV Smart Component](https://github.com/ollo69/ha-samsungtv-smart)
- Supports [LG webOS Smart TV](https://www.home-assistant.io/integrations/webostv/)
- Supports [Android TV](https://www.home-assistant.io/integrations/androidtv/)
+- Supports [Sony Bravia TV](https://www.home-assistant.io/integrations/braviatv)
- Much easier setup
- Implements haptics feedback
- Customizable layout, you can choose the order of the rows and buttons
@@ -31,12 +32,13 @@
| ---- | ---- | ------- | ---- | -----------
| type | string | **Required** | | `custom:tv-card`
| entity | string | **Required** | | The `media_player` entity to control.
-| platform | string | **Optional** | `samsungtv` | Platform of `media_player`. Supported values: `samsungtv`, `androidtv`, `webostv`, `roku`
-| remote_entity | string | **Optional** | `remote.{{entity_id}}` | The remote entity that controls the Roku `media_player`
+| platform | string | **Optional** | `samsungtv` | Platform of `media_player`. Supported values: `samsungtv`, `androidtv`, `webostv`, `roku`, `braviatv`
+| remote_entity | string | **Optional** | `remote.{{entity_id}}` | The remote entity that controls the Roku and Bravia `media_player`
| volume_entity | string | **Optional** | `entity` | The `media_player` entity for volume control (working only with volume_row: `slider`)
| title | string | **Optional** | | Card title for showing as header.
-| enable_double_click | boolean | **Optional** | `true` | Whether a double click on the touchpad should send the key in `double_click_keycode`
-| double_click_keycode | string | **Optional** | `KEY_RETURN` | The key for double clicks on the touchpad. Defaults to `KEY_RETURN`
+| rows | object | **Optional** | | Button rows
+| enable_double_click | boolean | **Optional** | `true` | Whether a double click on the touchpad should send the key in `double_click_action`
+| double_click_action | string | **Optional** | `return` | The action for double clicks on the touchpad. Defaults to `return`
| enable_button_feedback | boolean | **Optional** | `true` | Shall clicks on the buttons return a vibration feedback?
| enable_slider_feedback | boolean | **Optional** | `true` | Shall the volume slider return a vibration feedback when you slide through it?
| slider_config | object | **Optional** | | Custom configuration for the volume slider. See [slider-card](https://github.com/AnthonMS/my-cards)
@@ -48,26 +50,30 @@ In order to include the buttons, you need to specify in the config the rows you
You do it by declaring the rows as arrays and its buttons as values, like this:
```yaml
-power_row:
- - power
-media_control_row:
- - rewind
- - play
- - pause
- - fast_forward
+rows:
+ power_row:
+ - power
+ media_control_row:
+ - rewind
+ - play
+ - pause
+ - fast_forward
+ volume_row: slider
+ numpad_row: true
+ navigation_row: touchpad
```
-The available rows are `power_row`, `channel_row`, `apps_row`, `source_row` and `media_control_row`
-There also `volume_row` and `navigation_row`, but these requires a string as value.
+The preset rows are `volume_row`, `numpad_row` and `navigation_row`, which requires a string or boolean as value.
| Name | Type | Description
| ---- | ---- | -------
| volume_row | string | Can be either `slider` or `buttons`. This defines the mode you want for setting the volume (you'll see them soon below). You need to have [slider-card](https://github.com/AnthonMS/my-cards) installed in order to use `slider`.
+| numpad_row | boolean | If `true`, numpad row will show.
| navigation_row | string | Can be either `touchpad` or `buttons`. This defines the mode you want for navigating around your tv (you'll also see them soon below).
## **Notice**
-This card supports `androidtv`, `webostv` and `samsungtv` out of the box. If your TV is from another brand you can use this card by setting [custom buttons](#custom-buttons) with services to send keys to your TV (or do whatever you want) in your way.
+This card supports `androidtv`, `webostv`, `roku`, `braviatv` and `samsungtv` out of the box. If your TV is from another brand you can use this card by setting [custom buttons](#custom-buttons) with services to send keys to your TV (or do whatever you want) in your way.
If you have time and wanna help, you can add new integrations to this card. Check [this PR](https://github.com/usernein/tv-card/pull/8).
Platform `webostv` doesn't support power key (see [webOS Integration](https://www.home-assistant.io/integrations/webostv/#turn-on-action))
@@ -112,16 +118,17 @@ custom_sources:
Then you can easily use these buttons in your card:
```yaml
-power_row:
- - browser
- - power
- - input_tv
-media_control_row:
- - rewind
- - play
- - pause
- - fast_forward
- - toggle_light
+rows:
+ power_row:
+ - browser
+ - power
+ - input_tv
+ media_control_row:
+ - rewind
+ - play
+ - pause
+ - fast_forward
+ - toggle_light
```
@@ -200,27 +207,28 @@ Add a custom element in your `ui-lovelace.yaml`
```yaml
- type: custom:tv-card
entity: media_player.tv
- power_row:
- - power
- channel_row:
- - channel_up
- - info
- - channel_down
- apps_row:
- - netflix
- - youtube
- - spotify
- volume_row: slider
- navigation_row: touchpad
- source_row:
- - return
- - home
- - source
- media_control_row:
- - rewind
- - play
- - pause
- - fast_forward
+ rows:
+ power_row:
+ - power
+ channel_row:
+ - channel_up
+ - info
+ - channel_down
+ apps_row:
+ - netflix
+ - youtube
+ - spotify
+ volume_row: slider
+ navigation_row: touchpad
+ source_row:
+ - return
+ - home
+ - source
+ media_control_row:
+ - rewind
+ - play
+ - pause
+ - fast_forward
```
### Example 1
@@ -232,29 +240,30 @@ type: custom:tv-card
entity: media_player.tv
platform: samsungtv
title: Example 1
-power_row:
- - power
-source_row:
- - return
- - home
- - source
- - netflix
-apps_row:
- - youtube
- - spotify
- - netflix
-navigation_row: touchpad
-volume_row: slider
-channel_row:
- - channel_up
- - channel_down
- - info
-media_control_row:
- - rewind
- - play
- - spotify
- - pause
- - fast_forward
+rows:
+ power_row:
+ - power
+ source_row:
+ - return
+ - home
+ - source
+ - netflix
+ apps_row:
+ - youtube
+ - spotify
+ - netflix
+ navigation_row: touchpad
+ volume_row: slider
+ channel_row:
+ - channel_up
+ - channel_down
+ - info
+ media_control_row:
+ - rewind
+ - play
+ - spotify
+ - pause
+ - fast_forward
```
Result:
@@ -270,27 +279,28 @@ type: custom:tv-card
entity: media_player.tv
platform: samsungtv
title: Example 2
-power_row:
- - power
-channel_row:
- - channel_up
- - info
- - channel_down
-apps_row:
- - netflix
- - youtube
- - spotify
-volume_row: buttons
-navigation_row: buttons
-source_row:
- - return
- - home
- - source
-media_control_row:
- - rewind
- - play
- - pause
- - fast_forward
+rows:
+ power_row:
+ - power
+ channel_row:
+ - channel_up
+ - info
+ - channel_down
+ apps_row:
+ - netflix
+ - youtube
+ - spotify
+ volume_row: buttons
+ navigation_row: buttons
+ source_row:
+ - return
+ - home
+ - source
+ media_control_row:
+ - rewind
+ - play
+ - pause
+ - fast_forward
```
Result:
@@ -306,17 +316,18 @@ type: custom:tv-card
entity: media_player.tv
platform: samsungtv
title: Example 3
-power_row:
- - power
-apps_row:
- - netflix
- - youtube
- - spotify
-volume_row: slider
-navigation_row: touchpad
-source_row:
- - return
- - home
+rows:
+ power_row:
+ - power
+ apps_row:
+ - netflix
+ - youtube
+ - spotify
+ volume_row: slider
+ navigation_row: touchpad
+ source_row:
+ - return
+ - home
```
Result:
@@ -328,15 +339,16 @@ Result:
In any row, if you add an ampty item, there will be an empty/invisible button filling the space:
```yaml
-source_row:
- - return
- - home
- - source
-media_control_row:
- - rewind
- -
- -
- - fast-forward
+rows:
+ source_row:
+ - return
+ - home
+ - source
+ media_control_row:
+ - rewind
+ -
+ -
+ - fast-forward
```
diff --git a/tv-card.js b/tv-card.js
index cb12a48..451c789 100644
--- a/tv-card.js
+++ b/tv-card.js
@@ -27,6 +27,7 @@ class TVCardServices extends LitElement {
this.custom_keys = {};
this.custom_sources = {};
this.custom_icons = {};
+ this.rows = {};
this.holdtimer = null;
this.holdaction = null;
@@ -69,6 +70,7 @@ class TVCardServices extends LitElement {
"up": {"key": "UP", "icon": "mdi:chevron-up"},
"left": {"key": "LEFT", "icon": "mdi:chevron-left"},
"enter": {"key": "CENTER", "icon": "mdi:checkbox-blank-circle"},
+ "enter_hold": {"key": "CENTER", "icon": "mdi:checkbox-blank-circle"},
"right": {"key": "RIGHT", "icon": "mdi:chevron-right"},
"down": {"key": "DOWN", "icon": "mdi:chevron-down"},
"rewind": {"key": "REWIND", "icon": "mdi:rewind"},
@@ -92,6 +94,7 @@ class TVCardServices extends LitElement {
"up": {"key": "UP", "icon": "mdi:chevron-up"},
"left": {"key": "LEFT", "icon": "mdi:chevron-left"},
"enter": {"key": "ENTER", "icon": "mdi:checkbox-blank-circle"},
+ "enter_hold": {"key": "ENTER", "icon": "mdi:checkbox-blank-circle"},
"right": {"key": "RIGHT", "icon": "mdi:chevron-right"},
"down": {"key": "DOWN", "icon": "mdi:chevron-down"},
"play": {"key": "PLAY", "icon": "mdi:play"},
@@ -117,6 +120,7 @@ class TVCardServices extends LitElement {
"up": {"icon": "mdi:chevron-up", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "up"}},
"left": {"icon": "mdi:chevron-left", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "left"}},
"enter": {"icon": "mdi:checkbox-blank-circle", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "select"}},
+ "enter_hold": {"icon": "mdi:checkbox-blank-circle", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "select"}},
"right": {"icon": "mdi:chevron-right", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "right"}},
"down": {"icon": "mdi:chevron-down", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "down"}},
"rewind": {"icon": "mdi:rewind", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "reverse"}},
@@ -125,6 +129,44 @@ class TVCardServices extends LitElement {
};
break;
}
+ case "braviatv": {
+ let remote_entity = !config.remote_entity ? "remote." + config.entity.split(".")[1] : config.remote_entity;
+ this.keys = {
+ "power": {"icon": "mdi:power", "service": "remote.toggle", "service_data": { "entity_id": remote_entity}},
+ "source": {"icon": "mdi:import", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Input"}},
+ "volume_up": {"icon": "mdi:volume-plus", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "VolumeUp"}},
+ "volume_down": {"icon": "mdi:volume-minus", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "VolumeDown"}},
+ "volume_mute": {"icon": "mdi:volume-mute", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Mute"}},
+ "return": {"icon": "mdi:arrow-u-left-top", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Return"}},
+ "info": {"icon": "mdi:asterisk", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "GGuide"}},
+ "home": {"icon": "mdi:home", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Home"}},
+ "channel_up": {"icon": "mdi:arrow-up", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "ChannelUp"}},
+ "channel_down": {"icon": "mdi:arrow-down", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "ChannelDown"}},
+ "up": {"icon": "mdi:chevron-up", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Up"}},
+ "left": {"icon": "mdi:chevron-left", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Left"}},
+ "enter": {"icon": "mdi:checkbox-blank-circle", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "DpadCenter"}},
+ "right": {"icon": "mdi:chevron-right", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Right"}},
+ "down": {"icon": "mdi:chevron-down", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Down"}},
+ "rewind": {"icon": "mdi:rewind", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Rewind"}},
+ "play": {"icon": "mdi:play-pause", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Play"}},
+ "pause": {"icon": "mdi:pause", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Pause"}},
+ "fast_forward": {"icon": "mdi:fast-forward", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Forward"}},
+ "settings": {"icon": "mdi:cog", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Options"}},
+ "assistant": {"icon": "mdi:google-assistant", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Assistant"}},
+ "netflix": {"icon": "mdi:netflix", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Netflix"}},
+ "num_0": {"icon": "mdi:numeric-0", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num0"}},
+ "num_1": {"icon": "mdi:numeric-1", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num1"}},
+ "num_2": {"icon": "mdi:numeric-2", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num2"}},
+ "num_3": {"icon": "mdi:numeric-3", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num3"}},
+ "num_4": {"icon": "mdi:numeric-4", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num4"}},
+ "num_5": {"icon": "mdi:numeric-5", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num5"}},
+ "num_6": {"icon": "mdi:numeric-6", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num6"}},
+ "num_7": {"icon": "mdi:numeric-7", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num7"}},
+ "num_8": {"icon": "mdi:numeric-8", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num8"}},
+ "num_9": {"icon": "mdi:numeric-9", "service": "remote.send_command", "service_data": { "entity_id": remote_entity, "command": "Num9"}},
+ };
+ break;
+ }
case "samsungtv":
default: {
this.keys = {
@@ -141,6 +183,7 @@ class TVCardServices extends LitElement {
"up": {"key": "KEY_UP", "icon": "mdi:chevron-up"},
"left": {"key": "KEY_LEFT", "icon": "mdi:chevron-left"},
"enter": {"key": "KEY_ENTER", "icon": "mdi:checkbox-blank-circle"},
+ "enter_hold": {"key": "KEY_ENTER", "icon": "mdi:checkbox-blank-circle"},
"right": {"key": "KEY_RIGHT", "icon": "mdi:chevron-right"},
"down": {"key": "KEY_DOWN", "icon": "mdi:chevron-down"},
"rewind": {"key": "KEY_REWIND", "icon": "mdi:rewind"},
@@ -154,6 +197,7 @@ class TVCardServices extends LitElement {
this.custom_keys = config.custom_keys || {};
this.custom_sources = config.custom_sources || {};
this.custom_icons = config.custom_icons || {};
+ this.rows = config.rows || {};
this.loadCardHelpers();
this.renderVolumeSlider();
@@ -239,6 +283,21 @@ class TVCardServices extends LitElement {
}
}
+ sendAction(action){
+ let info = this.custom_keys[action] || this.custom_sources[action] || this.keys[action] || sources[action];
+
+ if (info.key) {
+ this.sendKey(info.key);
+ }
+ else if (info.source) {
+ this.changeSource(info.source);
+ }
+ else if (info.service) {
+ const [domain, service] = info.service.split(".", 2);
+ this._hass.callService(domain, service, info.service_data);
+ }
+ }
+
changeSource(source) {
let entity_id = this._config.entity;
@@ -252,7 +311,7 @@ class TVCardServices extends LitElement {
onClick(event) {
event.stopImmediatePropagation();
let click_action = () => {
- this.sendKey(this.keys.enter.key);
+ this.sendAction("enter")
if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "light");
};
if (this._config.enable_double_click) {
@@ -270,21 +329,26 @@ class TVCardServices extends LitElement {
clearTimeout(this.timer);
this.timer = null;
- this.sendKey(this._config.double_click_keycode ? this._config.double_click_keycode : this.keys.return.key);
+ this.sendAction(this._config.double_click_action ? this._config.double_click_action : "return")
if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "success");
}
onTouchStart(event) {
event.stopImmediatePropagation();
- this.holdaction = this.keys.enter.key;
+ this.holdaction = "enter_hold";
this.holdtimer = setTimeout(() => {
-
- //hold
- this.holdinterval = setInterval(() => {
- this.sendKey(this.holdaction);
+ if(this.holdaction == "enter_hold"){
+ this.sendAction(this.holdaction)
if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "light");
- }, 200);
+ }
+ else {
+ //hold
+ this.holdinterval = setInterval(() => {
+ this.sendAction(this.holdaction)
+ if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "light");
+ }, 300);
+ }
}, 700);
window.initialX = event.touches[0].clientX;
window.initialY = event.touches[0].clientY;
@@ -315,15 +379,15 @@ class TVCardServices extends LitElement {
if (Math.abs(diffX) > Math.abs(diffY)) {
// sliding horizontally
- let key = diffX > 0 ? this.keys.left.key : this.keys.right.key;
- this.holdaction = key;
- this.sendKey(key);
+ let action = diffX > 0 ? "left" : "right";
+ this.holdaction = action;
+ this.sendAction(action);
} else {
// sliding vertically
- let key = diffY > 0 ? this.keys.up.key : this.keys.down.key;
- this.holdaction = key;
+ let action = diffY > 0 ? "up" : "down";
+ this.holdaction = action;
- this.sendKey(key);
+ this.sendAction(action);
}
if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "selection");
@@ -333,19 +397,7 @@ class TVCardServices extends LitElement {
handleActionClick(e) {
let action = e.currentTarget.action;
-
- let info = this.custom_keys[action] || this.custom_sources[action] || this.keys[action] || sources[action];
-
- if (info.key) {
- this.sendKey(info.key);
- }
- else if (info.source) {
- this.changeSource(info.source);
- }
- else if (info.service) {
- const [domain, service] = info.service.split(".", 2);
- this._hass.callService(domain, service, info.service_data);
- }
+ this.sendAction(action);
if (this._config.enable_button_feedback === undefined || this._config.enable_button_feedback) fireEvent(window, "haptic", "light");
}
@@ -390,53 +442,88 @@ class TVCardServices extends LitElement {
return html ``;
}
- const row_names = ["power_row", "channel_row", "apps_row", "source_row", "volume_row", "media_control_row", "navigation_row"];
+ const preset_rows = ["navigation_row","numpad_row","volume_row"]
var content = [];
- Object.keys(this._config).forEach((row_name) => {
- if (row_names.includes(row_name)) {
- let row_actions = this._config[row_name];
-
- if (row_name === "volume_row") {
- let volume_row = [];
- if (this._config.volume_row == "buttons") {
- volume_row = [
- this.buildIconButton("volume_down"),
- this.buildIconButton("volume_mute"),
- this.buildIconButton("volume_up")
- ];
- } else if (this._config.volume_row == "slider") {
- volume_row = [this.volume_slider];
- }
- content.push(volume_row);
- } else if (row_name === "navigation_row") {
- let navigation_row = [];
-
- if (this._config.navigation_row == "buttons") {
- let up_row = [this.buildIconButton("up")];
- let middle_row = [this.buildIconButton("left"), this.buildIconButton("enter"), this.buildIconButton("right")];
- let down_row = [this.buildIconButton("down")];
- navigation_row = [up_row, middle_row, down_row];
- } else if (this._config.navigation_row == "touchpad") {
- var touchpad = [html `
-
-
- `];
- navigation_row = [touchpad];
- }
- content.push(...navigation_row);
- } else {
- let row_content = this.buildButtonsFromActions(row_actions);
- content.push(row_content);
- }
- }
- });
+ Object.keys(this.rows).forEach((row_name) => {
+ let row_actions = this.rows[row_name];
+
+ if (preset_rows.includes(row_name)) {
+ if (row_name === "volume_row") {
+ let volume_row = [];
+ if (this.rows.volume_row == "buttons") {
+ volume_row = [
+ this.buildIconButton("volume_down"),
+ this.buildIconButton("volume_mute"),
+ this.buildIconButton("volume_up"),
+ ];
+ } else if (this.rows.volume_row == "slider") {
+ volume_row = [this.volume_slider];
+ }
+ content.push(volume_row);
+ } else if (row_name === "navigation_row") {
+ let navigation_row = [];
+
+ if (this.rows.navigation_row == "buttons") {
+ let up_row = [this.buildIconButton("up")];
+ let middle_row = [
+ this.buildIconButton("left"),
+ this.buildIconButton("enter"),
+ this.buildIconButton("right"),
+ ];
+ let down_row = [this.buildIconButton("down")];
+ navigation_row = [up_row, middle_row, down_row];
+ } else if (this.rows.navigation_row == "touchpad") {
+ var touchpad = [
+ html`
+
+
+ `,
+ ];
+ navigation_row = [touchpad];
+ }
+ content.push(...navigation_row);
+ } else if (row_name === "numpad_row") {
+ if (this.rows.numpad_row == true) {
+ let numpad_row = [
+ [
+ this.buildIconButton("num_1"),
+ this.buildIconButton("num_2"),
+ this.buildIconButton("num_3"),
+ ],
+ [
+ this.buildIconButton("num_4"),
+ this.buildIconButton("num_5"),
+ this.buildIconButton("num_6"),
+ ],
+ [
+ this.buildIconButton("num_7"),
+ this.buildIconButton("num_8"),
+ this.buildIconButton("num_9"),
+ ],
+ [
+ this.buildIconButton("channel_down"),
+ this.buildIconButton("num_0"),
+ this.buildIconButton("channel_up"),
+ ],
+ ];
+ content.push(...numpad_row);
+ }
+ }
+ } else {
+ if (!!row_actions) {
+ let row_content = this.buildButtonsFromActions(row_actions);
+ content.push(row_content);
+ }
+ }
+ });
content = content.map(this.buildRow);
@@ -460,6 +547,9 @@ class TVCardServices extends LitElement {
height: 64px;
cursor: pointer;
--mdc-icon-size: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
.row {
display: flex;