From aca681ca1ce52acee3d9d70a08937e3d8e4f205e Mon Sep 17 00:00:00 2001 From: Ian Richardson Date: Sat, 2 Nov 2019 20:46:51 -0600 Subject: [PATCH] rewrite and actions for apps --- LICENSE | 222 +---------- README.md | 166 ++++---- dist/roku-card.js | 678 ++++++++------------------------ example2.png | Bin 23284 -> 0 bytes hacs.json | 4 + package.json | 45 +-- package.yaml | 14 - src/action-handler-directive.ts | 205 ++++++++++ src/long-press.ts | 232 ----------- src/roku-card.ts | 530 ++++++------------------- src/types.ts | 9 +- types/lit-element.d.ts | 1 - 12 files changed, 619 insertions(+), 1487 deletions(-) delete mode 100644 example2.png create mode 100644 hacs.json delete mode 100644 package.yaml create mode 100644 src/action-handler-directive.ts delete mode 100644 src/long-press.ts delete mode 100644 types/lit-element.d.ts diff --git a/LICENSE b/LICENSE index 261eeb9..89ad0ba 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,21 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2019 Ian Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 00e6b1a..655fc5b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Roku Remote Card by [@iantrich](https://www.github.com/iantrich) +# Roku Remote Card + 📺 Roku Remote Lovelace Card [![GitHub Release][releases-shield]][releases] @@ -14,6 +15,7 @@ [![Github][github]][github] ## Support + Hey dude! Help me out for a couple of :beers: or a :coffee:! [![coffee](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/zJtVxUAgH) @@ -24,96 +26,83 @@ This card is for [Lovelace](https://www.home-assistant.io/lovelace) on [Home Ass ## Options -| Name | Type | Requirement | Description -| ---- | ---- | ------- | ----------- -| type | string | **Required** | `custom:roku-card` -| entity | string | **Required** | `media_player` entity of Roku device -| remote | string | **Optional** | `remote` entity of Roku device. Default assumed named like `entity` -| name | string | **Optional** | Card name -| theme | string | **Optional** | Card theme -| tv | boolean | **Optional** | If `true` shows volume and power buttons. Default `false` -| power | `object` | **Optional** | Button configuration for power `See button options` -| volume_up | `object` | **Optional** | Button configuration for volume_up `See button options` -| volume_down | `object` | **Optional** | Button configuration for volume_down `See button options` -| volume_mute | `object` | **Optional** | Button configuration for volume_mute `See button options` -| up | `object` | **Optional** | Button configuration for up `See button options` -| down | `object` | **Optional** | Button configuration for down `See button options` -| left | `object` | **Optional** | Button configuration for left `See button options` -| right | `object` | **Optional** | Button configuration for right `See button options` -| home | `object` | **Optional** | Button configuration for home `See button options` -| info | `object` | **Optional** | Button configuration for info `See button options` -| back | `object` | **Optional** | Button configuration for back `See button options` -| select | `object` | **Optional** | Button configuration for select `See button options` -| reverse | `object` | **Optional** | Button configuration for reverse `See button options` -| play | `object` | **Optional** | Button configuration for play `See button options` -| forward | `object` | **Optional** | Button configuration for forward `See button options` -| apps | `object` | **Optional** | List of app shortcuts `See app options` - -## `app` Options - -| Name | Type | Requirement | Description -| ---- | ---- | ------- | ----------- -| id | `string` | **Optional** | Name of the source to launch -| icon | `string` | **Optional** | Path to image to use for app - -## `button` Options - -| Name | Type | Requirement | Description -| ---- | ---- | ------- | ----------- -| show | `boolean` | **Optional** | Show/Hide button `true` -| tap_action | `object` | **Optional** | Tap action object `See action options` -| hold_action | `object` | **Optional** | Hold action object `See action options` -| dbltap_action | `object` | **Optional** | Doulbe Tap action object `See action options` - -## `action` Options - -| Name | Type | Default | Supported options | Description | -| ----------------- | ------ | -------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `action` | `string` | `toggle` | `more-info`, `toggle`, `call-service`, `none`, `navigate`, `url` | Action to perform | -| `entity` | `string` | none | Any entity id | **Only valid for `action: more-info`** to override the entity on which you want to call `more-info` | -| `navigation_path` | `string` | none | Eg: `/lovelace/0/` | Path to navigate to (e.g. `/lovelace/0/`) when action defined as navigate | -| `url` | `string` | none | Eg: `https://www.google.fr` | URL to open on click when action is `url`. The URL will open in a new tab | -| `service` | `string` | none | Any service | Service to call (e.g. `media_player.media_play_pause`) when `action` defined as `call-service` | -| `service_data` | `object` | none | Any service data | Service data to include (e.g. `entity_id: media_player.bedroom`) when `action` defined as `call-service`. If your `service_data` requires an `entity_id`, you can use the keywork `entity`, this will actually call the service on the entity defined in the main configuration of this card. Useful for [configuration templates](#configuration-templates) | -| `haptic` | `string` | none | `success`, `warning`, `failure`, `light`, `medium`, `heavy`, `selection` | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) | -| `repeat` | `number` | none | eg: `500` | For a hold_action, you can optionally configure the action to repeat while the button is being held down (for example, to repeatedly increase the volume of a media player). Define the number of milliseconds between repeat actions here. | +| Name | Type | Requirement | Description | +| ----------- | --------- | ------------ | -------------------------------------------------------------------------- | +| type | `string` | **Required** | `custom:roku-card` | +| entity | `string` | **Required** | `media_player` entity of Roku device | +| remote | `string` | **Optional** | `remote` entity of Roku device. Default assumed named like `entity` | +| name | `string` | **Optional** | Card name | +| theme | `string` | **Optional** | Card theme | +| tv | `boolean` | **Optional** | If `true` shows volume and power buttons. Default `false` | +| power | `map` | **Optional** | Button configuration for power [See button options](#button-options) | +| volume_up | `map` | **Optional** | Button configuration for volume_up [See button options](#button-options) | +| volume_down | `map` | **Optional** | Button configuration for volume_down [See button options](#button-options) | +| volume_mute | `map` | **Optional** | Button configuration for volume_mute [See button options](#button-options) | +| up | `map` | **Optional** | Button configuration for up [See button options](#button-options) | +| down | `map` | **Optional** | Button configuration for down [See button options](#button-options) | +| left | `map` | **Optional** | Button configuration for left [See button options](#button-options) | +| right | `map` | **Optional** | Button configuration for right [See button options](#button-options) | +| home | `map` | **Optional** | Button configuration for home [See button options](#button-options) | +| info | `map` | **Optional** | Button configuration for info [See button options](#button-options) | +| back | `map` | **Optional** | Button configuration for back [See button options](#button-options) | +| select | `map` | **Optional** | Button configuration for select [See button options](#button-options) | +| reverse | `map` | **Optional** | Button configuration for reverse [See button options](#button-options) | +| play | `map` | **Optional** | Button configuration for play [See button options](#button-options) | +| forward | `map` | **Optional** | Button configuration for forward [See button options](#button-options) | +| apps | `map` | **Optional** | List of app shortcuts [See app options](#app-options) | + +## app Options + +| Name | Type | Requirement | Description | +| ----------------- | -------- | ------------ | ----------------------------------------------------------- | +| app | `string` | **Optional** | Name of the source to launch as `tap_action` | +| image | `string` | **Optional** | Path to image to use for app | +| tap_action | `map` | **Optional** | Tap action map [See action options](#action-options) | +| hold_action | `map` | **Optional** | Hold action map [See action options](#action-options) | +| double_tap_action | `map` | **Optional** | Doulbe Tap action map [See action options](#action-options) | + +## button Options + +| Name | Type | Requirement | Description | +| ----------------- | --------- | ------------ | ----------------------------------------------------------- | +| show | `boolean` | **Optional** | Show/Hide button `true` | +| tap_action | `map` | **Optional** | Tap action map [See action options](#action-options) | +| hold_action | `map` | **Optional** | Hold action map [See action options](#action-options) | +| double_tap_action | `map` | **Optional** | Doulbe Tap action map [See action options](#action-options) | + +## action Options + +| Name | Type | Default | Supported options | Description | +| ----------------- | -------- | -------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | +| `action` | `string` | `toggle` | `more-info`, `toggle`, `call-service`, `none`, `navigate`, `url` | Action to perform | +| `entity` | `string` | none | Any entity id | **Only valid for `action: more-info`** to override the entity on which you want to call `more-info` | +| `navigation_path` | `string` | none | Eg: `/lovelace/0/` | Path to navigate to (e.g. `/lovelace/0/`) when action defined as navigate | +| `url_path` | `string` | none | Eg: `https://www.google.com` | URL to open on click when action is `url`. | +| `service` | `string` | none | Any service | Service to call (e.g. `media_player.media_play_pause`) when `action` defined as `call-service` | +| `service_data` | `map` | none | Any service data | Service data to include (e.g. `entity_id: media_player.bedroom`) when `action` defined as `call-service`. | +| `haptic` | `string` | none | `success`, `warning`, `failure`, `light`, `medium`, `heavy`, `selection` | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) | +| `repeat` | `number` | none | eg: `500` | How often to repeat the `hold_action` in milliseconds. | ## Installation -### Step 1 - -Install `roku-card` by copying `dist/roku-card.js` from this repo to `/www/roku-card.js` on your Home Assistant instance. - -**Example:** - -```bash -wget https://raw.githubusercontent.com/custom-cards/roku-card/master/dist/roku-card.js -mv roku-card* /config/www/ -``` - -### Step 2 +[Follow this guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins) -Link `roku-card` inside your `ui-lovelace.yaml`. +## Usage ```yaml -resources: - - url: /local/roku-card.js?v=0 - type: module -``` - -### Step 3 - -Add a custom element in your `ui-lovelace.yaml` - -```yaml -type: 'custom:roku-card' +type: "custom:roku-card" entity: media_player.basement_roku tv: true apps: - - id: Netflix - icon: /local/netflix.webp - - id: Hulu - icon: /local/hulu.webp + - image: /local/netflix.webp + app: Netflix + - image: /local/hulu.webp + app: Hulu + hold_action: + action: call-service + service: media_player.select_source + service_data: + source: ESPN volume_up: tap_action: action: call-service @@ -122,26 +111,25 @@ volume_up: entity_id: remote.basement_roku command: play volume_down: - dbltap_action: + double_tap_action: action: call-service service: remote.send_command service_data: entity_id: remote.basement_roku command: play - ``` [Troubleshooting](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins) -[commits-shield]: https://img.shields.io/github/commit-activity/y/custom-cards/roku-card.svg?style=for-the-badge -[commits]: https://github.com/custom-cards/roku-card/commits/master +[commits-shield]: https://img.shields.io/github/commit-activity/y/iantrich/roku-card.svg?style=for-the-badge +[commits]: https://github.com/iantrich/roku-card/commits/master [discord]: https://discord.gg/Qa5fW2R [discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge [forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge [forum]: https://community.home-assistant.io/t/lovelace-roku-remote-card/91476 -[license-shield]: https://img.shields.io/github/license/custom-cards/roku-card.svg?style=for-the-badge +[license-shield]: https://img.shields.io/github/license/iantrich/roku-card.svg?style=for-the-badge [maintenance-shield]: https://img.shields.io/badge/maintainer-Ian%20Richardson%20%40iantrich-blue.svg?style=for-the-badge -[releases-shield]: https://img.shields.io/github/release/custom-cards/roku-card.svg?style=for-the-badge -[releases]: https://github.com/custom-cards/roku-card/releases +[releases-shield]: https://img.shields.io/github/release/iantrich/roku-card.svg?style=for-the-badge +[releases]: https://github.com/iantrich/roku-card/releases [twitter]: https://img.shields.io/twitter/follow/iantrich.svg?style=social [github]: https://img.shields.io/github/followers/iantrich.svg?style=social diff --git a/dist/roku-card.js b/dist/roku-card.js index 52aaccd..8b38a1e 100644 --- a/dist/roku-card.js +++ b/dist/roku-card.js @@ -2547,37 +2547,6 @@ LitElement['finalized'] = true; */ LitElement.render = render$1; -/** - * @license - * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at - * http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at - * http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at - * http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at - * http://polymer.github.io/PATENTS.txt - */ -/** - * For AttributeParts, sets the attribute if the value is defined and removes - * the attribute if the value is undefined. - * - * For other part types, this directive is a no-op. - */ -const ifDefined = directive((value) => (part) => { - if (value === undefined && part instanceof AttributePart) { - if (value !== part.value) { - const name = part.committer.name; - part.committer.element.removeAttribute(name); - } - } - else { - part.setValue(value); - } -}); - /** * Parse or format dates * @class fecha @@ -2905,46 +2874,39 @@ fecha.parse = function (dateStr, format, i18nSettings) { return date; }; -var a=function(){try{(new Date).toLocaleDateString("i");}catch(e){return "RangeError"===e.name}return !1}()?function(e,t){return e.toLocaleDateString(t,{year:"numeric",month:"long",day:"numeric"})}:function(t){return fecha.format(t,"mediumDate")},n=function(){try{(new Date).toLocaleString("i");}catch(e){return "RangeError"===e.name}return !1}()?function(e,t){return e.toLocaleString(t,{year:"numeric",month:"long",day:"numeric",hour:"numeric",minute:"2-digit"})}:function(t){return fecha.format(t,"haDateTime")},r=function(){try{(new Date).toLocaleTimeString("i");}catch(e){return "RangeError"===e.name}return !1}()?function(e,t){return e.toLocaleTimeString(t,{hour:"numeric",minute:"2-digit"})}:function(t){return fecha.format(t,"shortTime")};var h=function(e,t,a,n){void 0===n&&(n=!1),e._themes||(e._themes={});var r=t.default_theme;("default"===a||a&&t.themes[a])&&(r=a);var i=Object.assign({},e._themes);if("default"!==r){var o=t.themes[r];Object.keys(o).forEach(function(t){var a="--"+t;e._themes[a]="",i[a]=o[t];});}if(e.updateStyles?e.updateStyles(i):window.ShadyCSS&&window.ShadyCSS.styleSubtree(e,i),n){var s=document.querySelector("meta[name=theme-color]");if(s){s.hasAttribute("default-content")||s.setAttribute("default-content",s.getAttribute("content"));var c=i["--primary-color"]||s.getAttribute("default-content");s.setAttribute("content",c);}}};function d(e){return e.substr(0,e.indexOf("."))}var S=["closed","locked","off"],L=function(e,t,a,n){n=n||{},a=null==a?{}:a;var r=new Event(t,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return r.detail=a,e.dispatchEvent(r),r};var A=function(e,t){L(e,"haptic",t);},O=function(e,t,a){void 0===a&&(a=!1),a?history.replaceState(null,"",t):history.pushState(null,"",t),L(window,"location-changed",{replace:a});},j=function(e,t,a){void 0===a&&(a=!0);var n,r=d(t),i="group"===r?"homeassistant":r;switch(r){case"lock":n=a?"unlock":"lock";break;case"cover":n=a?"open_cover":"close_cover";break;default:n=a?"turn_on":"turn_off";}return e.callService(i,n,{entity_id:t})},z=function(e,t){var a=S.includes(e.states[t].state);return j(e,t,a)},P=function(e,t,a,n,r){var i;switch(r&&a.dbltap_action?i=a.dbltap_action:n&&a.hold_action?i=a.hold_action:!n&&a.tap_action&&(i=a.tap_action),i||(i={action:"more-info"}),i.action){case"more-info":(a.entity||a.camera_image)&&(L(e,"hass-more-info",{entityId:i.entity?i.entity:a.entity?a.entity:a.camera_image}),i.haptic&&A(e,i.haptic));break;case"navigate":i.navigation_path&&(O(0,i.navigation_path),i.haptic&&A(e,i.haptic));break;case"url":i.url&&window.open(i.url),i.haptic&&A(e,i.haptic);break;case"toggle":a.entity&&(z(t,a.entity),i.haptic&&A(e,i.haptic));break;case"call-service":if(!i.service)return;var o=i.service.split(".",2),s=o[0],c=o[1],u=Object.assign({},i.service_data);"entity"===u.entity_id&&(u.entity_id=a.entity),t.callService(s,c,u),i.haptic&&A(e,i.haptic);}};String(Math.random()).slice(2);try{const e={get capture(){return !1}};window.addEventListener("test",e,e),window.removeEventListener("test",e,e);}catch(e){}(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.0.0");var H="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0,N=function(e){function t(){e.call(this),this.holdTime=500,this.ripple=document.createElement("paper-ripple"),this.timer=void 0,this.held=!1,this.cooldownStart=!1,this.cooldownEnd=!1,this.nbClicks=0;}return e&&(t.__proto__=e),(t.prototype=Object.create(e&&e.prototype)).constructor=t,t.prototype.connectedCallback=function(){var e=this;Object.assign(this.style,{borderRadius:"50%",position:"absolute",width:H?"100px":"50px",height:H?"100px":"50px",transform:"translate(-50%, -50%)",pointerEvents:"none"}),this.appendChild(this.ripple),this.ripple.style.color="#03a9f4",this.ripple.style.color="var(--primary-color)",["touchcancel","mouseout","mouseup","touchmove","mousewheel","wheel","scroll"].forEach(function(t){document.addEventListener(t,function(){clearTimeout(e.timer),e.stopAnimation(),e.timer=void 0;},{passive:!0});});},t.prototype.bind=function(e){var t=this;if(!e.longPress){e.longPress=!0,e.addEventListener("contextmenu",function(e){var t=e||window.event;return t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.cancelBubble=!0,t.returnValue=!1,!1});var a=function(a){var n,r;t.cooldownStart||(t.held=!1,a.touches?(n=a.touches[0].pageX,r=a.touches[0].pageY):(n=a.pageX,r=a.pageY),t.timer=window.setTimeout(function(){t.startAnimation(n,r),t.held=!0,e.repeat&&!e.isRepeating&&(e.isRepeating=!0,t.repeatTimeout=setInterval(function(){e.dispatchEvent(new Event("ha-hold"));},e.repeat));},t.holdTime),t.cooldownStart=!0,window.setTimeout(function(){return t.cooldownStart=!1},100));},n=function(a){t.cooldownEnd||["touchend","touchcancel"].includes(a.type)&&void 0===t.timer?e.isRepeating&&t.repeatTimeout&&(clearInterval(t.repeatTimeout),e.isRepeating=!1):(clearTimeout(t.timer),e.isRepeating&&t.repeatTimeout&&clearInterval(t.repeatTimeout),e.isRepeating=!1,t.stopAnimation(),t.timer=void 0,t.held?e.repeat||e.dispatchEvent(new Event("ha-hold")):e.hasDblClick?0===t.nbClicks?(t.nbClicks+=1,t.dblClickTimeout=window.setTimeout(function(){1===t.nbClicks&&(t.nbClicks=0,e.dispatchEvent(new Event("ha-click")));},250)):(t.nbClicks=0,clearTimeout(t.dblClickTimeout),e.dispatchEvent(new Event("ha-dblclick"))):e.dispatchEvent(new Event("ha-click")),t.cooldownEnd=!0,window.setTimeout(function(){return t.cooldownEnd=!1},100));};e.addEventListener("touchstart",a,{passive:!0}),e.addEventListener("touchend",n),e.addEventListener("touchcancel",n),e.addEventListener("mousedown",a,{passive:!0}),e.addEventListener("click",n);}},t.prototype.startAnimation=function(e,t){Object.assign(this.style,{left:e+"px",top:t+"px",display:null}),this.ripple.holdDown=!0,this.ripple.simulatedRipple();},t.prototype.stopAnimation=function(){this.ripple.holdDown=!1,this.style.display="none";},t}(HTMLElement);customElements.get("long-press-custom-card-helpers")||customElements.define("long-press-custom-card-helpers",N); +var a=function(){try{(new Date).toLocaleDateString("i");}catch(e){return "RangeError"===e.name}return !1}()?function(e,t){return e.toLocaleDateString(t,{year:"numeric",month:"long",day:"numeric"})}:function(t){return fecha.format(t,"mediumDate")},n=function(){try{(new Date).toLocaleString("i");}catch(e){return "RangeError"===e.name}return !1}()?function(e,t){return e.toLocaleString(t,{year:"numeric",month:"long",day:"numeric",hour:"numeric",minute:"2-digit"})}:function(t){return fecha.format(t,"haDateTime")},r=function(){try{(new Date).toLocaleTimeString("i");}catch(e){return "RangeError"===e.name}return !1}()?function(e,t){return e.toLocaleTimeString(t,{hour:"numeric",minute:"2-digit"})}:function(t){return fecha.format(t,"shortTime")};var m=function(e,t,a,n){void 0===n&&(n=!1),e._themes||(e._themes={});var r=t.default_theme;("default"===a||a&&t.themes[a])&&(r=a);var i=Object.assign({},e._themes);if("default"!==r){var o=t.themes[r];Object.keys(o).forEach(function(t){var a="--"+t;e._themes[a]="",i[a]=o[t];});}if(e.updateStyles?e.updateStyles(i):window.ShadyCSS&&window.ShadyCSS.styleSubtree(e,i),n){var s=document.querySelector("meta[name=theme-color]");if(s){s.hasAttribute("default-content")||s.setAttribute("default-content",s.getAttribute("content"));var c=i["--primary-color"]||s.getAttribute("default-content");s.setAttribute("content",c);}}};function f(e){return e.substr(0,e.indexOf("."))}var E=["closed","locked","off"],A=function(e,t,a,n){n=n||{},a=null==a?{}:a;var r=new Event(t,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return r.detail=a,e.dispatchEvent(r),r};var F=function(e){A(window,"haptic",e);},B=function(e,t,a){void 0===a&&(a=!1),a?history.replaceState(null,"",t):history.pushState(null,"",t),A(window,"location-changed",{replace:a});},U=function(e,t,a){void 0===a&&(a=!0);var n,r=f(t),i="group"===r?"homeassistant":r;switch(r){case"lock":n=a?"unlock":"lock";break;case"cover":n=a?"open_cover":"close_cover";break;default:n=a?"turn_on":"turn_off";}return e.callService(i,n,{entity_id:t})},V=function(e,t){var a=E.includes(e.states[t].state);return U(e,t,a)},W=function(e,t,a,n){var r;if("double_tap"===n&&a.double_tap_action?r=a.double_tap_action:"hold"===n&&a.hold_action?r=a.hold_action:"tap"===n&&a.tap_action&&(r=a.tap_action),r||(r={action:"more-info"}),!r.confirmation||r.confirmation.exemptions&&r.confirmation.exemptions.some(function(e){return e.user===t.user.id})||(F("warning"),confirm(r.confirmation.text||"Are you sure you want to "+r.action+"?")))switch(r.action){case"more-info":(a.entity||a.camera_image)&&A(e,"hass-more-info",{entityId:a.entity?a.entity:a.camera_image});break;case"navigate":r.navigation_path&&B(0,r.navigation_path);break;case"url":r.url_path&&window.open(r.url_path);break;case"toggle":a.entity&&(V(t,a.entity),F("success"));break;case"call-service":if(!r.service)return void F("failure");var i=r.service.split(".",2);t.callService(i[0],i[1],r.service_data),F("success");}};function G(e){return void 0!==e&&"none"!==e.action} -// See https://github.com/home-assistant/home-assistant-polymer/pull/2457 -// on how to undo mwc -> paper migration -// import '@material/mwc-ripple'; -const isTouch = 'ontouchstart' in window - || navigator.maxTouchPoints > 0 - || navigator.msMaxTouchPoints > 0; -class LongPress extends HTMLElement { +const isTouch = "ontouchstart" in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0; +class ActionHandler extends HTMLElement { constructor() { super(); this.holdTime = 500; - this.ripple = document.createElement('paper-ripple'); + this.ripple = document.createElement("mwc-ripple"); this.timer = undefined; this.held = false; this.cooldownStart = false; this.cooldownEnd = false; - this.nbClicks = 0; } connectedCallback() { Object.assign(this.style, { - borderRadius: '50%', - position: 'absolute', - width: isTouch ? '100px' : '50px', - height: isTouch ? '100px' : '50px', - transform: 'translate(-50%, -50%)', - pointerEvents: 'none', + position: "absolute", + width: isTouch ? "100px" : "50px", + height: isTouch ? "100px" : "50px", + transform: "translate(-50%, -50%)", + pointerEvents: "none", }); this.appendChild(this.ripple); - this.ripple.style.color = '#03a9f4'; // paper-ripple - this.ripple.style.color = 'var(--primary-color)'; // paper-ripple - // this.ripple.primary = true; + this.ripple.primary = true; [ - 'touchcancel', - 'mouseout', - 'mouseup', - 'touchmove', - 'mousewheel', - 'wheel', - 'scroll', + "touchcancel", + "mouseout", + "mouseup", + "touchmove", + "mousewheel", + "wheel", + "scroll", ].forEach((ev) => { document.addEventListener(ev, () => { clearTimeout(this.timer); @@ -2953,13 +2915,12 @@ class LongPress extends HTMLElement { }, { passive: true }); }); } - bind(element) { - /* eslint no-param-reassign: 0 */ - if (element.longPress) { + bind(element, options) { + if (element.actionHandler) { return; } - element.longPress = true; - element.addEventListener('contextmenu', (ev) => { + element.actionHandler = true; + element.addEventListener("contextmenu", (ev) => { const e = ev || window.event; if (e.preventDefault) { e.preventDefault(); @@ -2986,68 +2947,56 @@ class LongPress extends HTMLElement { x = ev.pageX; y = ev.pageY; } - this.timer = window.setTimeout(() => { - this.startAnimation(x, y); - this.held = true; - if (element.repeat && !element.isRepeating) { - element.isRepeating = true; - this.repeatTimeout = setInterval(() => { - element.dispatchEvent(new Event('ha-hold')); - }, element.repeat); - } - }, this.holdTime); + if (options.hasHold) { + this.timer = window.setTimeout(() => { + this.startAnimation(x, y); + this.held = true; + }, this.holdTime); + } this.cooldownStart = true; window.setTimeout(() => (this.cooldownStart = false), 100); }; const clickEnd = (ev) => { - if (this.cooldownEnd - || (['touchend', 'touchcancel'].includes(ev.type) - && this.timer === undefined)) { - if (element.isRepeating && this.repeatTimeout) { - clearInterval(this.repeatTimeout); - element.isRepeating = false; - } + if (this.cooldownEnd || + (["touchend", "touchcancel"].includes(ev.type) && + this.timer === undefined)) { return; } clearTimeout(this.timer); - if (element.isRepeating && this.repeatTimeout) { - clearInterval(this.repeatTimeout); - } - element.isRepeating = false; this.stopAnimation(); this.timer = undefined; if (this.held) { - if (!element.repeat) { - element.dispatchEvent(new Event('ha-hold')); - } + A(element, "action", { action: "hold" }); } - else if (element.hasDblClick) { - if (this.nbClicks === 0) { - this.nbClicks += 1; + else if (options.hasDoubleTap) { + if (ev.detail === 1) { this.dblClickTimeout = window.setTimeout(() => { - if (this.nbClicks === 1) { - this.nbClicks = 0; - element.dispatchEvent(new Event('ha-click')); - } + A(element, "action", { action: "tap" }); }, 250); } else { - this.nbClicks = 0; clearTimeout(this.dblClickTimeout); - element.dispatchEvent(new Event('ha-dblclick')); + A(element, "action", { action: "double_tap" }); } } else { - element.dispatchEvent(new Event('ha-click')); + A(element, "action", { action: "tap" }); } this.cooldownEnd = true; window.setTimeout(() => (this.cooldownEnd = false), 100); }; - element.addEventListener('touchstart', clickStart, { passive: true }); - element.addEventListener('touchend', clickEnd); - element.addEventListener('touchcancel', clickEnd); - element.addEventListener('mousedown', clickStart, { passive: true }); - element.addEventListener('click', clickEnd); + element.addEventListener("touchstart", clickStart, { passive: true }); + element.addEventListener("touchend", clickEnd); + element.addEventListener("touchcancel", clickEnd); + // iOS 13 sends a complete normal touchstart-touchend series of events followed by a mousedown-click series. + // That might be a bug, but until it's fixed, this should make action-handler work. + // If it's not a bug that is fixed, this might need updating with the next iOS version. + // Note that all events (both touch and mouse) must be listened for in order to work on computers with both mouse and touchscreen. + const isIOS13 = window.navigator.userAgent.match(/iPhone OS 13_/); + if (!isIOS13) { + element.addEventListener("mousedown", clickStart, { passive: true }); + element.addEventListener("click", clickEnd); + } } startAnimation(x, y) { Object.assign(this.style, { @@ -3055,38 +3004,35 @@ class LongPress extends HTMLElement { top: `${y}px`, display: null, }); - this.ripple.holdDown = true; // paper-ripple - this.ripple.simulatedRipple(); // paper-ripple - // this.ripple.disabled = false; - // this.ripple.active = true; - // this.ripple.unbounded = true; + this.ripple.disabled = false; + this.ripple.active = true; + this.ripple.unbounded = true; } stopAnimation() { - this.ripple.holdDown = false; // paper-ripple - // this.ripple.active = false; - // this.ripple.disabled = true; - this.style.display = 'none'; + this.ripple.active = false; + this.ripple.disabled = true; + this.style.display = "none"; } } -customElements.define('long-press-roku-card', LongPress); -const getLongPress = () => { +customElements.define("action-handler-roku", ActionHandler); +const geActionHandler = () => { const body = document.body; - if (body.querySelector('long-press-roku-card')) { - return body.querySelector('long-press-roku-card'); + if (body.querySelector("action-handler-roku")) { + return body.querySelector("action-handler-roku"); } - const longpress = document.createElement('long-press-roku-card'); - body.appendChild(longpress); - return longpress; + const actionhandler = document.createElement("action-handler-roku"); + body.appendChild(actionhandler); + return actionhandler; }; -const longPressBind = (element) => { - const longpress = getLongPress(); - if (!longpress) { +const actionHandlerBind = (element, options) => { + const actionhandler = geActionHandler(); + if (!actionhandler) { return; } - longpress.bind(element); + actionhandler.bind(element, options); }; -const longPress = directive(() => (part) => { - longPressBind(part.committer.element); +const actionHandler = directive((options = {}) => (part) => { + actionHandlerBind(part.committer.element, options); }); const defaultRemoteAction = { @@ -3109,323 +3055,50 @@ let RokuCard = class RokuCard extends LitElement { return html ``; } const stateObj = this.hass.states[this._config.entity]; + if (!stateObj) { + return html ` + +
Show Warning
+
+ `; + } return html `
+
${stateObj.attributes.app_name}
${this._config.tv || (this._config.power && this._config.power.show) - ? html ` - - ` + ? this._renderButton("power", "mdi:power", "Power") : ""}
- ${this._config.back && this._config.back.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.info && this._config.info.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.home && this._config.home.show === false - ? html ` - - ` - : html ` - - `} + ${this._renderButton("back", "mdi:arrow-left", "Back")} + ${this._renderButton("info", "mdi:asterisk", "Info")} + ${this._renderButton("home", "mdi:home", "Home")}
- ${this._config.apps && this._config.apps.length > 0 - ? html ` - - ` - : html ` - - `} - ${this._config.up && this._config.up.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.apps && this._config.apps.length > 1 - ? html ` - - ` - : html ` - - `} + ${this._renderImage(0)} + ${this._renderButton("up", "mdi:chevron-up", "Up")} + ${this._renderImage(1)}
- ${this._config.left && this._config.left.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.select && this._config.select.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.right && this._config.right.show === false - ? html ` - - ` - : html ` - - `} + ${this._renderButton("left", "mdi:chevron-left", "Left")} + ${this._renderButton("select", "mdi:checkbox-blank-circle", "Select")} + ${this._renderButton("right", "mdi:chevron-right", "Right")}
- ${this._config.apps && this._config.apps.length > 2 - ? html ` - - ` - : html ` - - `} - ${this._config.down && this._config.down.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.apps && this._config.apps.length > 3 - ? html ` - - ` - : html ` - - `} + ${this._renderImage(2)} + ${this._renderButton("down", "mdi:chevron-down", "Down")} + ${this._renderImage(3)}
- ${this._config.reverse && this._config.reverse.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.play && this._config.play.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.forward && this._config.forward.show === false - ? html ` - - ` - : html ` - - `} + ${this._renderButton("reverse", "mdi:rewind", "Rewind")} + ${this._renderButton("play", "mdi:play-pause", "Play/Pause")} + ${this._renderButton("forward", "mdi:fast-forward", "Fast-Forward")}
${this._config.tv || @@ -3434,75 +3107,9 @@ let RokuCard = class RokuCard extends LitElement { (this._config.volume_up && this._config.volume_up.show) ? html `
- ${this._config.volume_mute && - this._config.volume_mute.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.volume_down && - this._config.volume_down.show === false - ? html ` - - ` - : html ` - - `} - ${this._config.volume_up && - this._config.volume_up.show === false - ? html ` - - ` - : html ` - - `} + ${this._renderButton("volume_mute", "mdi:volume-mute", "Volume Mute")} + ${this._renderButton("volume_down", "mdi:volume-minus", "Volume Down")} + ${this._renderButton("volume_up", "mdi:volume-plus", "Volume Up")}
` : ""} @@ -3516,7 +3123,7 @@ let RokuCard = class RokuCard extends LitElement { } const oldHass = changedProps.get("hass"); if (!oldHass || oldHass.themes !== this.hass.themes) { - h(this, this.hass.themes, this._config.theme); + m(this, this.hass.themes, this._config.theme); } } static get styles() { @@ -3537,46 +3144,81 @@ let RokuCard = class RokuCard extends LitElement { display: flex; padding: 8px 36px 8px 36px; justify-content: space-evenly; + align-items: center; + } + .warning { + display: block; + color: black; + background-color: #fce588; + padding: 8px; + } + .app { + flex-grow: 3; + font-size: 20px; } `; } - launchApp(e) { - const target = e.currentTarget; - this.hass.callService("media_player", "select_source", { - entity_id: this._config.entity, - source: target.app - }); + _renderImage(index) { + return this._config.apps && this._config.apps.length > index + ? html ` + + ` + : html ` + + `; } - _handleTap(ev) { + _renderButton(button, icon, title) { + return this._config[button] && this._config[button].show === false + ? html ` + + ` + : html ` + + `; + } + _handleAction(ev) { const button = ev.currentTarget.button; - console.log(button); - const config = this._config[button]; - console.log(config); - let remote = this._config.remote + const config = this._config[button] || ev.currentTarget.config; + const app = ev.currentTarget.app; + const remote = this._config.remote ? this._config.remote : "remote." + this._config.entity.split(".")[1]; - P(this, this.hass, config && config.tap_action + W(this, this.hass, config && config.tap_action ? config - : { + : app + ? Object.assign({ tap_action: { + action: "call-service", + service: "media_player.select_source", + service_data: { + entity_id: this._config.entity, + source: app + } + } }, config) : { tap_action: Object.assign({ service_data: { command: button, entity_id: remote } }, defaultRemoteAction) - }, false, false); - } - _handleHold(ev) { - const button = ev.currentTarget.button; - const config = this._config[button]; - if (config && config.hold_action) { - P(this, this.hass, config, true, false); - } - } - _handleDblTap(ev) { - const button = ev.currentTarget.button; - const config = this._config[button]; - if (config && config.dbltap_action) { - P(this, this.hass, config, false, true); - } + }, ev.detail.action); } }; __decorate([ diff --git a/example2.png b/example2.png deleted file mode 100644 index e2d59f6fc4505d78a1d43dee41599a5630cd2763..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23284 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", "license": "MIT", "dependencies": { - "custom-card-helpers": "^1.2.2", - "dev": "^0.1.3", - "home-assistant-js-websocket": "^3.4.0", - "lit-element": "^2.1.0", - "superstruct": "^0.6.0" + "custom-card-helpers": "^1.3.5", + "home-assistant-js-websocket": "^4.4.0", + "lit-element": "^2.2.1" }, "devDependencies": { - "@babel/core": "^7.4.3", - "@babel/plugin-proposal-class-properties": "^7.4.0", + "@babel/core": "^7.6.4", + "@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-decorators": "^7.4.0", - "@typescript-eslint/eslint-plugin": "^1.4.2", - "@typescript-eslint/parser": "^1.4.1", - "eslint": "^5.14.1", - "eslint-config-airbnb-base": "^13.1.0", - "eslint-plugin-import": "^2.16.0", - "prettier": "^1.16.4", - "rollup": "^1.2.3", - "rollup-plugin-babel": "^4.3.2", - "rollup-plugin-node-resolve": "^4.0.1", - "rollup-plugin-typescript2": "^0.19.2", - "typescript": "^3.3.3333" + "@typescript-eslint/eslint-plugin": "^2.6.0", + "@typescript-eslint/parser": "^2.6.0", + "eslint": "^6.6.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-import": "^2.18.2", + "prettier": "^1.18.2", + "rollup": "^1.26.0", + "rollup-plugin-babel": "^4.3.3", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-terser": "^5.1.2", + "rollup-plugin-typescript2": "^0.24.3", + "rollup-plugin-uglify": "^6.0.3", + "typescript": "^3.6.4" }, "scripts": { "start": "rollup -c --watch", diff --git a/package.yaml b/package.yaml deleted file mode 100644 index 7882df1..0000000 --- a/package.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: Roku Card -description: 📺 Roku Remote Card -type: lovelace -keywords: - - roku - - remote - - tv - - television -author: - name: Ian Richardson -license: Apache License, Version 2.0 -files: - - roku-card.js - - roku-card-editor.js diff --git a/src/action-handler-directive.ts b/src/action-handler-directive.ts new file mode 100644 index 0000000..ac14003 --- /dev/null +++ b/src/action-handler-directive.ts @@ -0,0 +1,205 @@ +import { directive, PropertyPart } from "lit-html"; +import { fireEvent, ActionHandlerOptions } from "custom-card-helpers"; + +const isTouch = + "ontouchstart" in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0; + +interface ActionHandler extends HTMLElement { + holdTime: number; + bind(element: Element, options): void; +} +interface ActionHandlerElement extends Element { + actionHandler?: boolean; +} + +class ActionHandler extends HTMLElement implements ActionHandler { + public holdTime: number; + public ripple: any; + protected timer: number | undefined; + protected held: boolean; + protected cooldownStart: boolean; + protected cooldownEnd: boolean; + private dblClickTimeout: number | undefined; + + constructor() { + super(); + this.holdTime = 500; + this.ripple = document.createElement("mwc-ripple"); + this.timer = undefined; + this.held = false; + this.cooldownStart = false; + this.cooldownEnd = false; + } + + public connectedCallback() { + Object.assign(this.style, { + position: "absolute", + width: isTouch ? "100px" : "50px", + height: isTouch ? "100px" : "50px", + transform: "translate(-50%, -50%)", + pointerEvents: "none", + }); + + this.appendChild(this.ripple); + this.ripple.primary = true; + + [ + "touchcancel", + "mouseout", + "mouseup", + "touchmove", + "mousewheel", + "wheel", + "scroll", + ].forEach((ev) => { + document.addEventListener( + ev, + () => { + clearTimeout(this.timer); + this.stopAnimation(); + this.timer = undefined; + }, + { passive: true } + ); + }); + } + + public bind(element: ActionHandlerElement, options) { + if (element.actionHandler) { + return; + } + element.actionHandler = true; + + element.addEventListener("contextmenu", (ev: Event) => { + const e = ev || window.event; + if (e.preventDefault) { + e.preventDefault(); + } + if (e.stopPropagation) { + e.stopPropagation(); + } + e.cancelBubble = true; + e.returnValue = false; + return false; + }); + + const clickStart = (ev: Event) => { + if (this.cooldownStart) { + return; + } + this.held = false; + let x; + let y; + if ((ev as TouchEvent).touches) { + x = (ev as TouchEvent).touches[0].pageX; + y = (ev as TouchEvent).touches[0].pageY; + } else { + x = (ev as MouseEvent).pageX; + y = (ev as MouseEvent).pageY; + } + + if (options.hasHold) { + this.timer = window.setTimeout(() => { + this.startAnimation(x, y); + this.held = true; + }, this.holdTime); + } + + this.cooldownStart = true; + window.setTimeout(() => (this.cooldownStart = false), 100); + }; + + const clickEnd = (ev: Event) => { + if ( + this.cooldownEnd || + (["touchend", "touchcancel"].includes(ev.type) && + this.timer === undefined) + ) { + return; + } + clearTimeout(this.timer); + this.stopAnimation(); + this.timer = undefined; + if (this.held) { + fireEvent(element as HTMLElement, "action", { action: "hold" }); + } else if (options.hasDoubleTap) { + if ((ev as MouseEvent).detail === 1) { + this.dblClickTimeout = window.setTimeout(() => { + fireEvent(element as HTMLElement, "action", { action: "tap" }); + }, 250); + } else { + clearTimeout(this.dblClickTimeout); + fireEvent(element as HTMLElement, "action", { action: "double_tap" }); + } + } else { + fireEvent(element as HTMLElement, "action", { action: "tap" }); + } + this.cooldownEnd = true; + window.setTimeout(() => (this.cooldownEnd = false), 100); + }; + + element.addEventListener("touchstart", clickStart, { passive: true }); + element.addEventListener("touchend", clickEnd); + element.addEventListener("touchcancel", clickEnd); + + // iOS 13 sends a complete normal touchstart-touchend series of events followed by a mousedown-click series. + // That might be a bug, but until it's fixed, this should make action-handler work. + // If it's not a bug that is fixed, this might need updating with the next iOS version. + // Note that all events (both touch and mouse) must be listened for in order to work on computers with both mouse and touchscreen. + const isIOS13 = window.navigator.userAgent.match(/iPhone OS 13_/); + if (!isIOS13) { + element.addEventListener("mousedown", clickStart, { passive: true }); + element.addEventListener("click", clickEnd); + } + } + + private startAnimation(x: number, y: number) { + Object.assign(this.style, { + left: `${x}px`, + top: `${y}px`, + display: null, + }); + this.ripple.disabled = false; + this.ripple.active = true; + this.ripple.unbounded = true; + } + + private stopAnimation() { + this.ripple.active = false; + this.ripple.disabled = true; + this.style.display = "none"; + } +} + +customElements.define("action-handler-roku", ActionHandler); + +const geActionHandler = (): ActionHandler => { + const body = document.body; + if (body.querySelector("action-handler-roku")) { + return body.querySelector("action-handler-roku") as ActionHandler; + } + + const actionhandler = document.createElement("action-handler-roku"); + body.appendChild(actionhandler); + + return actionhandler as ActionHandler; +}; + +export const actionHandlerBind = ( + element: ActionHandlerElement, + options: ActionHandlerOptions +) => { + const actionhandler: ActionHandler = geActionHandler(); + if (!actionhandler) { + return; + } + actionhandler.bind(element, options); +}; + +export const actionHandler = directive( + (options: ActionHandlerOptions = {}) => (part: PropertyPart) => { + actionHandlerBind(part.committer.element, options); + } +); \ No newline at end of file diff --git a/src/long-press.ts b/src/long-press.ts deleted file mode 100644 index 1c80643..0000000 --- a/src/long-press.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { directive, PropertyPart } from 'lit-html'; -// See https://github.com/home-assistant/home-assistant-polymer/pull/2457 -// on how to undo mwc -> paper migration -// import '@material/mwc-ripple'; - -const isTouch = 'ontouchstart' in window - || navigator.maxTouchPoints > 0 - || navigator.msMaxTouchPoints > 0; - -interface LongPress extends HTMLElement { - holdTime: number; - bind(element: Element): void; -} -interface LongPressElement extends Element { - longPress?: boolean; - repeat?: number | undefined; - isRepeating?: boolean | undefined; - hasDblClick?: boolean | undefined; -} - -class LongPress extends HTMLElement implements LongPress { - public holdTime: number; - - protected ripple: any; - - protected timer: number | undefined; - - protected held: boolean; - - protected cooldownStart: boolean; - - protected cooldownEnd: boolean; - - private repeatTimeout: NodeJS.Timeout | undefined; - - private dblClickTimeout: number | undefined; - - private nbClicks: number; - - constructor() { - super(); - this.holdTime = 500; - this.ripple = document.createElement('paper-ripple'); - this.timer = undefined; - this.held = false; - this.cooldownStart = false; - this.cooldownEnd = false; - this.nbClicks = 0; - } - - public connectedCallback() { - Object.assign(this.style, { - borderRadius: '50%', // paper-ripple - position: 'absolute', - width: isTouch ? '100px' : '50px', - height: isTouch ? '100px' : '50px', - transform: 'translate(-50%, -50%)', - pointerEvents: 'none', - }); - - this.appendChild(this.ripple); - this.ripple.style.color = '#03a9f4'; // paper-ripple - this.ripple.style.color = 'var(--primary-color)'; // paper-ripple - // this.ripple.primary = true; - - [ - 'touchcancel', - 'mouseout', - 'mouseup', - 'touchmove', - 'mousewheel', - 'wheel', - 'scroll', - ].forEach((ev) => { - document.addEventListener( - ev, - () => { - clearTimeout(this.timer); - this.stopAnimation(); - this.timer = undefined; - }, - { passive: true }, - ); - }); - } - - public bind(element: LongPressElement) { - /* eslint no-param-reassign: 0 */ - if (element.longPress) { - return; - } - element.longPress = true; - - element.addEventListener('contextmenu', (ev: Event) => { - const e = ev || window.event; - if (e.preventDefault) { - e.preventDefault(); - } - if (e.stopPropagation) { - e.stopPropagation(); - } - e.cancelBubble = true; - e.returnValue = false; - return false; - }); - - const clickStart = (ev: Event) => { - if (this.cooldownStart) { - return; - } - this.held = false; - let x; - let y; - if ((ev as TouchEvent).touches) { - x = (ev as TouchEvent).touches[0].pageX; - y = (ev as TouchEvent).touches[0].pageY; - } else { - x = (ev as MouseEvent).pageX; - y = (ev as MouseEvent).pageY; - } - this.timer = window.setTimeout(() => { - this.startAnimation(x, y); - this.held = true; - if (element.repeat && !element.isRepeating) { - element.isRepeating = true; - this.repeatTimeout = setInterval(() => { - element.dispatchEvent(new Event('ha-hold')); - }, element.repeat); - } - }, this.holdTime); - - this.cooldownStart = true; - window.setTimeout(() => (this.cooldownStart = false), 100); - }; - - const clickEnd = (ev: Event) => { - if ( - this.cooldownEnd - || (['touchend', 'touchcancel'].includes(ev.type) - && this.timer === undefined) - ) { - if (element.isRepeating && this.repeatTimeout) { - clearInterval(this.repeatTimeout); - element.isRepeating = false; - } - return; - } - clearTimeout(this.timer); - if (element.isRepeating && this.repeatTimeout) { - clearInterval(this.repeatTimeout); - } - element.isRepeating = false; - this.stopAnimation(); - this.timer = undefined; - if (this.held) { - if (!element.repeat) { - element.dispatchEvent(new Event('ha-hold')); - } - } else if (element.hasDblClick) { - if (this.nbClicks === 0) { - this.nbClicks += 1; - this.dblClickTimeout = window.setTimeout(() => { - if (this.nbClicks === 1) { - this.nbClicks = 0; - element.dispatchEvent(new Event('ha-click')); - } - }, 250); - } else { - this.nbClicks = 0; - clearTimeout(this.dblClickTimeout); - element.dispatchEvent(new Event('ha-dblclick')); - } - } else { - element.dispatchEvent(new Event('ha-click')); - } - this.cooldownEnd = true; - window.setTimeout(() => (this.cooldownEnd = false), 100); - }; - - element.addEventListener('touchstart', clickStart, { passive: true }); - element.addEventListener('touchend', clickEnd); - element.addEventListener('touchcancel', clickEnd); - element.addEventListener('mousedown', clickStart, { passive: true }); - element.addEventListener('click', clickEnd); - } - - private startAnimation(x: number, y: number) { - Object.assign(this.style, { - left: `${x}px`, - top: `${y}px`, - display: null, - }); - this.ripple.holdDown = true; // paper-ripple - this.ripple.simulatedRipple(); // paper-ripple - // this.ripple.disabled = false; - // this.ripple.active = true; - // this.ripple.unbounded = true; - } - - private stopAnimation() { - this.ripple.holdDown = false; // paper-ripple - // this.ripple.active = false; - // this.ripple.disabled = true; - this.style.display = 'none'; - } -} - -customElements.define('long-press-roku-card', LongPress); - -const getLongPress = (): LongPress => { - const body = document.body; - if (body.querySelector('long-press-roku-card')) { - return body.querySelector('long-press-roku-card') as LongPress; - } - - const longpress = document.createElement('long-press-roku-card'); - body.appendChild(longpress); - - return longpress as LongPress; -}; - -export const longPressBind = (element: LongPressElement) => { - const longpress: LongPress = getLongPress(); - if (!longpress) { - return; - } - longpress.bind(element); -}; - -export const longPress = directive(() => (part: PropertyPart) => { - longPressBind(part.committer.element); -}); diff --git a/src/roku-card.ts b/src/roku-card.ts index 28c1e59..ea60f80 100644 --- a/src/roku-card.ts +++ b/src/roku-card.ts @@ -7,15 +7,15 @@ import { CSSResult, css } from "lit-element"; -import { ifDefined } from "lit-html/directives/if-defined"; import { HomeAssistant, applyThemesOnElement, - handleClick + hasAction, + handleAction } from "custom-card-helpers"; import { RokuCardConfig } from "./types"; -import { longPress } from "./long-press"; +import { actionHandler } from "./action-handler-directive"; const defaultRemoteAction = { action: "call-service", @@ -25,7 +25,6 @@ const defaultRemoteAction = { @customElement("roku-card") class RokuCard extends LitElement { @property() public hass?: HomeAssistant; - @property() private _config?: RokuCardConfig; public getCardSize() { @@ -49,325 +48,57 @@ class RokuCard extends LitElement { return html``; } - const stateObj = this.hass.states[this._config.entity]; + const stateObj = this.hass!.states[this._config.entity]; + + if (!stateObj) { + return html` + +
Show Warning
+
+ `; + } return html`
+
${stateObj.attributes.app_name}
${this._config.tv || (this._config.power && this._config.power.show) - ? html` - - ` + ? this._renderButton("power", "mdi:power", "Power") : ""}
- ${this._config.back && this._config.back.show === false - ? html` - - ` - : html` - - `} - ${this._config.info && this._config.info.show === false - ? html` - - ` - : html` - - `} - ${this._config.home && this._config.home.show === false - ? html` - - ` - : html` - - `} + ${this._renderButton("back", "mdi:arrow-left", "Back")} + ${this._renderButton("info", "mdi:asterisk", "Info")} + ${this._renderButton("home", "mdi:home", "Home")}
- ${this._config.apps && this._config.apps.length > 0 - ? html` - - ` - : html` - - `} - ${this._config.up && this._config.up.show === false - ? html` - - ` - : html` - - `} - ${this._config.apps && this._config.apps.length > 1 - ? html` - - ` - : html` - - `} + ${this._renderImage(0)} + ${this._renderButton("up", "mdi:chevron-up", "Up")} + ${this._renderImage(1)}
- ${this._config.left && this._config.left.show === false - ? html` - - ` - : html` - - `} - ${this._config.select && this._config.select.show === false - ? html` - - ` - : html` - - `} - ${this._config.right && this._config.right.show === false - ? html` - - ` - : html` - - `} + ${this._renderButton("left", "mdi:chevron-left", "Left")} + ${this._renderButton( + "select", + "mdi:checkbox-blank-circle", + "Select" + )} + ${this._renderButton("right", "mdi:chevron-right", "Right")}
- ${this._config.apps && this._config.apps.length > 2 - ? html` - - ` - : html` - - `} - ${this._config.down && this._config.down.show === false - ? html` - - ` - : html` - - `} - ${this._config.apps && this._config.apps.length > 3 - ? html` - - ` - : html` - - `} + ${this._renderImage(2)} + ${this._renderButton("down", "mdi:chevron-down", "Down")} + ${this._renderImage(3)}
- ${this._config.reverse && this._config.reverse.show === false - ? html` - - ` - : html` - - `} - ${this._config.play && this._config.play.show === false - ? html` - - ` - : html` - - `} - ${this._config.forward && this._config.forward.show === false - ? html` - - ` - : html` - - `} + ${this._renderButton("reverse", "mdi:rewind", "Rewind")} + ${this._renderButton("play", "mdi:play-pause", "Play/Pause")} + ${this._renderButton("forward", "mdi:fast-forward", "Fast-Forward")}
${this._config.tv || @@ -376,81 +107,21 @@ class RokuCard extends LitElement { (this._config.volume_up && this._config.volume_up.show) ? html`
- ${this._config.volume_mute && - this._config.volume_mute.show === false - ? html` - - ` - : html` - - `} - ${this._config.volume_down && - this._config.volume_down.show === false - ? html` - - ` - : html` - - `} - ${this._config.volume_up && - this._config.volume_up.show === false - ? html` - - ` - : html` - - `} + ${this._renderButton( + "volume_mute", + "mdi:volume-mute", + "Volume Mute" + )} + ${this._renderButton( + "volume_down", + "mdi:volume-minus", + "Volume Down" + )} + ${this._renderButton( + "volume_up", + "mdi:volume-plus", + "Volume Up" + )}
` : ""} @@ -488,32 +159,94 @@ class RokuCard extends LitElement { display: flex; padding: 8px 36px 8px 36px; justify-content: space-evenly; + align-items: center; + } + .warning { + display: block; + color: black; + background-color: #fce588; + padding: 8px; + } + .app { + flex-grow: 3; + font-size: 20px; } `; } - private launchApp(e: Event): void { - const target = e.currentTarget as any; + private _renderImage(index: number): TemplateResult { + return this._config!.apps && this._config!.apps.length > index + ? html` + + ` + : html` + + `; + } - this.hass!.callService("media_player", "select_source", { - entity_id: this._config!.entity, - source: target.app - }); + private _renderButton( + button: string, + icon: string, + title: string + ): TemplateResult { + return this._config![button] && this._config![button].show === false + ? html` + + ` + : html` + + `; } - private _handleTap(ev): void { + private _handleAction(ev): void { const button = ev.currentTarget.button; - console.log(button); - const config = this._config![button]; - console.log(config); - let remote = this._config!.remote + const config = this._config![button] || ev.currentTarget.config; + const app = ev.currentTarget.app; + const remote = this._config!.remote ? this._config!.remote : "remote." + this._config!.entity.split(".")[1]; - handleClick( + + handleAction( this, this.hass!, config && config.tap_action ? config + : app + ? { + tap_action: { + action: "call-service", + service: "media_player.select_source", + service_data: { + entity_id: this._config!.entity, + source: app + } + }, + ...config + } : { tap_action: { service_data: { @@ -523,24 +256,7 @@ class RokuCard extends LitElement { ...defaultRemoteAction } }, - false, - false + ev.detail.action! ); } - - private _handleHold(ev): void { - const button = ev.currentTarget.button; - const config = this._config![button]; - if (config && config.hold_action) { - handleClick(this, this.hass!, config, true, false); - } - } - - private _handleDblTap(ev): void { - const button = ev.currentTarget.button; - const config = this._config![button]; - if (config && config.dbltap_action) { - handleClick(this, this.hass!, config, false, true); - } - } } diff --git a/src/types.ts b/src/types.ts index 561eba4..0f13d54 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,10 +29,13 @@ export interface ActionButtonConfig { show?: boolean; tap_action?: ActionConfig; hold_action?: ActionConfig; - dbltap_action?: ActionConfig; + double_tap_action?: ActionConfig; } export interface AppConfig { - icon?: string; - id?: string; + app?: string; + image?: string; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; } diff --git a/types/lit-element.d.ts b/types/lit-element.d.ts deleted file mode 100644 index 8b13789..0000000 --- a/types/lit-element.d.ts +++ /dev/null @@ -1 +0,0 @@ -