Repository: TBSniller/piccap Branch: main Commit: 930627851a41 Files: 33 Total size: 187.6 KB Directory structure: gitextract_z3_vjouk/ ├── .eslintignore ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ ├── piccap_bug.yml │ │ ├── piccap_documentation.yml │ │ ├── piccap_feature.yml │ │ └── piccap_question.yml │ └── workflows/ │ └── build_piccap.yml ├── .gitignore ├── .gitmodules ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── babel.config.json ├── docs/ │ └── DIY_Ambilight.md ├── frontend/ │ ├── appinfo.json │ ├── css/ │ │ ├── basicui.css │ │ ├── blackui.css │ │ ├── blueui.css │ │ └── darkui.css │ ├── index.html │ ├── js/ │ │ ├── domrect-polyfill.js │ │ ├── servicecalls.js │ │ ├── spatial-navigation.js │ │ └── ui.js │ └── webOSTVjs-1.2.4/ │ ├── LICENSE-2.0.txt │ ├── webOSTV-dev.js │ └── webOSTV.js ├── package.json ├── servicefiles/ │ ├── piccapautostart │ ├── services.json │ └── setuplegacylogging.sh └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ node_modules/ hyperion-webos/ build/ dist/ servicenative/ .vscode/ .github/ frontend/js/spatial-navigation.js frontend/js/domrect-polyfill.js ================================================ FILE: .eslintrc.js ================================================ module.exports = { env: { browser: true, }, extends: [ 'airbnb-base', 'plugin:compat/recommended' ], overrides: [ ], parserOptions: { ecmaVersion: 'latest', sourceType: 'module', }, rules: { 'no-console': 0, 'no-bitwise': 0, 'no-await-in-loop': 0, 'no-constant-condition': 0 }, }; ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: "Community: OpenLGTV Discord" url: https://discord.gg/9sqAgHVRhP about: Helpful community and first place to find already occured issues ================================================ FILE: .github/ISSUE_TEMPLATE/piccap_bug.yml ================================================ name: "PicCap bug report" description: "PicCap UI does not work as intendend" labels: ["bug"] body: - type: checkboxes attributes: label: PicCap related issues only description: | ⚠️ **ATTENTION:** This repository contains only components related to the UI. Please create your issue at [hyperion-webos](https://github.com/webosbrew/hyperion-webos/issues) if its related to capture/video/ambilight. options: - label: I belive that my issue is related to the PicCap UI and not to the [video capture backend](https://github.com/webosbrew/hyperion-webos) required: true - type: textarea id: osinfo attributes: label: WebOS Information description: | Please run `grep -h -E '"(hardware_id|core_os_release|product_id|webos_manufacturing_version|board_type)"' /var/run/nyx/*` on your TV and put the output in here render: json placeholder: | "board_type": "K6HP_DVB", "hardware_id": "HE_DTV_W20H_AFADABAA", "product_id": "65NANO867NA", "core_os_release": "5.3.0-21", "webos_manufacturing_version": "04.30.50", validations: required: true - type: input id: piccap_version attributes: label: PicCap Version description: "Which version do you use? (Check top left corner in PicCap)" placeholder: "0.5.1" validations: required: true - type: input id: description attributes: label: Description description: "What bug do you encounter? Formly describe it." placeholder: "The color of the app is changing to red randomly" validations: required: true - type: textarea id: reproduce attributes: label: Reproduction steps description: "How do you trigger this bug? Please walk us through it step by step." value: | 1. Open PicCap using magic remote 2. Click x 3. Click y ... render: text validations: required: true - type: input id: expected attributes: label: Expected result description: "What should have happen instead?" placeholder: "The color of the app should stay at blue how it was set." validations: required: true - type: textarea id: logs attributes: label: Logs / Screenshots description: "Please append screenshots or log output from PicCap's log tab." placeholder: "Drag and drop your screenshots here" render: text validations: required: false - type: textarea id: additional attributes: label: Additional information placeholder: "A workaround is to just hit the bulb button" render: text validations: required: false - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! ================================================ FILE: .github/ISSUE_TEMPLATE/piccap_documentation.yml ================================================ name: "PicCap documentation change" description: "We need to change or add some missing information in our documentation" labels: ["documentation"] body: - type: checkboxes attributes: label: PicCap related issues only description: | ⚠️ **ATTENTION:** This repository contains only components related to the UI. Please create your issue at [hyperion-webos](https://github.com/webosbrew/hyperion-webos/issues) if its related to capture/video/ambilight. options: - label: I belive that my issue is related to the PicCap UI and not to the [video capture backend](https://github.com/webosbrew/hyperion-webos) required: true - label: I have checked [open and closed issues](https://github.com/TBSniller/piccap/issues) to avoid duplicated issues required: true - label: I have checked [open and closed pull requests](https://github.com/TBSniller/piccap/pulls) to avoid duplicated issues required: true - type: textarea id: description attributes: label: Description description: "What part of the documentation do you want to change or add? Formly describe it." placeholder: "Please change the URL of this item" render: text validations: required: true - type: textarea id: additional attributes: label: Additional information placeholder: "Drag and drop images here if needed" render: text validations: required: false - type: markdown attributes: value: | Thanks for taking the time to fill out this documentation change request! ================================================ FILE: .github/ISSUE_TEMPLATE/piccap_feature.yml ================================================ name: "PicCap feature request" description: "This new UI feature does not exists and would be nice to have" labels: ["feature request"] body: - type: checkboxes attributes: label: PicCap related issues only description: | ⚠️ **ATTENTION:** This repository contains only components related to the UI. Please create your issue at [hyperion-webos](https://github.com/webosbrew/hyperion-webos/issues) if its related to capture/video/ambilight. options: - label: I belive that my issue is related to the PicCap UI and not to the [video capture backend](https://github.com/webosbrew/hyperion-webos) required: true - label: I have checked [open and closed issues](https://github.com/TBSniller/piccap/issues?q=is%3Aissue%20state%3Aopen%20) to avoid duplicated issues required: true - label: Please check this box if we have a already existing backend option missing in PicCap UI required: false - type: textarea id: description attributes: label: Description description: "What feature do you want to have? Formly describe it." placeholder: "Switch color design to red would be nice" render: text validations: required: true - type: textarea id: additional attributes: label: Additional information placeholder: "Drag and drop images here if needed" render: text validations: required: false - type: markdown attributes: value: | Thanks for taking the time to fill out this feature request! ================================================ FILE: .github/ISSUE_TEMPLATE/piccap_question.yml ================================================ name: "PicCap question" description: "I can't find an answer to my question" labels: ["question"] body: - type: checkboxes attributes: label: PicCap related issues only description: | ⚠️ **ATTENTION:** This repository contains only components related to the UI. Please create your issue at [hyperion-webos](https://github.com/webosbrew/hyperion-webos/issues) if its related to capture/video/ambilight. options: - label: I belive that my question is related to the PicCap UI and not to the [video capture backend](https://github.com/webosbrew/hyperion-webos) required: true - label: I have checked [open and closed issues](https://github.com/TBSniller/piccap/issues?q=is%3Aissue%20state%3Aopen%20) to avoid duplicated issues required: true - label: My question is not related to DRM protected content as it's already answered in [this issue](https://github.com/TBSniller/piccap/issues/38) required: true - type: textarea id: question attributes: label: Question description: "What question do you have?" placeholder: "How can I change the color?" render: text validations: required: true - type: textarea id: additional attributes: label: Additional information placeholder: "Drag and drop images here if needed" render: text validations: required: false ================================================ FILE: .github/workflows/build_piccap.yml ================================================ name: Build PicCap with hyperion-webos and create IPK on: push: pull_request: workflow_dispatch: env: TOOLCHAIN_URL: https://github.com/openlgtv/buildroot-nc4/releases/download/webos-d7ed7ee/arm-webos-linux-gnueabi_sdk-buildroot.tar.gz TOOLCHAIN_SHA256: 32816626e99fb34922a49d0c639f7c8a30356fffb222372d4823027f1382f640 TOOLCHAIN_DIR: /opt/arm-webos-linux-gnueabi_sdk-buildroot TOOLCHAIN_FILE: /opt/arm-webos-linux-gnueabi_sdk-buildroot/share/buildroot/toolchainfile.cmake jobs: build-all: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: submodules: recursive fetch-depth: 0 - uses: actions/setup-node@v3 with: node-version: 18 - name: Install webOS CLI run: sudo npm install --location=global @webosose/ares-cli - name: Install dependencies working-directory: ${{github.workspace}} run: npm install - name: Download and unpack toolchain working-directory: /opt run: | wget -q -O toolchain.tar.gz ${TOOLCHAIN_URL} echo "${TOOLCHAIN_SHA256} toolchain.tar.gz"|sha256sum -c - tar xf toolchain.tar.gz - name: Relocate toolchain working-directory: ${{ env.TOOLCHAIN_DIR }} run: | ./relocate-sdk.sh - name: Check CMAKE Version run: which cmake && cmake --version - name: Build PicCap, hyperion-webos and copy files working-directory: ${{github.workspace}} run: npm run-script build-all - name: Package Frontend and Service run: npm run-script package - name: List files run: find . && find ./build - name: Upload PicCap IPK uses: actions/upload-artifact@v4 with: name: piccap_ipk path: ./build/*.ipk - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: files: ./build/*.ipk ================================================ FILE: .gitignore ================================================ build .vscode/* !.vscode/settings.json node_modules ================================================ FILE: .gitmodules ================================================ [submodule "hyperion-webos"] path = hyperion-webos url = https://github.com/webosbrew/hyperion-webos ================================================ FILE: .vscode/settings.json ================================================ { "cmake.sourceDirectory": "${workspaceFolder}/hyperion-webos", "cmake.buildDirectory": "${workspaceFolder}/hyperion-webos/build" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 webOS Brew 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. ================================================ FILE: README.md ================================================ # PicCap - Hyperion Sender App | Ambilight for LG WebOS TVs ## What's this? ### PicCap? PicCap is an frontend app, which you can install on your TV, to make TV content capturing as easy as possible. It ships and controls the seperated [hyperion-webos](https://github.com/webosbrew/hyperion-webos) background native service, which uses capture interfaces on your TV based on reverse engineering, proccesses the output and sends as result a low quality image to a receiver like Hyperion's flatbuffer server. On newer TVs there is no official way for capturing DRM-protected content like from Netflix or Amazon. This restriction doesn't take place for content comming from an HDMI input. So currently as a workaround you can play your media using your PC, FireTV-Stick or Chromecast and still enjoy your LEDs. ### Hyperion? [hyperion.ng](https://github.com/hyperion-project/hyperion.ng) basicly is a server service, which transforms incomming image data to an LED output. The idea is to have an ambilight like it's known from Philipps TVs. It is used in DIY-environments, like builded up in [this tutorial](https://github.com/TBSniller/piccap/blob/main/docs/DIY_Ambilight.md). You can also run [Hyperion.ng webOS loader](https://github.com/webosbrew/hyperion.ng-webos-loader) or it's fork [HyperHDR webOS loader](https://github.com/webosbrew/hyperhdr-webos-loader) directly on your webOS TV, so you don't need any further hardware, expect of your LED driver. Both apps can be found in [Homebrew Channels app repository](https://repo.webosbrew.org/apps/). https://user-images.githubusercontent.com/51515147/192116249-f5080f02-3566-4c94-b9e1-bdb11aba2698.mp4 This footage is captured on a webOS5-TV using vtCapture as library. ## How to install ### What do you need? - [Root access](https://cani.rootmy.tv) to your TV - webOS 3.4 or above - Latest version of [Homebrew Channel](https://github.com/webosbrew/webos-homebrew-channel) installed, as we take use of its elevate-service script - Brain with some basic knowledge - We haven't encountered any bricks, but standard no warranty clause applies ### Easy way Open Homebrew Channel and install PicCap directly from there. ### Manual way First you will have to [build](https://github.com/TBSniller/piccap#development) it from scratch, or download pre-compiled IPK from [releases](https://github.com/TBSniller/piccap/releases). ``` # Copy IPK to TV scp /home/[USER]/downloads/org.webosbrew.piccap_[version]_all.ipk root@[TVIP]:/tmp/org.webosbrew.piccap_[version]_all.ipk # On TV install IPK luna-send -i -f luna://com.webos.appInstallService/dev/install '{"id":"org.webosbrew.piccap","ipkUrl":"/tmp/org.webosbrew.piccap_[version]_all.ipk","subscribe":true}' ``` ## How to use ### First start Wait a few secounds to let the service elevate root permissions through Homebrew Channel-Service. Check status message in bottom right corner, to see when it's done. ### Settings - If you use hyperion.ng or HyperHDR loader, you will have to fill `127.0.0.1` as IP address. - Change priority if you have other capture or effect sources for your Hyperion or HyperHDR instance. ### Backends We use different libraries to capture TVs content. These are used by hyperion-webos and described [here](https://github.com/webosbrew/hyperion-webos/tree/main#backends). ### Advanced settings Some TV models are comptabile with a specific backend, but require a slightly different routine to work reliably. You can find an explaination for these so called quirks [here](https://github.com/webosbrew/hyperion-webos/tree/main#quirks). ## Development ### Dependencies To build PicCap and hyperion-webos you will need: - [Node.js](https://nodejs.org/en/download/) - [buildroot-nc4](https://github.com/openlgtv/buildroot-nc4) You will also need `clang-format-14` if you want to contribute. ### How to build We have tried to make build process as easy as possible. After building all files can be found in `./build`. ``` # Setup buildroot-nc4 (needed for hyperion-webos) cd /desired/path wget -O toolchain.tar.gz $TOOLCHAIN_URL_FROM_RELEASES tar -xvzf toolchain.tar.gz rm toolchain.tar.gz arm-webos-linux-gnueabi_sdk-buildroot/relocate-sdk.sh export TOOLCHAIN_FILE=/desired/path/arm-webos-linux-gnueabi_sdk-buildroot/share/buildroot/toolchainfile.cmake # Clone project and submodules git clone --recursive https://github.com/TBSniller/piccap.git cd ./piccap # Install node dependencies npm install # Build npm run-script build-all # Build PicCap & hyperion-webos + deps npm run-script build-frontend # Build PicCap only npm run-script build-backend # Build hyperion-webos + deps only # Package IPK-file for TV installation npm run-script package ``` ## Other ### Known issues **Expect bugs - This app is still in early development** Attention! New AI options have been added to newer LG models. Since the 'AI Picture Pro', 'AI Brightness', 'AI Genre Selection', 'AI Image Game Optimizer' and 'AI Game Sound' options use the same recording process as Hyperion WebOS, dropouts may occur during recording. Consequently, a live preview in HyperHDR is temporarily unavailable, and the LEDs also switch off briefly. Workaround: Deactivate these options (can be found under “Settings” > ‘General’ > “AI service”). Depending on the model, this can also be done under “Settings” > ‘General’ > “AI service”. Please see [hyperion-webos#known-issues](https://github.com/webosbrew/hyperion-webos/tree/main#known-issues) for issues regarding the backend service. - This only is the frontend application and has nothing to do with capture related things! Version tracker is available in [hyperion-webos#16](https://github.com/webosbrew/hyperion-webos/issues/16). ### Credits This project would never ever exist without help from [@Mariotaku](https://github.com/mariotaku) and [@Informatic](https://github.com/Informatic). Both programmed important things at the beginning of this whole ambilight project. [@tuxuser](https://github.com/tuxuser) also made some important changes in the mid of this project. Share them some love if you can, they taught and showed me alot! You should also check out the other contributors. Some nice enhancements wouldn't be there without them :) Check out [OpenLGs-Discord](https://discord.gg/9sqAgHVRhP) server, if you have some questions. You will find a very helpful community. <3 ### Screenshots ![Main](https://user-images.githubusercontent.com/51515147/192116360-efd6483a-8dc7-4b61-af27-d428f90740a3.jpg) ![Settings](https://user-images.githubusercontent.com/51515147/192116365-1f9a7590-7a88-4bfd-924f-3b8bf9ae47b6.jpg) ![Advanced settings](https://user-images.githubusercontent.com/51515147/192116371-21387541-7c56-4866-b7fd-23246a9a4d2e.jpg) ![Logs](https://user-images.githubusercontent.com/51515147/192116386-df4e5e3c-7260-451e-8685-69a69a3abc0b.jpg) ![About](https://user-images.githubusercontent.com/51515147/192116388-6dce877d-41be-41b1-922e-630783fbca7c.jpg) ================================================ FILE: babel.config.json ================================================ { "presets": [ [ "@babel/preset-env", { "targets": { "chrome": "38" }, "useBuiltIns": "entry", "corejs": "3" } ] ] } ================================================ FILE: docs/DIY_Ambilight.md ================================================ ## Requirements (Maybe differs for your country) - Raspberrry Pi 3 or 4 + Stuff (MicroSD, Power, maybe a Case,..) - 5V 8A Power Source (I am using this: https://www.amazon.de/borui-uk-Netzteil-MeanWell-LPV-60-5-Schaltnetzteil/dp/B017U7JGGY) - Old Powercord - WS2801 LED Stripes (I am using this: https://www.amazon.de/gp/product/B072B6561S/ You can get them cheaper on AliExpress + Measure up your TV for real cm needed) - Jumping Wires (Like: https://www.amazon.de/gp/product/B00OK74ABO) - Few Wagos (Like: https://www.amazon.de/Wago-Verbindungsklemme-221-413-50-St%C3%BCck/dp/B00JB3U9CG) - Soldering iron and solder - Datacable (I recommend these, if you have a long way from TV to Pi, because they are shieded: https://www.amazon.de/Busleitung-Datenleitung-Datenkabel-Installationsbusleitung-Telekommunikationskabel/dp/B071W5TQPY/ otherwise use the next one cables) - Power cables (LED | Like: https://www.amazon.de/WITTKOWARE-Sortiment-Schaltdr%C3%A4hte-5mm-10m/dp/B075V79HGF/) ## Hardware ## TV preparing 1. Measure up each side of your TV and cut the stripe at the edges. 2. There are arrows under the sticky tape. Make sure all of them are looking in the same direction: -> -> not -><-. Data and clock cannot flow if they do not go in one direction. 3. Tape them on your TV. You have two options. Begin your input stripe in one of the corners, or let it begin anywhere. You can set it later in the settings of hyperion. 4. Use your soldering iron and powercables to connect the corners. You also have to connect (!!ONLY!!) GND and V+ with the end and the beginning of your stripe, to prevent powerloss over a long ledstripe. You can also use some edge connectors (like: https://www.amazon.de/dp/B08C2M18XF), but in this case they are a way to small and you have to cut them a bit (Trust me you would not do this.. took me 4 hours to get this sh\*\* working). 5. Get your datacable or another powercables ready. Connect them to CLK(CK) and SI(DI) on your input stripe. They have to be as long, as your way to your Pi is. After that you can use the jumpercables to solder one end with the ledstripe cable and use the other end as GPIO connector. I got flickering issues (caused by the powernet) so I decided to wiggle the data cables with aluminum foil. Flickering disappeard. You can avoid this if you buy the datacable right at first. #### Power source preparing --MAKE SURE YOU KNOW WHAT YOU ARE DOING! IF NOT ASK AN ELECTRICAN! THIS EXAMPLE IS FOR GERMANY!! -- 1. Get your power source, the Wagos, a cutted old powercode and connect it together: 2. Get your power source, the Wagos, the powercables and connect them together: #### Led+Pi connection 1. Connect GND(Black) and V+(Red) with your LED-Stripe 2. Connect GND(Black) also with a GND Pin of the Pi (eg. Pin 9 | **Pi 3/4**) You can find a plan here: https://www.elektronik-kompendium.de/sites/raspberry-pi/fotos/raspberry-pi-15b.jpg (Pi 3 and 4 are the same. **Pi 2 is different!**) 3. Connect CLK(CK) of your input stripe with Pin 23 4. Connect SI(DI) of your input stripe with Pin 19 Here is a very nice picture, how to look after all: https://digitalewelt.at/wp-content/uploads/2018/02/Ambilight-Projekt-Verkabelung-LED-Netzteil.jpg ## Software - Add **dtparam=spi=on** to the end of your /boot/config.txt and reboot - Installation and usage guide: https://docs.hyperion-project.org/en/user/Installation.html#requirements - After installation you have to setup the WS2801s. Therefore in Hyperion go to Configuration -> Led-Hardware -> Controller type: **WS2801**, SPI path: **/dev/spidev0.0**, , Baudrate: **1000000**. Change RGB byte order if you experience wrong colors. Mine are on **RGB** - Count your LEDs and pass them to the LED-Layout tab. ## Credits Build it myself using this guide: https://digitalewelt.at/ambilight-mit-dem-raspberry-pi-3-4k-schritt-fuer-schritt-anleitung/ ================================================ FILE: frontend/appinfo.json ================================================ { "id": "org.webosbrew.piccap", "version": "0.5.2", "vendor": "Homebrew", "type": "web", "main": "index.html", "title": "PicCap", "appDescription": "Hyperion Sender App - Ambilight for WebOS", "icon": "assets/logo_small.png", "largeIcon": "assets/logo_big.png", "splashBackground": "assets/splash_1080p.png", "bgImage": "assets/splash_1080p.png", "iconColor": "#000000", "supportGIP": true } ================================================ FILE: frontend/css/basicui.css ================================================ *{ box-sizing: border-box; margin: 0; padding: 0; font-family: 'Poppins',sans-serif; } ::selection{ background: #fff; color: #000; } .menu .logo p{ display: inline; } nav{ position: fixed; width: 100%; z-index: 12; } .menu{ margin: auto; display: flex; align-items: center; justify-content: space-between; padding: 10px 100px 10px; transition: all 0.3s ease; } .menu .logo p{ text-decoration: none; font-size: 35px; font-weight: 600; } .menu ul{ display: inline-flex; } .menu ul li{ list-style: none; margin-left: 7px; } .menu ul li:first-child{ margin-left: 0; } .menu ul li button{ text-decoration: none; font-size: 18px; font-weight: 500; padding: 8px 15px; transition: all 0.3s ease; } .background{ position: absolute; height: 100%; width: 100%; transition: all 0.3s ease; } .main{ position: absolute; top: 52%; left: 50%; transform: translate(-50%, -50%); width: 100%; padding: 0 20px; text-align: center; } .main .title{ font-size: 55px; font-weight: 600; } .main .title p{ display: inline; } .main .sub_title{ font-size: 32px; font-weight: 600; } .main .btns{ margin-top: 20px; } .main .btns button{ height: 55px; width: 170px; margin: 0 10px; font-size: 20px; font-weight: 500; padding: 0 10px; cursor: pointer; transition: all 0.3s ease; } .service { display: block; } .settings { display: none; } .logs { display: none; } .about { display: none; } .status { position: fixed; bottom: 0; width: 100%; padding: 10px 10px; transition: all 0.3s ease; } .status .lightMode{ display: inline-table; } .status .info { display: inline-table; float: right; margin-top: 4px; } .status .info p { text-decoration: none; display: inline; text-align: right; font-size: 17px; } .lightMode button { display: inline-block; padding: 1px 6px; font-size: 16px; font-weight: 500; cursor: pointer; margin: 0 1px; transition: all 0.3s ease; } .lightMode img { width: 20px; height: 20px; margin: 0px 0px -3px 0px; } #txtPicCapVersion { color: #fff; font-size: 11px; font-weight: 600; } #txtServiceVersion { color: #fff; font-size: 11px; font-weight: 600; } .settings { margin-left: 15%; margin-right: 15%; } .advSettingitems { display: none; } .settingItemsAdv { display: none; } .settingItemsNormal { display: block; } .settingItems { text-align: center; } .settingItems p{ font-size: 20px; font-weight: 500; margin-bottom: 2px; } .settingItems ul{ display: inline-flex; margin-bottom: 20px; } .settingItems ul li{ text-align: left; list-style: none; margin-left: 10px; margin-right: 10px; } .settingItems input[type="text"]{ border-style: none; border-radius: 5px; height: 30px; min-width: 150px; padding: 0 20px; font-size: 15px; } .checkboxes{ text-align: right; margin-left: 20px; } .checklabel label{ font-size: 19px; font-weight: 500; } .settingItems input[type="checkbox"]{ height: 25px; width: 25px; padding: 0 20px; vertical-align: middle; margin-left: 7px; margin-bottom: 4px; } .settingItems select{ height: 30px; min-width: 230px; padding: 0 20px; font-size: 15px; } .manualres { display: none; } .settingsocket { display: none } .manualsocket { display: none } .settingaddressport { display: flex } .avatars img{ width: 30px; height: 30px; border-radius: 100%; margin-right: 10px; } .avatars p{ display: flex; align-items:center; } .aboutItems { text-align: center; } .aboutItems p{ font-size: 20px; font-weight: 500; margin-bottom: 15px; text-align: center; } .aboutItems ul{ display: inline-flex; margin-right: 20px; } .aboutItems ul li{ text-align: left; list-style: none; margin-right: 10px; } .upperTab p{ font-size: 20px; font-weight: 500; color: #fff; margin-bottom: 15px; text-align: center; } .logsBtns { display: block; } .upperTab .btns button{ width: 200px; margin-bottom: 10px; } .logBox textarea{ resize: none; width: 95%; height: 500px; } .consoleLog { display: block; height: 95%; } .hyperionLog { display: none; } ================================================ FILE: frontend/css/blackui.css ================================================ .menu.blackMode { background: black; border-bottom: 2px solid grey; } .menu .logo.blackMode p{ color: #fff; } .menu.blackMode ul li button{ color: #fff; border-radius: 5px; border-style: none; background: none; } .menu.blackMode ul li button:hover{ background: #fff; color: black; } .background.blackMode{ background: black; } .main .title.blackMode{ color: #fff; } .main .sub_title.blackMode{ color: #fff; } .main .btns.blackMode button{ border-radius: 5px; border: 2px solid white; outline: none; background: none; color: #fff; } .btns.blackMode button:hover{ background: white; color: black; } .status.blackMode { background: black; border-top: 2px solid grey; } .status .info.blackMode p { color: #fff; } .lightMode.blackMode button { outline: none; border: 2px solid white; border-radius: 5px; background-color: #0d0d0d; } .lightMode.blackMode button:hover{ background-color: white; color: black; } .lightMode.blackMode button:focus { background-color: white; color: black; border: 2px solid black; outline: 1px solid white; } .menu.blackMode :focus { border-style: solid; border-radius: 3px; border: 3px solid black; outline: 1px solid white; } .main.blackMode button:focus { background: white; color: black; border: 2px solid black; outline: 1px solid white; } .main.blackMode button:active { background: lightgray; } .settingItems.blackMode p{ color: #fff; } .checklabel.blackMode label{ color: #fff; } .settingItems.blackMode input[type="checkbox"]{ border-style: none; border-radius: 5px; } .settingItems.blackMode select{ border-style: none; border-radius: 5px; background-color: white; } .settingItems.blackMode :focus{ border-style: solid; border: 2px solid black; outline: 1px solid white; } .settingItems.blackMode button:active{ background: lightgray; } .settingItems.blackMode input:focus{ border-style: solid; border-radius: 2px; border: 2px solid black; outline: 1px solid white; } .checkboxes.blackMode input[type="checkbox"]:focus{ outline: 2px solid black; outline: 1px solid white; } .aboutItems.blackMode p{ color: #fff; } .aboutItems.blackMode a{ color: white; } .aboutItems.blackMode :focus { border-style: solid; border-radius: 3px; border: 3px solid white; } .upperTab.blackMode p{ color: #fff; } .logBox.blackMode textarea{ color: white; background-color: black; border: 1px solid white; } .logBox.blackMode textarea:focus{ background-color: black; border: 2px solid white; outline: 1px solid white; } ================================================ FILE: frontend/css/blueui.css ================================================ .menu.blueMode { background: #0d47a1; } .menu .logo.blueMode p{ color: #fff; } .menu.blueMode ul li button{ color: #fff; border-radius: 5px; border-style: none; background: none; } .menu.blueMode ul li button:hover{ background: #fff; color: black; } .background.blueMode{ background: #1e88e5; } .main .title.blueMode{ color: #fff; } .main .sub_title.blueMode{ color: #fff; } .main .btns.blueMode button{ border-radius: 5px; border: 2px solid white; outline: none; background: none; color: #fff; } .btns.blueMode button:hover{ background: white; color: black; } .status.blueMode { background: #1565c0; } .status .info.blueMode p { color: #fff; } .lightMode.blueMode button { outline: none; border: 2px solid white; border-radius: 5px; background-color: transparent; } .lightMode.blueMode button:hover{ background-color: white; color: black; } .lightMode.blueMode button:focus { background-color: white; color: black; border: 2px solid black; } .menu.blueMode :focus { border-style: solid; border-radius: 3px; border: 3px solid black; } .main.blueMode button:focus { background: white; color: black; border: 2px solid black; } .main.blueMode button:active { background: lightgray; } .settingItems.blueMode p{ color: #fff; } .checklabel.blueMode label{ color: #fff; } .settingItems.blueMode input[type="checkbox"]{ border-style: none; border-radius: 5px; } .settingItems.blueMode select{ border-style: none; border-radius: 5px; background-color: white; } .settingItems.blueMode :focus{ border-style: solid; border-radius: 2px; border: 2px solid black; } .settingItems.blueMode button:active{ background: lightgray; } .settingItems.blueMode input:focus{ border-style: solid; border-radius: 2px; border: 2px solid black; } .checkboxes.blueMode input[type="checkbox"]:focus{ outline: 2px solid black; } .aboutItems.blueMode p{ color: #fff; } .aboutItems.blueMode a{ color: white; } .aboutItems.blueMode :focus { border-style: solid; border-radius: 3px; border: 3px solid white; } .upperTab.blueMode p{ color: #fff; } .logBox.blueMode textarea{ color: white; background-color: #1e88e5; border: 1px solid white; } .logBox.blueMode textarea:focus{ background-color: #1e88e5; border: 2px solid white; outline: 1px solid white; } ================================================ FILE: frontend/css/darkui.css ================================================ .menu.darkMode { background: #303030; } .menu .logo.darkMode p{ color: #fff; } .menu.darkMode ul li button{ color: #fff; border-radius: 5px; border-style: none; background: none; } .menu.darkMode ul li button:hover{ background: #fff; color: black; } .background.darkMode{ background: #424242; } .main .title.darkMode{ color: #fff; } .main .sub_title.darkMode{ color: #fff; } .main .btns.darkMode button{ border-radius: 5px; border: 2px solid white; outline: none; background: none; color: #fff; } .btns.darkMode button:hover{ background: white; color: black; } .status.darkMode { background: #212121; } .status .info.darkMode p { color: #fff; } .lightMode.darkMode button { outline: none; border: 2px solid white; border-radius: 5px; background-color: transparent; } .lightMode.darkMode button:hover{ background-color: white; color: black; } .lightMode.darkMode button:focus { background-color: white; color: black; border: 2px solid black; } .menu.darkMode :focus { border-style: solid; border-radius: 3px; border: 3px solid black; } .main.darkMode button:focus { background: white; color: black; border: 2px solid black; } .main.darkMode button:active { background: lightgray; } .settingItems.darkMode p{ color: #fff; } .checklabel.darkMode label{ color: #fff; } .settingItems.darkMode input[type="checkbox"]{ border-style: none; border-radius: 5px; } .settingItems.darkMode select{ border-style: none; border-radius: 5px; background-color: white; } .settingItems.darkMode :focus{ border-style: solid; border-radius: 2px; border: 2px solid black; } .settingItems.darkMode button:active{ background: lightgray; } .settingItems.darkMode input:focus{ border-style: solid; border-radius: 2px; border: 2px solid black; } .checkboxes.darkMode input[type="checkbox"]:focus{ outline: 2px solid black; } .aboutItems.darkMode p{ color: #fff; } .aboutItems.darkMode a{ color: white; } .aboutItems.darkMode :focus { border-style: solid; border-radius: 3px; border: 3px solid white; } .upperTab.darkMode p{ color: #fff; } .logBox.darkMode textarea{ color: white; background-color: #424242; border: 1px solid white; } .logBox.darkMode textarea:focus{ background-color: #424242; border: 2px solid white; outline: 1px solid white; } ================================================ FILE: frontend/index.html ================================================ PicCap - Hyperion Sender App

hyperion-webos

loading..

Loading status..
  • Service settings



  • Socket

  • Socket path

  • Address

  • Port

  • Hyperion priority


  • Resolution

  • Width


  • Height

  • Maximal FPS


  • _______________________________________________


  • Video capture backend

  • Graphical capture backend



  • Service advanced settings




  • _______________________________________________




  • _______________________________________________




Some very simple experimental feature to collect logs. Setup logging is needed after a reboot.
Will be reworked in newer versions. Press the load button to get last 200 log entries.

Some info about this project


  • PicCap is the frontend app, which you have installed on your TV and you can see here, to make things as easy as possible. It ships and controls the seperated hyperion-webos background service, which controls the capture interfaces on your TV based on reverse engineering, proccesses the output and sends the resulting low quality image data to a receiver like Hyperion's flatbuffer server.
    On newer TVs there is no official way for capturing DRM-protected content like Netflix or Amazon. This restriction doesn't take place for content comming from an HDMI input.
    So currently as a workaround you can play your media using your PC, FireTV-Stick or Chromecast and still enjoy your LEDs.
    This app requires to be run as root and tries to do this at the first start using the Homebrew Channel.

  • _______________________________________________



  • Feel free to raise an issue or pull request, or come to the OpenLG-Discord, if you have some questions.




  • _______________________________________________



  • Some love to everyone who was, or still is involved into this project and of course the OpenLG-/Hyperion-Community! ♥



  • hyperion-webos

  • PicCap

State:

Loading..

| Receiver:

n/a

| UI:

n/a

| Video:

n/a

| FPS:

n/a

================================================ FILE: frontend/js/domrect-polyfill.js ================================================ // // https://raw.githubusercontent.com/Financial-Times/polyfill-library/c25c30e4463bef60fba1213ecb697f3e3f253d7b/polyfills/DOMRect/polyfill.js // License: MIT // (function (global) { function number(v) { return v === undefined ? 0 : Number(v); } function different(u, v) { return u !== v && !(isNaN(u) && isNaN(v)); } function DOMRect(xArg, yArg, wArg, hArg) { var x, y, width, height, left, right, top, bottom; x = number(xArg); y = number(yArg); width = number(wArg); height = number(hArg); Object.defineProperties(this, { x: { get: function () { return x; }, set: function (newX) { if (different(x, newX)) { x = newX; left = right = undefined; } }, enumerable: true }, y: { get: function () { return y; }, set: function (newY) { if (different(y, newY)) { y = newY; top = bottom = undefined; } }, enumerable: true }, width: { get: function () { return width; }, set: function (newWidth) { if (different(width, newWidth)) { width = newWidth; left = right = undefined; } }, enumerable: true }, height: { get: function () { return height; }, set: function (newHeight) { if (different(height, newHeight)) { height = newHeight; top = bottom = undefined; } }, enumerable: true }, left: { get: function () { if (left === undefined) { left = x + Math.min(0, width); } return left; }, enumerable: true }, right: { get: function () { if (right === undefined) { right = x + Math.max(0, width); } return right; }, enumerable: true }, top: { get: function () { if (top === undefined) { top = y + Math.min(0, height); } return top; }, enumerable: true }, bottom: { get: function () { if (bottom === undefined) { bottom = y + Math.max(0, height); } return bottom; }, enumerable: true } }); } global.DOMRect = DOMRect; }(self)); ================================================ FILE: frontend/js/servicecalls.js ================================================ require('core-js/stable'); const availableQuirks = { // DILE_VT QUIRK_DILE_VT_CREATE_EX: '0x1', QUIRK_DILE_VT_NO_FREEZE_CAPTURE: '0x2', QUIRK_DILE_VT_DUMP_LOCATION_2: '0x4', // vtCapture QUIRK_VTCAPTURE_FORCE_CAPTURE: '0x100', }; let isRoot = false; let rootingInProgress = false; function logIt(message) { const textareaConsoleLog = document.getElementById('textareaConsoleLog'); console.log(message); textareaConsoleLog.value += `${message}\n`; } function onHBExec(result) { if (result.returnValue === true) { logIt(`HBChannel exec returned. stdout: ${result.stdoutString} stderr: ${result.stderrString}`); } else { logIt(`HBChannel exec failed! Code: ${result.errorCode}`); } } function killHyperion() { document.getElementById('txtInfoState').innerHTML = 'Killing service..'; logIt('Calling HBChannel exec to kill hyperion-webos'); /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.hbchannel.service', { method: 'exec', parameters: { command: 'kill -9 $(pidof hyperion-webos)', }, onSuccess: onHBExec, onFailure: onHBExec, }, ); /* eslint-enable no-undef */ } function makeServiceRoot() { logIt('Rooting..'); document.getElementById('txtInfoState').innerHTML = 'Rooting app and service..'; logIt('Calling HBChannel exec to elevate app and service'); /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.hbchannel.service', { method: 'exec', parameters: { command: '/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service org.webosbrew.piccap; /media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service org.webosbrew.piccap.service', }, onSuccess(result) { onHBExec(result); logIt('Elevation completed - killing service process..'); document.getElementById('txtInfoState').innerHTML = 'Killing service..'; killHyperion(); }, onFailure: onHBExec, }, ); /* eslint-enable no-undef */ document.getElementById('txtInfoState').innerHTML = 'Finished making root processing'; } let checkRootStatusIntervalID = null; function onCheckRootStatus(result) { if (result.returnValue === true) { if (result.elevated) { logIt('PicCap-Service returned rooted!'); document.getElementById('txtInfoState').innerHTML = 'Running as root'; isRoot = true; clearInterval(checkRootStatusIntervalID); rootingInProgress = false; } else { if (rootingInProgress === false) { logIt('Rooting not in progress yet.'); makeServiceRoot(); rootingInProgress = true; } logIt('PicCap-Service returned not rooted yet! Will check again soon.'); document.getElementById('txtInfoState').innerHTML = 'Not running as root. Service elevation in progress..'; } } else { logIt(`Getting root-status from PicCap-Service failed! Will try again. Code: ${result.errorCode}`); document.getElementById('txtInfoState').innerHTML = 'PicCap-Service status failed!'; } } function checkRoot() { document.getElementById('txtServiceStatus').innerHTML = 'Processing root check'; logIt('Starting loop for PicCap-Service to get root-status'); let firstInterval = true; checkRootStatusIntervalID = window.setInterval(() => { logIt('Calling PicCap-Service to get root-status'); document.getElementById('txtInfoState').innerHTML = 'Checking root status'; /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.piccap.service', { method: 'status', parameters: {}, onSuccess: onCheckRootStatus, onFailure: onCheckRootStatus, }, ); /* eslint-enable no-undef */ if (rootingInProgress === false && isRoot === false && firstInterval === false) { logIt('Not rooted and rooting not in progress yet.'); makeServiceRoot(); rootingInProgress = true; } firstInterval = false; }, 3000); } function getStatus() { document.getElementById('txtInfoState').innerHTML = 'Getting status info..'; /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.piccap.service', { method: 'status', parameters: {}, onSuccess(result) { if (result.returnValue === true) { document.getElementById('txtServiceVersion').innerHTML = result.version; document.getElementById('txtServiceStatus').innerHTML = result.isRunning ? 'Capturing' : 'Not capturing'; document.getElementById('txtInfoReceiver').innerHTML = result.connected ? 'Connected' : 'Disconnected'; document.getElementById('txtInfoVideo').innerHTML = result.videoRunning ? `Capturing with ${result.videoBackend}` : 'Not capturing'; document.getElementById('txtInfoUI').innerHTML = result.uiRunning ? `Capturing with ${result.uiBackend}` : 'Not capturing'; document.getElementById('txtInfoFPS').innerHTML = result.framerate.toFixed(2); /* Round to 2 decimal points */ document.getElementById('txtInfoState').innerHTML = 'Status info refreshed'; } else { logIt('Getting status info from PicCap-Service failed! Return value false!'); document.getElementById('txtInfoState').innerHTML = 'Getting status info failed!'; } }, onFailure(result) { logIt(`Getting status info from PicCap-Service failed! Code: ${result.errorCode}`); document.getElementById('txtInfoState').innerHTML = 'Getting status info failed!'; }, }, ); /* eslint-enable no-undef */ } function getSettings() { document.getElementById('txtInfoState').innerHTML = 'Loading settings..'; /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.piccap.service', { method: 'getSettings', parameters: {}, onSuccess(result) { if (result.returnValue === true) { document.getElementById('selectSettingsVideoBackend').value = result.novideo === true ? 'disabled' : result.backend || 'auto'; document.getElementById('selectSettingsGraphicalBackend').value = result.nogui === true ? 'disabled' : result.uibackend || 'auto'; document.getElementById('checkSettingsLocalSocket').checked = result['unix-socket']; socketCheckChanged(document.getElementById('checkSettingsLocalSocket')); if (result.address.includes('/')) { switch (result.address) { case '/tmp/hyperhdr-domain': document.getElementById('selectSettingsSocket').value = 'hyperhdr'; break; default: document.getElementById('selectSettingsSocket').value = 'manual'; document.getElementById('txtInputSettingsAddress').value = result.address; } document.getElementById('txtInputSettingsAddress').value = '127.0.0.1'; socketSelectChanged(document.getElementById('selectSettingsSocket')); } else { document.getElementById('txtInputSettingsAddress').value = result.address || '127.0.0.1'; } document.getElementById('txtInputSettingsPort').value = result.port; document.getElementById('txtInputSettingsPriority').value = result.priority; document.getElementById('txtInputSettingsFPS').value = result.fps; // Process Height/Width for easier selection switch (result.width * result.height) { case 57600: document.getElementById('selectSettingsResolution').value = '320x180'; break; case 36864: document.getElementById('selectSettingsResolution').value = '256x144'; break; case 20736: document.getElementById('selectSettingsResolution').value = '192x108'; break; case 9984: document.getElementById('selectSettingsResolution').value = '128x78'; break; default: document.getElementById('selectSettingsResolution').value = 'manual'; document.getElementById('txtInputSettingsWidth').value = result.width; document.getElementById('txtInputSettingsHeight').value = result.height; break; } Object.keys(availableQuirks).forEach((quirk) => { logIt(`Processing: ${quirk}`); const quirkval = availableQuirks[quirk]; /* eslint-disable eqeqeq */ if ((result.quirks & quirkval) == quirkval) { logIt(`Quirk ${quirk} enabled!`); document.getElementById(`checkSettings${quirk}`).checked = true; } /* eslint-enable eqeqeq */ }); document.getElementById('checkSettingsVSync').checked = result.vsync; document.getElementById('checkSettingsAutostart').checked = result.autostart; document.getElementById('checkSettingsNoHDR').checked = result.nohdr; document.getElementById('checkSettingsNoPowerstate').checked = result.nopowerstate; document.getElementById('checkSettingsNV12').checked = result.nv12; logIt('Loading settings done!'); document.getElementById('txtInfoState').innerHTML = 'Settings loaded'; } else { logIt('Getting settings from PicCap-Service failed! Return value false!'); document.getElementById('txtInfoState').innerHTML = 'Getting settings failed!'; } }, onFailure(result) { logIt(`Getting settings from PicCap-Service failed! Code: ${result.errorCode}`); document.getElementById('txtInfoState').innerHTML = 'Getting settings failed!'; }, }, ); /* eslint-enable no-undef */ } window.restartHyperion = () => { document.getElementById('txtInfoState').innerHTML = 'Killing hyperion.. Will be started again through status loop'; killHyperion(); }; function saveSettings(config) { /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.piccap.service', { method: 'setSettings', parameters: config, onSuccess(result) { if (result.returnValue === true) { logIt('Saving settings success!'); document.getElementById('txtInfoState').innerHTML = 'Save settings success!'; getSettings(); } else { logIt('Save settings for PicCap-Service failed! Return value false!'); document.getElementById('txtInfoState').innerHTML = 'Save settings failed!'; } }, onFailure(result) { logIt(`Save settings for PicCap-Service failed! Code: ${result.errorCode}`); document.getElementById('txtInfoState').innerHTML = 'Sace settings failed!'; }, }, ); /* eslint-enable no-undef */ } window.serviceResetSettings = () => { document.getElementById('txtInfoState').innerHTML = 'Loading default settings..'; const config = { backend: 'auto', uibackend: 'auto', novideo: false, nogui: false, address: '', port: 19400, priority: 150, fps: 0, width: 320, height: 180, quirks: 0, vsync: true, autostart: false, nv12: false, }; logIt(config); document.getElementById('txtInfoState').innerHTML = 'Sending default settings..'; saveSettings(config); }; window.serviceSaveSettings = () => { document.getElementById('txtInfoState').innerHTML = 'Collecting settings..'; let quirkcalc = 0; Object.keys(availableQuirks).forEach((quirk) => { logIt(`Processing quirk: ${quirk}`); const quirkval = availableQuirks[quirk]; logIt(`Quirk val: ${quirkval}`); if (document.getElementById(`checkSettings${quirk}`).checked === true) { quirkcalc |= quirkval; logIt(`Quirkcalc: ${quirkcalc}`); } }); let width; let height; switch (document.getElementById('selectSettingsResolution').value) { case '320x180': width = 320; height = 180; break; case '256x144': width = 256; height = 144; break; case '192x108': width = 192; height = 108; break; case '128x78': width = 128; height = 78; break; case 'manual': width = parseInt(document.getElementById('txtInputSettingsWidth').value, 10); height = parseInt(document.getElementById('txtInputSettingsHeight').value, 10); break; default: width = 320; height = 180; break; } let address; if (document.getElementById('checkSettingsLocalSocket').checked === true) { switch (document.getElementById('selectSettingsSocket').value) { case 'hyperhdr': address = '/tmp/hyperhdr-domain'; break; case 'manual': address = document.getElementById('txtInputSettingsSocketPath').value; break; default: address = undefined; logIt('Address wasnt found!'); break; } } else { address = document.getElementById('txtInputSettingsAddress').value; } const config = { backend: document.getElementById('selectSettingsVideoBackend').value === 'disabled' ? 'auto' : document.getElementById('selectSettingsVideoBackend').value, uibackend: document.getElementById('selectSettingsGraphicalBackend').value === 'disabled' ? 'auto' : document.getElementById('selectSettingsGraphicalBackend').value, novideo: document.getElementById('selectSettingsVideoBackend').value === 'disabled', nogui: document.getElementById('selectSettingsGraphicalBackend').value === 'disabled', 'unix-socket': document.getElementById('checkSettingsLocalSocket').checked, address, port: parseInt(document.getElementById('txtInputSettingsPort').value, 10) || undefined, priority: parseInt(document.getElementById('txtInputSettingsPriority').value, 10) || undefined, fps: parseInt(document.getElementById('txtInputSettingsFPS').value, 10) || 0, width: width || undefined, height: height || undefined, quirks: quirkcalc, vsync: document.getElementById('checkSettingsVSync').checked, autostart: document.getElementById('checkSettingsAutostart').checked, nohdr: document.getElementById('checkSettingsNoHDR').checked, nopowerstate: document.getElementById('checkSettingsNoPowerstate').checked, nv12: document.getElementById('checkSettingsNV12').checked, }; logIt(`Config: ${JSON.stringify(config)}`); document.getElementById('txtInfoState').innerHTML = 'Sending settings..'; saveSettings(config); }; window.reloadHyperionLog = () => { logIt('Calling HBCHannel to get latest 200 hyperion-webos log lines.'); /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.hbchannel.service', { method: 'exec', parameters: { command: 'grep hyperion-webos /var/log/messages | tail -n200', }, onSuccess(result) { onHBExec(result); const textareaHyperionLog = document.getElementById('textareaHyperionLog'); textareaHyperionLog.value += `${result.stdoutString}\r\n`; }, onFailure: onHBExec, }, ); /* eslint-enable no-undef */ }; // Using this function to setup logging for now. // Future start/stop of currently not implemented hyperion-webos log method. window.startStopLogging = () => { logIt('Setup logging using HBChannel'); document.getElementById('txtInfoState').innerHTML = 'Calling HBChannel for log setup'; /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.hbchannel.service', { method: 'exec', parameters: { command: '/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/setuplegacylogging.sh', }, onSuccess: onHBExec, onFailure: onHBExec, }, ); /* eslint-enable no-undef */ /* // Future Stuff const btnLogStartStop = document.getElementById('btnLogStartStop'); if (!loggingStarted) { loggingStarted = true; btnLogStartStop.innerHTML = 'Stop logging'; } else { loggingStarted = false; btnLogStartStop.innerHTML = 'Start logging'; } */ }; function onServiceCallback(result) { if (result.returnValue === true) { logIt('Servicecall returned successfully.'); document.getElementById('txtInfoState').innerHTML = 'Servicecall success!'; } else { logIt(`Servicecall failed! Code: ${result.errorCode}`); document.getElementById('txtInfoState').innerHTML = 'Servicecall failed!'; } } window.serviceStart = () => { logIt('Start clicked'); try { document.getElementById('txtServiceStatus').innerHTML = 'Starting service...'; document.getElementById('txtInfoState').innerHTML = 'Sending start command'; /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.piccap.service', { method: 'start', parameters: {}, onSuccess: onServiceCallback, onFailure: onServiceCallback, }, ); /* eslint-enable no-undef */ } catch (err) { document.getElementById('txtServiceStatus').innerHTML = `Failed: ${JSON.stringify(err)}`; } document.getElementById('txtInfoState').innerHTML = 'Start command send'; }; window.serviceStop = () => { logIt('Stop clicked'); try { document.getElementById('txtServiceStatus').innerHTML = 'Stopping service...'; document.getElementById('txtInfoState').innerHTML = 'Sending stop command'; /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.piccap.service', { method: 'stop', parameters: {}, onSuccess: onServiceCallback, onFailure: onServiceCallback, }, ); /* eslint-enable no-undef */ } catch (err) { document.getElementById('txtServiceStatus').innerHTML = `Failed: ${JSON.stringify(err)}`; } document.getElementById('txtInfoState').innerHTML = 'Stop command send'; }; window.serviceReload = () => { logIt('Reload clicked'); getSettings(); }; window.tvReboot = () => { logIt('Trying to reboot TV using HBChannel..'); document.getElementById('txtInfoState').innerHTML = 'Rebooting TV..'; /* eslint-disable no-undef */ webOS.service.request( 'luna://org.webosbrew.hbchannel.service', { method: 'reboot', parameters: {}, onSuccess: onServiceCallback, onFailure: onServiceCallback, }, ); /* eslint-enable no-undef */ }; window.addEventListener('load', () => { logIt('Startup of PicCap...'); checkRoot(); logIt('Starting load settings loop...'); const getStatusIntervalID = window.setInterval(() => { if (isRoot === true) { logIt('Loading settings, we are rooted.'); getSettings(); clearInterval(getStatusIntervalID); } }, 3000); logIt('Starting status loop...'); setInterval(() => { getStatus(); }, 4000); }); ================================================ FILE: frontend/js/spatial-navigation.js ================================================ // // https://raw.githubusercontent.com/WICG/spatial-navigation/183f0146b6741007e46fa64ab0950447defdf8af/polyfill/spatial-navigation-polyfill.js // License: MIT // Line 97-104: Added checkbox behavior // /* Spatial Navigation Polyfill * * It follows W3C official specification * https://drafts.csswg.org/css-nav-1/ * * Copyright (c) 2018-2019 LG Electronics Inc. * https://github.com/WICG/spatial-navigation/polyfill * * Licensed under the MIT license (MIT) */ import 'core-js'; import './domrect-polyfill'; (function () { // The polyfill must not be executed, if it's already enabled via browser engine or browser extensions. if ('navigate' in window) { return; } const ARROW_KEY_CODE = { 37: 'left', 38: 'up', 39: 'right', 40: 'down', }; const TAB_KEY_CODE = 9; const ENTER_KEY_CODE = 13; let mapOfBoundRect = null; let startingPoint = null; // Saves spatial navigation starting point const savedSearchOrigin = { element: null, rect: null }; // Saves previous search origin let searchOriginRect = null; // Rect of current search origin /** * Initiate the spatial navigation features of the polyfill. * @function initiateSpatialNavigation */ function initiateSpatialNavigation() { /* * Bind the standards APIs to be exposed to the window object for authors */ window.navigate = navigate; window.Element.prototype.spatialNavigationSearch = spatialNavigationSearch; window.Element.prototype.focusableAreas = focusableAreas; window.Element.prototype.getSpatialNavigationContainer = getSpatialNavigationContainer; /* * CSS.registerProperty() from the Properties and Values API * Reference: https://drafts.css-houdini.org/css-properties-values-api/#the-registerproperty-function */ if (window.CSS && CSS.registerProperty) { if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-contain') === '') { CSS.registerProperty({ name: '--spatial-navigation-contain', syntax: 'auto | contain', inherits: false, initialValue: 'auto', }); } if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-action') === '') { CSS.registerProperty({ name: '--spatial-navigation-action', syntax: 'auto | focus | scroll', inherits: false, initialValue: 'auto', }); } if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-function') === '') { CSS.registerProperty({ name: '--spatial-navigation-function', syntax: 'normal | grid', inherits: false, initialValue: 'normal', }); } } } /** * Add event handlers for the spatial navigation behavior. * This function defines which input methods trigger the spatial navigation behavior. * @function spatialNavigationHandler */ function spatialNavigationHandler() { /* * keydown EventListener : * If arrow key pressed, get the next focusing element and send it to focusing controller */ window.addEventListener('keydown', (e) => { const currentKeyMode = (parent && parent.__spatialNavigation__.keyMode) || window.__spatialNavigation__.keyMode; const eventTarget = document.activeElement; const dir = ARROW_KEY_CODE[e.keyCode]; if (e.keyCode === TAB_KEY_CODE) { startingPoint = null; } // Added checkbox checking check if ((e.keyCode === ENTER_KEY_CODE) && ('checkbox'.includes(eventTarget.getAttribute('type')))) { if (eventTarget.checked == true) { eventTarget.checked = false; } else { eventTarget.checked = true; } } if (!currentKeyMode || (currentKeyMode === 'NONE') || ((currentKeyMode === 'SHIFTARROW') && !e.shiftKey) || ((currentKeyMode === 'ARROW') && e.shiftKey) || (e.ctrlKey || e.metaKey || e.altKey)) return; if (!e.defaultPrevented) { let focusNavigableArrowKey = { left: true, up: true, right: true, down: true, }; // Edge case (text input, area) : Don't move focus, just navigate cursor in text area if ((eventTarget.nodeName === 'INPUT') || eventTarget.nodeName === 'TEXTAREA') { focusNavigableArrowKey = handlingEditableElement(e); } if (focusNavigableArrowKey[dir]) { e.preventDefault(); mapOfBoundRect = new Map(); navigate(dir); mapOfBoundRect = null; startingPoint = null; } } }); /* * mouseup EventListener : * If the mouse click a point in the page, the point will be the starting point. * NOTE: Let UA set the spatial navigation starting point based on click */ document.addEventListener('mouseup', (e) => { startingPoint = { x: e.clientX, y: e.clientY }; }); /* * focusin EventListener : * When the element get the focus, save it and its DOMRect for resetting the search origin * if it disappears. */ window.addEventListener('focusin', (e) => { if (e.target !== window) { savedSearchOrigin.element = e.target; savedSearchOrigin.rect = e.target.getBoundingClientRect(); } }); } /** * Enable the author to trigger spatial navigation programmatically, as if the user had done so manually. * @see {@link https://drafts.csswg.org/css-nav-1/#dom-window-navigate} * @function navigate * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) */ function navigate(dir) { // spatial navigation steps // 1 const searchOrigin = findSearchOrigin(); let eventTarget = searchOrigin; let elementFromPosition = null; // 2 Optional step, UA defined starting point if (startingPoint) { // if there is a starting point, set eventTarget as the element from position for getting the spatnav container elementFromPosition = document.elementFromPoint(startingPoint.x, startingPoint.y); // Use starting point if the starting point isn't inside the focusable element (but not container) // * Starting point is meaningfull when: // 1) starting point is inside the spatnav container // 2) starting point is inside the non-focusable element if (elementFromPosition === null) { elementFromPosition = document.body; } if (isFocusable(elementFromPosition) && !isContainer(elementFromPosition)) { startingPoint = null; } else if (isContainer(elementFromPosition)) { eventTarget = elementFromPosition; } else { eventTarget = elementFromPosition.getSpatialNavigationContainer(); } } // 4 if (eventTarget === document || eventTarget === document.documentElement) { eventTarget = document.body || document.documentElement; } // 5 // At this point, spatialNavigationSearch can be applied. // If startingPoint is either a scroll container or the document, // find the best candidate within startingPoint let container = null; if ((isContainer(eventTarget) || eventTarget.nodeName === 'BODY') && !(eventTarget.nodeName === 'INPUT')) { if (eventTarget.nodeName === 'IFRAME') { eventTarget = eventTarget.contentDocument.documentElement; } container = eventTarget; let bestInsideCandidate = null; // 5-2 if ((document.activeElement === searchOrigin) || (document.activeElement === document.body) && (searchOrigin === document.documentElement)) { if (getCSSSpatNavAction(eventTarget) === 'scroll') { if (scrollingController(eventTarget, dir)) return; } else if (getCSSSpatNavAction(eventTarget) === 'focus') { bestInsideCandidate = eventTarget.spatialNavigationSearch(dir, { container: eventTarget, candidates: getSpatialNavigationCandidates(eventTarget, { mode: 'all' }) }); if (focusingController(bestInsideCandidate, dir)) return; } else if (getCSSSpatNavAction(eventTarget) === 'auto') { bestInsideCandidate = eventTarget.spatialNavigationSearch(dir, { container: eventTarget }); if (focusingController(bestInsideCandidate, dir) || scrollingController(eventTarget, dir)) return; } } else { // when the previous search origin became offscreen container = container.getSpatialNavigationContainer(); } } // 6 // Let container be the nearest ancestor of eventTarget container = eventTarget.getSpatialNavigationContainer(); let parentContainer = (container.parentElement) ? container.getSpatialNavigationContainer() : null; // When the container is the viewport of a browsing context if (!parentContainer && (window.location !== window.parent.location)) { parentContainer = window.parent.document.documentElement; } if (getCSSSpatNavAction(container) === 'scroll') { if (scrollingController(container, dir)) return; } else if (getCSSSpatNavAction(container) === 'focus') { navigateChain(eventTarget, container, parentContainer, dir, 'all'); } else if (getCSSSpatNavAction(container) === 'auto') { navigateChain(eventTarget, container, parentContainer, dir, 'visible'); } } /** * Move the focus to the best candidate or do nothing. * @function focusingController * @param bestCandidate {Node} - The best candidate of the spatial navigation * @param dir {SpatialNavigationDirection}- The directional information for the spatial navigation (e.g. LRUD) * @returns {boolean} */ function focusingController(bestCandidate, dir) { // 10 & 11 // When bestCandidate is found if (bestCandidate) { // When bestCandidate is a focusable element and not a container : move focus /* * [event] navbeforefocus : Fired before spatial or sequential navigation changes the focus. */ if (!createSpatNavEvents('beforefocus', bestCandidate, null, dir)) return true; const container = bestCandidate.getSpatialNavigationContainer(); if ((container !== window) && (getCSSSpatNavAction(container) === 'focus')) { bestCandidate.focus(); } else { bestCandidate.focus({ preventScroll: true }); } startingPoint = null; return true; } // When bestCandidate is not found within the scrollport of a container: Nothing return false; } /** * Directionally scroll the scrollable spatial navigation container if it can be manually scrolled more. * @function scrollingController * @param container {Node} - The spatial navigation container which can scroll * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {boolean} */ function scrollingController(container, dir) { // If there is any scrollable area among parent elements and it can be manually scrolled, scroll the document if (isScrollable(container, dir) && !isScrollBoundary(container, dir)) { moveScroll(container, dir); return true; } // If the spatnav container is document and it can be scrolled, scroll the document if (!container.parentElement && !isHTMLScrollBoundary(container, dir)) { moveScroll(container.ownerDocument.documentElement, dir); return true; } return false; } /** * Find the candidates within a spatial navigation container include delegable container. * This function does not search inside delegable container or focusable container. * In other words, this return candidates set is not included focusable elements inside delegable container or focusable container. * * @function getSpatialNavigationCandidates * @param container {Node} - The spatial navigation container * @param option {FocusableAreasOptions} - 'mode' attribute takes 'visible' or 'all' for searching the boundary of focusable elements. * Default value is 'visible'. * @returns {sequence} candidate elements within the container */ function getSpatialNavigationCandidates(container, option = { mode: 'visible' }) { let candidates = []; if (container.childElementCount > 0) { if (!container.parentElement) { container = container.getElementsByTagName('body')[0] || document.body; } const { children } = container; for (const elem of children) { if (isDelegableContainer(elem)) { candidates.push(elem); } else if (isFocusable(elem)) { candidates.push(elem); if (!isContainer(elem) && elem.childElementCount) { candidates = candidates.concat(getSpatialNavigationCandidates(elem, { mode: 'all' })); } } else if (elem.childElementCount) { candidates = candidates.concat(getSpatialNavigationCandidates(elem, { mode: 'all' })); } } } return (option.mode === 'all') ? candidates : candidates.filter(isVisible); } /** * Find the candidates among focusable elements within a spatial navigation container from the search origin (currently focused element) * depending on the directional information. * @function getFilteredSpatialNavigationCandidates * @param element {Node} - The currently focused element which is defined as 'search origin' in the spec * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @param candidates {sequence} - The candidates for spatial navigation without the directional information * @param container {Node} - The spatial navigation container * @returns {Node} The candidates for spatial navigation considering the directional information */ function getFilteredSpatialNavigationCandidates(element, dir, candidates, container) { const targetElement = element; // Removed below line due to a bug. (iframe body rect is sometime weird.) // const targetElement = (element.nodeName === 'IFRAME') ? element.contentDocument.body : element; // If the container is unknown, get the closest container from the element container = container || targetElement.getSpatialNavigationContainer(); // If the candidates is unknown, find candidates // 5-1 candidates = (!candidates || candidates.length <= 0) ? getSpatialNavigationCandidates(container) : candidates; return filteredCandidates(targetElement, candidates, dir, container); } /** * Find the best candidate among the candidates within the container from the search origin (currently focused element) * @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-spatialnavigationsearch} * @function spatialNavigationSearch * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @param candidates {sequence} - The candidates for spatial navigation * @param container {Node} - The spatial navigation container * @returns {Node} The best candidate which will gain the focus */ function spatialNavigationSearch(dir, args) { const targetElement = this; let internalCandidates = []; let externalCandidates = []; const insideOverlappedCandidates = getOverlappedCandidates(targetElement); let bestTarget; // Set default parameter value if (!args) args = {}; const defaultContainer = targetElement.getSpatialNavigationContainer(); let defaultCandidates = getSpatialNavigationCandidates(defaultContainer); const container = args.container || defaultContainer; if (args.container && (defaultContainer.contains(args.container))) { defaultCandidates = defaultCandidates.concat(getSpatialNavigationCandidates(container)); } const candidates = (args.candidates && args.candidates.length > 0) ? args.candidates.filter((candidate) => container.contains(candidate)) : defaultCandidates.filter((candidate) => container.contains(candidate) && (container !== candidate)); // Find the best candidate // 5 // If startingPoint is either a scroll container or the document, // find the best candidate within startingPoint if (candidates && candidates.length > 0) { // Divide internal or external candidates candidates.forEach((candidate) => { if (candidate !== targetElement) { (targetElement.contains(candidate) && targetElement !== candidate ? internalCandidates : externalCandidates).push(candidate); } }); // include overlapped element to the internalCandidates const fullyOverlapped = insideOverlappedCandidates.filter((candidate) => !internalCandidates.includes(candidate)); const overlappedContainer = candidates.filter((candidate) => (isContainer(candidate) && isEntirelyVisible(targetElement, candidate))); const overlappedByParent = overlappedContainer.map((elm) => elm.focusableAreas()).flat().filter((candidate) => candidate !== targetElement); internalCandidates = internalCandidates.concat(fullyOverlapped).filter((candidate) => container.contains(candidate)); externalCandidates = externalCandidates.concat(overlappedByParent).filter((candidate) => container.contains(candidate)); // Filter external Candidates if (externalCandidates.length > 0) { externalCandidates = getFilteredSpatialNavigationCandidates(targetElement, dir, externalCandidates, container); } // If there isn't search origin element but search orgin rect exist (search origin isn't in the layout case) if (searchOriginRect) { bestTarget = selectBestCandidate(targetElement, getFilteredSpatialNavigationCandidates(targetElement, dir, internalCandidates, container), dir); } if ((internalCandidates && internalCandidates.length > 0) && !(targetElement.nodeName === 'INPUT')) { bestTarget = selectBestCandidateFromEdge(targetElement, internalCandidates, dir); } bestTarget = bestTarget || selectBestCandidate(targetElement, externalCandidates, dir); if (bestTarget && isDelegableContainer(bestTarget)) { // if best target is delegable container, then find descendants candidate inside delegable container. const innerTarget = getSpatialNavigationCandidates(bestTarget, { mode: 'all' }); const descendantsBest = innerTarget.length > 0 ? targetElement.spatialNavigationSearch(dir, { candidates: innerTarget, container: bestTarget }) : null; if (descendantsBest) { bestTarget = descendantsBest; } else if (!isFocusable(bestTarget)) { // if there is no target inside bestTarget and delegable container is not focusable, // then try to find another best target without curren best target. candidates.splice(candidates.indexOf(bestTarget), 1); bestTarget = candidates.length ? targetElement.spatialNavigationSearch(dir, { candidates, container }) : null; } } return bestTarget; } return null; } /** * Get the filtered candidate among candidates. * @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate} * @function filteredCandidates * @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec * @param candidates {sequence} - The candidates for spatial navigation * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @param container {Node} - The spatial navigation container * @returns {sequence} The filtered candidates which are not the search origin and not in the given spatial navigation direction from the search origin */ // TODO: Need to fix filtering the candidates with more clean code function filteredCandidates(currentElm, candidates, dir, container) { const originalContainer = currentElm.getSpatialNavigationContainer(); let eventTargetRect; // If D(dir) is null, let candidates be the same as visibles if (dir === undefined) return candidates; // Offscreen handling when originalContainer is not if (originalContainer.parentElement && container !== originalContainer && !isVisible(currentElm)) { eventTargetRect = getBoundingClientRect(originalContainer); } else { eventTargetRect = searchOriginRect || getBoundingClientRect(currentElm); } /* * Else, let candidates be the subset of the elements in visibles * whose principal box’s geometric center is within the closed half plane * whose boundary goes through the geometric center of starting point and is perpendicular to D. */ if ((isContainer(currentElm) || currentElm.nodeName === 'BODY') && !(currentElm.nodeName === 'INPUT')) { return candidates.filter((candidate) => { const candidateRect = getBoundingClientRect(candidate); return container.contains(candidate) && ((currentElm.contains(candidate) && isInside(eventTargetRect, candidateRect) && candidate !== currentElm) || isOutside(candidateRect, eventTargetRect, dir)); }); } return candidates.filter((candidate) => { const candidateRect = getBoundingClientRect(candidate); const candidateBody = (candidate.nodeName === 'IFRAME') ? candidate.contentDocument.body : null; return container.contains(candidate) && candidate !== currentElm && candidateBody !== currentElm && isOutside(candidateRect, eventTargetRect, dir) && !isInside(eventTargetRect, candidateRect); }); } /** * Select the best candidate among given candidates. * @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate} * @function selectBestCandidate * @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec * @param candidates {sequence} - The candidates for spatial navigation * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {Node} The best candidate which will gain the focus */ function selectBestCandidate(currentElm, candidates, dir) { const container = currentElm.getSpatialNavigationContainer(); const spatialNavigationFunction = getComputedStyle(container).getPropertyValue('--spatial-navigation-function'); const currentTargetRect = searchOriginRect || getBoundingClientRect(currentElm); let distanceFunction; let alignedCandidates; switch (spatialNavigationFunction) { case 'grid': alignedCandidates = candidates.filter((elm) => isAligned(currentTargetRect, getBoundingClientRect(elm), dir)); if (alignedCandidates.length > 0) { candidates = alignedCandidates; } distanceFunction = getAbsoluteDistance; break; default: distanceFunction = getDistance; break; } return getClosestElement(currentElm, candidates, dir, distanceFunction); } /** * Select the best candidate among candidates by finding the closet candidate from the edge of the currently focused element (search origin). * @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate (Step 5)} * @function selectBestCandidateFromEdge * @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec * @param candidates {sequence} - The candidates for spatial navigation * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {Node} The best candidate which will gain the focus */ function selectBestCandidateFromEdge(currentElm, candidates, dir) { if (startingPoint) return getClosestElement(currentElm, candidates, dir, getDistanceFromPoint); return getClosestElement(currentElm, candidates, dir, getInnerDistance); } /** * Select the closest candidate from the currently focused element (search origin) among candidates by using the distance function. * @function getClosestElement * @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec * @param candidates {sequence} - The candidates for spatial navigation * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @param distanceFunction {function} - The distance function which measures the distance from the search origin to each candidate * @returns {Node} The candidate which is the closest one from the search origin */ function getClosestElement(currentElm, candidates, dir, distanceFunction) { let eventTargetRect = null; if ((window.location !== window.parent.location) && (currentElm.nodeName === 'BODY' || currentElm.nodeName === 'HTML')) { // If the eventTarget is iframe, then get rect of it based on its containing document // Set the iframe's position as (0,0) because the rects of elements inside the iframe don't know the real iframe's position. eventTargetRect = window.frameElement.getBoundingClientRect(); eventTargetRect.x = 0; eventTargetRect.y = 0; } else { eventTargetRect = searchOriginRect || currentElm.getBoundingClientRect(); } let minDistance = Number.POSITIVE_INFINITY; let minDistanceElements = []; if (candidates) { for (let i = 0; i < candidates.length; i++) { const distance = distanceFunction(eventTargetRect, getBoundingClientRect(candidates[i]), dir); // If the same distance, the candidate will be selected in the DOM order if (distance < minDistance) { minDistance = distance; minDistanceElements = [candidates[i]]; } else if (distance === minDistance) { minDistanceElements.push(candidates[i]); } } } if (minDistanceElements.length === 0) return null; return (minDistanceElements.length > 1 && distanceFunction === getAbsoluteDistance) ? getClosestElement(currentElm, minDistanceElements, dir, getEuclideanDistance) : minDistanceElements[0]; } /** * Get container of an element. * @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-getspatialnavigationcontainer} * @module Element * @function getSpatialNavigationContainer * @returns {Node} The spatial navigation container */ function getSpatialNavigationContainer() { let container = this; do { if (!container.parentElement) { if (window.location !== window.parent.location) { container = window.parent.document.documentElement; } else { container = window.document.documentElement; } break; } else { container = container.parentElement; } } while (!isContainer(container)); return container; } /** * Get nearest scroll container of an element. * @function getScrollContainer * @param Element * @returns {Node} The spatial navigation container */ function getScrollContainer(element) { let scrollContainer = element; do { if (!scrollContainer.parentElement) { if (window.location !== window.parent.location) { scrollContainer = window.parent.document.documentElement; } else { scrollContainer = window.document.documentElement; } break; } else { scrollContainer = scrollContainer.parentElement; } } while (!isScrollContainer(scrollContainer) || !isVisible(scrollContainer)); if (scrollContainer === document || scrollContainer === document.documentElement) { scrollContainer = window; } return scrollContainer; } /** * Find focusable elements within the spatial navigation container. * @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-focusableareas} * @function focusableAreas * @param option {FocusableAreasOptions} - 'mode' attribute takes 'visible' or 'all' for searching the boundary of focusable elements. * Default value is 'visible'. * @returns {sequence} All focusable elements or only visible focusable elements within the container */ function focusableAreas(option = { mode: 'visible' }) { const container = this.parentElement ? this : document.body; const focusables = Array.prototype.filter.call(container.getElementsByTagName('*'), isFocusable); return (option.mode === 'all') ? focusables : focusables.filter(isVisible); } /** * Create the NavigationEvent: navbeforefocus, navnotarget * @see {@link https://drafts.csswg.org/css-nav-1/#events-navigationevent} * @function createSpatNavEvents * @param option {string} - Type of the navigation event (beforefocus, notarget) * @param element {Node} - The target element of the event * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) */ function createSpatNavEvents(eventType, containerElement, currentElement, direction) { if (['beforefocus', 'notarget'].includes(eventType)) { const data = { causedTarget: currentElement, dir: direction, }; const triggeredEvent = new CustomEvent(`nav${eventType}`, { bubbles: true, cancelable: true, detail: data }); return containerElement.dispatchEvent(triggeredEvent); } } /** * Get the value of the CSS custom property of the element * @function readCssVar * @param element {Node} * @param varName {string} - The name of the css custom property without '--' * @returns {string} The value of the css custom property */ function readCssVar(element, varName) { return element.style.getPropertyValue(`--${varName}`).trim(); } /** * Decide whether or not the 'contain' value is given to 'spatial-navigation-contain' css property of an element * @function isCSSSpatNavContain * @param element {Node} * @returns {boolean} */ function isCSSSpatNavContain(element) { return readCssVar(element, 'spatial-navigation-contain') === 'contain'; } /** * Return the value of 'spatial-navigation-action' css property of an element * @function getCSSSpatNavAction * @param element {Node} - would be the spatial navigation container * @returns {string} auto | focus | scroll */ function getCSSSpatNavAction(element) { return readCssVar(element, 'spatial-navigation-action') || 'auto'; } /** * Only move the focus with spatial navigation. Manually scrolling isn't available. * @function navigateChain * @param eventTarget {Node} - currently focused element * @param container {SpatialNavigationContainer} - container * @param parentContainer {SpatialNavigationContainer} - parent container * @param option - visible || all * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) */ function navigateChain(eventTarget, container, parentContainer, dir, option) { let currentOption = { candidates: getSpatialNavigationCandidates(container, { mode: option }), container }; while (parentContainer) { if (focusingController(eventTarget.spatialNavigationSearch(dir, currentOption), dir)) { return; } if ((option === 'visible') && scrollingController(container, dir)) return; if (!createSpatNavEvents('notarget', container, eventTarget, dir)) return; // find the container if (container === document || container === document.documentElement) { if (window.location !== window.parent.location) { // The page is in an iframe. eventTarget needs to be reset because the position of the element in the iframe eventTarget = window.frameElement; container = eventTarget.ownerDocument.documentElement; } } else { container = parentContainer; } currentOption = { candidates: getSpatialNavigationCandidates(container, { mode: option }), container }; const nextContainer = container.getSpatialNavigationContainer(); if (nextContainer !== container) { parentContainer = nextContainer; } else { parentContainer = null; } } currentOption = { candidates: getSpatialNavigationCandidates(container, { mode: option }), container }; // Behavior after 'navnotarget' - Getting out from the current spatnav container if ((!parentContainer && container) && focusingController(eventTarget.spatialNavigationSearch(dir, currentOption), dir)) return; if (!createSpatNavEvents('notarget', currentOption.container, eventTarget, dir)) return; if ((getCSSSpatNavAction(container) === 'auto') && (option === 'visible')) { if (scrollingController(container, dir)) return; } } /** * Find search origin * @see {@link https://drafts.csswg.org/css-nav-1/#nav} * @function findSearchOrigin * @returns {Node} The search origin for the spatial navigation */ function findSearchOrigin() { let searchOrigin = document.activeElement; if (!searchOrigin || (searchOrigin === document.body && !document.querySelector(':focus'))) { // When the previous search origin lost its focus by blur: (1) disable attribute (2) visibility: hidden if (savedSearchOrigin.element && (searchOrigin !== savedSearchOrigin.element)) { const elementStyle = window.getComputedStyle(savedSearchOrigin.element, null); const invisibleStyle = ['hidden', 'collapse']; if (savedSearchOrigin.element.disabled || invisibleStyle.includes(elementStyle.getPropertyValue('visibility'))) { searchOrigin = savedSearchOrigin.element; return searchOrigin; } } searchOrigin = document.documentElement; } // When the previous search origin lost its focus by blur: (1) display:none () element size turned into zero if (savedSearchOrigin.element && ((getBoundingClientRect(savedSearchOrigin.element).height === 0) || (getBoundingClientRect(savedSearchOrigin.element).width === 0))) { searchOriginRect = savedSearchOrigin.rect; } if (!isVisibleInScroller(searchOrigin)) { const scroller = getScrollContainer(searchOrigin); if (scroller && ((scroller === window) || (getCSSSpatNavAction(scroller) === 'auto'))) return scroller; } return searchOrigin; } /** * Move the scroll of an element depending on the given spatial navigation directrion * (Assume that User Agent defined distance is '40px') * @see {@link https://drafts.csswg.org/css-nav-1/#directionally-scroll-an-element} * @function moveScroll * @param element {Node} - The scrollable element * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @param offset {Number} - The explicit amount of offset for scrolling. Default value is 0. */ function moveScroll(element, dir, offset = 0) { if (element) { switch (dir) { case 'left': element.scrollLeft -= (40 + offset); break; case 'right': element.scrollLeft += (40 + offset); break; case 'up': element.scrollTop -= (40 + offset); break; case 'down': element.scrollTop += (40 + offset); break; } } } /** * Decide whether an element is container or not. * @function isContainer * @param element {Node} element * @returns {boolean} */ function isContainer(element) { return (!element.parentElement) || (element.nodeName === 'IFRAME') || (isScrollContainer(element)) || (isCSSSpatNavContain(element)); } /** * Decide whether an element is delegable container or not. * NOTE: THIS IS NON-NORMATIVE API. * @function isDelegableContainer * @param element {Node} element * @returns {boolean} */ function isDelegableContainer(element) { return readCssVar(element, 'spatial-navigation-contain') === 'delegable'; } /** * Decide whether an element is a scrollable container or not. * @see {@link https://drafts.csswg.org/css-overflow-3/#scroll-container} * @function isScrollContainer * @param element {Node} * @returns {boolean} */ function isScrollContainer(element) { const elementStyle = window.getComputedStyle(element, null); const overflowX = elementStyle.getPropertyValue('overflow-x'); const overflowY = elementStyle.getPropertyValue('overflow-y'); return !!(((overflowX !== 'visible' && overflowX !== 'clip' && isOverflow(element, 'left')) || (overflowY !== 'visible' && overflowY !== 'clip' && isOverflow(element, 'down')))); } /** * Decide whether this element is scrollable or not. * NOTE: If the value of 'overflow' is given to either 'visible', 'clip', or 'hidden', the element isn't scrollable. * If the value is 'hidden', the element can be only programmically scrollable. (https://drafts.csswg.org/css-overflow-3/#valdef-overflow-hidden) * @function isScrollable * @param element {Node} * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {boolean} */ function isScrollable(element, dir) { // element, dir if (element && typeof element === 'object') { if (dir && typeof dir === 'string') { // parameter: dir, element if (isOverflow(element, dir)) { // style property const elementStyle = window.getComputedStyle(element, null); const overflowX = elementStyle.getPropertyValue('overflow-x'); const overflowY = elementStyle.getPropertyValue('overflow-y'); switch (dir) { case 'left': /* falls through */ case 'right': return (overflowX !== 'visible' && overflowX !== 'clip' && overflowX !== 'hidden'); case 'up': /* falls through */ case 'down': return (overflowY !== 'visible' && overflowY !== 'clip' && overflowY !== 'hidden'); } } return false; } // parameter: element return (element.nodeName === 'HTML' || element.nodeName === 'BODY') || (isScrollContainer(element) && isOverflow(element)); } } /** * Decide whether an element is overflow or not. * @function isOverflow * @param element {Node} * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {boolean} */ function isOverflow(element, dir) { if (element && typeof element === 'object') { if (dir && typeof dir === 'string') { // parameter: element, dir switch (dir) { case 'left': /* falls through */ case 'right': return (element.scrollWidth > element.clientWidth); case 'up': /* falls through */ case 'down': return (element.scrollHeight > element.clientHeight); } } else { // parameter: element return (element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight); } return false; } } /** * Decide whether the scrollbar of the browsing context reaches to the end or not. * @function isHTMLScrollBoundary * @param element {Node} - The top browsing context * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {boolean} */ function isHTMLScrollBoundary(element, dir) { let result = false; switch (dir) { case 'left': result = element.scrollLeft === 0; break; case 'right': result = (element.scrollWidth - element.scrollLeft - element.clientWidth) === 0; break; case 'up': result = element.scrollTop === 0; break; case 'down': result = (element.scrollHeight - element.scrollTop - element.clientHeight) === 0; break; } return result; } /** * Decide whether the scrollbar of an element reaches to the end or not. * @function isScrollBoundary * @param element {Node} * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {boolean} */ function isScrollBoundary(element, dir) { if (isScrollable(element, dir)) { const winScrollY = element.scrollTop; const winScrollX = element.scrollLeft; const height = element.scrollHeight - element.clientHeight; const width = element.scrollWidth - element.clientWidth; switch (dir) { case 'left': return (winScrollX === 0); case 'right': return (Math.abs(winScrollX - width) <= 1); case 'up': return (winScrollY === 0); case 'down': return (Math.abs(winScrollY - height) <= 1); } } return false; } /** * Decide whether an element is inside the scorller viewport or not * * @function isVisibleInScroller * @param element {Node} * @returns {boolean} */ function isVisibleInScroller(element) { const elementRect = element.getBoundingClientRect(); const nearestScroller = getScrollContainer(element); let scrollerRect = null; if (nearestScroller !== window) { scrollerRect = getBoundingClientRect(nearestScroller); } else { scrollerRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight); } if (isInside(scrollerRect, elementRect, 'left') && isInside(scrollerRect, elementRect, 'down')) return true; return false; } /** * Decide whether an element is focusable for spatial navigation. * 1. If element is the browsing context (document, iframe), then it's focusable, * 2. If the element is scrollable container (regardless of scrollable axis), then it's focusable, * 3. The value of tabIndex >= 0, then it's focusable, * 4. If the element is disabled, it isn't focusable, * 5. If the element is expressly inert, it isn't focusable, * 6. Whether the element is being rendered or not. * * @function isFocusable * @param element {Node} * @returns {boolean} * * @see {@link https://html.spec.whatwg.org/multipage/interaction.html#focusable-area} */ function isFocusable(element) { if ((element.tabIndex < 0) || isAtagWithoutHref(element) || isActuallyDisabled(element) || isExpresslyInert(element) || !isBeingRendered(element)) return false; if ((!element.parentElement) || (isScrollable(element) && isOverflow(element)) || (element.tabIndex >= 0)) return true; } /** * Decide whether an element is a tag without href attribute or not. * * @function isAtagWithoutHref * @param element {Node} * @returns {boolean} */ function isAtagWithoutHref(element) { return (element.tagName === 'A' && element.getAttribute('href') === null && element.getAttribute('tabIndex') === null); } /** * Decide whether an element is actually disabled or not. * * @function isActuallyDisabled * @param element {Node} * @returns {boolean} * * @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled} */ function isActuallyDisabled(element) { if (['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET'].includes(element.tagName)) return (element.disabled); return false; } /** * Decide whether the element is expressly inert or not. * @see {@link https://html.spec.whatwg.org/multipage/interaction.html#expressly-inert} * @function isExpresslyInert * @param element {Node} * @returns {boolean} */ function isExpresslyInert(element) { return ((element.inert) && (!element.ownerDocument.documentElement.inert)); } /** * Decide whether the element is being rendered or not. * 1. If an element has the style as "visibility: hidden | collapse" or "display: none", it is not being rendered. * 2. If an element has the style as "opacity: 0", it is not being rendered.(that is, invisible). * 3. If width and height of an element are explicitly set to 0, it is not being rendered. * 4. If a parent element is hidden, an element itself is not being rendered. * (CSS visibility property and display property are inherited.) * @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered} * @function isBeingRendered * @param element {Node} * @returns {boolean} */ function isBeingRendered(element) { if (!isVisibleStyleProperty(element.parentElement)) return false; if (!isVisibleStyleProperty(element) || (element.style.opacity === '0') || (window.getComputedStyle(element).height === '0px' || window.getComputedStyle(element).width === '0px')) return false; return true; } /** * Decide whether this element is partially or completely visible to user agent. * @function isVisible * @param element {Node} * @returns {boolean} */ function isVisible(element) { return (!element.parentElement) || (isVisibleStyleProperty(element) && hitTest(element)); } /** * Decide whether this element is completely visible in this viewport for the arrow direction. * @function isEntirelyVisible * @param element {Node} * @returns {boolean} */ function isEntirelyVisible(element, container) { const rect = getBoundingClientRect(element); const containerElm = container || element.getSpatialNavigationContainer(); const containerRect = getBoundingClientRect(containerElm); // FIXME: when element is bigger than container? const entirelyVisible = !((rect.left < containerRect.left) || (rect.right > containerRect.right) || (rect.top < containerRect.top) || (rect.bottom > containerRect.bottom)); return entirelyVisible; } /** * Decide the style property of this element is specified whether it's visible or not. * @function isVisibleStyleProperty * @param element {CSSStyleDeclaration} * @returns {boolean} */ function isVisibleStyleProperty(element) { const elementStyle = window.getComputedStyle(element, null); const thisVisibility = elementStyle.getPropertyValue('visibility'); const thisDisplay = elementStyle.getPropertyValue('display'); const invisibleStyle = ['hidden', 'collapse']; return (thisDisplay !== 'none' && !invisibleStyle.includes(thisVisibility)); } /** * Decide whether this element is entirely or partially visible within the viewport. * @function hitTest * @param element {Node} * @returns {boolean} */ function hitTest(element) { const elementRect = getBoundingClientRect(element); if (element.nodeName !== 'IFRAME' && (elementRect.top < 0 || elementRect.left < 0 || elementRect.top > element.ownerDocument.documentElement.clientHeight || elementRect.left > element.ownerDocument.documentElement.clientWidth)) return false; let offsetX = parseInt(element.offsetWidth) / 10; let offsetY = parseInt(element.offsetHeight) / 10; offsetX = isNaN(offsetX) ? 1 : offsetX; offsetY = isNaN(offsetY) ? 1 : offsetY; const hitTestPoint = { // For performance, just using the three point(middle, leftTop, rightBottom) of the element for hit testing middle: [(elementRect.left + elementRect.right) / 2, (elementRect.top + elementRect.bottom) / 2], leftTop: [elementRect.left + offsetX, elementRect.top + offsetY], rightBottom: [elementRect.right - offsetX, elementRect.bottom - offsetY], }; for (const point in hitTestPoint) { const elemFromPoint = element.ownerDocument.elementFromPoint(...hitTestPoint[point]); if (element === elemFromPoint || element.contains(elemFromPoint)) { return true; } } return false; } /** * Decide whether a child element is entirely or partially Included within container visually. * @function isInside * @param containerRect {DOMRect} * @param childRect {DOMRect} * @returns {boolean} */ function isInside(containerRect, childRect) { const rightEdgeCheck = (containerRect.left < childRect.right && containerRect.right >= childRect.right); const leftEdgeCheck = (containerRect.left <= childRect.left && containerRect.right > childRect.left); const topEdgeCheck = (containerRect.top <= childRect.top && containerRect.bottom > childRect.top); const bottomEdgeCheck = (containerRect.top < childRect.bottom && containerRect.bottom >= childRect.bottom); return (rightEdgeCheck || leftEdgeCheck) && (topEdgeCheck || bottomEdgeCheck); } /** * Decide whether this element is entirely or partially visible within the viewport. * Note: rect1 is outside of rect2 for the dir * @function isOutside * @param rect1 {DOMRect} * @param rect2 {DOMRect} * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {boolean} */ function isOutside(rect1, rect2, dir) { switch (dir) { case 'left': return isRightSide(rect2, rect1); case 'right': return isRightSide(rect1, rect2); case 'up': return isBelow(rect2, rect1); case 'down': return isBelow(rect1, rect2); default: return false; } } /* rect1 is right of rect2 */ function isRightSide(rect1, rect2) { return rect1.left >= rect2.right || (rect1.left >= rect2.left && rect1.right > rect2.right && rect1.bottom > rect2.top && rect1.top < rect2.bottom); } /* rect1 is below of rect2 */ function isBelow(rect1, rect2) { return rect1.top >= rect2.bottom || (rect1.top >= rect2.top && rect1.bottom > rect2.bottom && rect1.left < rect2.right && rect1.right > rect2.left); } /* rect1 is completely aligned or partially aligned for the direction */ function isAligned(rect1, rect2, dir) { switch (dir) { case 'left': /* falls through */ case 'right': return rect1.bottom > rect2.top && rect1.top < rect2.bottom; case 'up': /* falls through */ case 'down': return rect1.right > rect2.left && rect1.left < rect2.right; default: return false; } } /** * Get distance between the search origin and a candidate element along the direction when candidate element is inside the search origin. * @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance} * @function getDistanceFromPoint * @param point {Point} - The search origin * @param element {DOMRect} - A candidate element * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {Number} The euclidian distance between the spatial navigation container and an element inside it */ function getDistanceFromPoint(point, element, dir) { point = startingPoint; // Get exit point, entry point -> {x: '', y: ''}; const points = getEntryAndExitPoints(dir, point, element); // Find the points P1 inside the border box of starting point and P2 inside the border box of candidate // that minimize the distance between these two points const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x); const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y); // The result is euclidian distance between P1 and P2. return Math.sqrt(P1 ** 2 + P2 ** 2); } /** * Get distance between the search origin and a candidate element along the direction when candidate element is inside the search origin. * @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance} * @function getInnerDistance * @param rect1 {DOMRect} - The search origin * @param rect2 {DOMRect} - A candidate element * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {Number} The euclidean distance between the spatial navigation container and an element inside it */ function getInnerDistance(rect1, rect2, dir) { const baseEdgeForEachDirection = { left: 'right', right: 'left', up: 'bottom', down: 'top', }; const baseEdge = baseEdgeForEachDirection[dir]; return Math.abs(rect1[baseEdge] - rect2[baseEdge]); } /** * Get the distance between the search origin and a candidate element considering the direction. * @see {@link https://drafts.csswg.org/css-nav-1/#calculating-the-distance} * @function getDistance * @param searchOrigin {DOMRect | Point} - The search origin * @param candidateRect {DOMRect} - A candidate element * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {Number} The distance scoring between two elements */ function getDistance(searchOrigin, candidateRect, dir) { const kOrthogonalWeightForLeftRight = 30; const kOrthogonalWeightForUpDown = 2; let orthogonalBias = 0; let alignBias = 0; const alignWeight = 5.0; // Get exit point, entry point -> {x: '', y: ''}; const points = getEntryAndExitPoints(dir, searchOrigin, candidateRect); // Find the points P1 inside the border box of starting point and P2 inside the border box of candidate // that minimize the distance between these two points const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x); const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y); // A: The euclidean distance between P1 and P2. const A = Math.sqrt(P1 ** 2 + P2 ** 2); let B; let C; // B: The absolute distance in the direction which is orthogonal to dir between P1 and P2, or 0 if dir is null. // C: The intersection edges between a candidate and the starting point. // D: The square root of the area of intersection between the border boxes of candidate and starting point const intersectionRect = getIntersectionRect(searchOrigin, candidateRect); const D = intersectionRect.area; switch (dir) { case 'left': /* falls through */ case 'right': // If two elements are aligned, add align bias // else, add orthogonal bias if (isAligned(searchOrigin, candidateRect, dir)) alignBias = Math.min(intersectionRect.height / searchOrigin.height, 1); else orthogonalBias = (searchOrigin.height / 2); B = (P2 + orthogonalBias) * kOrthogonalWeightForLeftRight; C = alignWeight * alignBias; break; case 'up': /* falls through */ case 'down': // If two elements are aligned, add align bias // else, add orthogonal bias if (isAligned(searchOrigin, candidateRect, dir)) alignBias = Math.min(intersectionRect.width / searchOrigin.width, 1); else orthogonalBias = (searchOrigin.width / 2); B = (P1 + orthogonalBias) * kOrthogonalWeightForUpDown; C = alignWeight * alignBias; break; default: B = 0; C = 0; break; } return (A + B - C - D); } /** * Get the euclidean distance between the search origin and a candidate element considering the direction. * @function getEuclideanDistance * @param rect1 {DOMRect} - The search origin * @param rect2 {DOMRect} - A candidate element * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {Number} The distance scoring between two elements */ function getEuclideanDistance(rect1, rect2, dir) { // Get exit point, entry point const points = getEntryAndExitPoints(dir, rect1, rect2); // Find the points P1 inside the border box of starting point and P2 inside the border box of candidate // that minimize the distance between these two points const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x); const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y); // Return the euclidean distance between P1 and P2. return Math.sqrt(P1 ** 2 + P2 ** 2); } /** * Get the absolute distance between the search origin and a candidate element considering the direction. * @function getAbsoluteDistance * @param rect1 {DOMRect} - The search origin * @param rect2 {DOMRect} - A candidate element * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD) * @returns {Number} The distance scoring between two elements */ function getAbsoluteDistance(rect1, rect2, dir) { // Get exit point, entry point const points = getEntryAndExitPoints(dir, rect1, rect2); // Return the absolute distance in the dir direction between P1 and P. return ((dir === 'left') || (dir === 'right')) ? Math.abs(points.entryPoint.x - points.exitPoint.x) : Math.abs(points.entryPoint.y - points.exitPoint.y); } /** * Get entry point and exit point of two elements considering the direction. * @function getEntryAndExitPoints * @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD). Default value for dir is 'down'. * @param searchOrigin {DOMRect | Point} - The search origin which contains the exit point * @param candidateRect {DOMRect} - One of candidates which contains the entry point * @returns {Points} The exit point from the search origin and the entry point from a candidate */ function getEntryAndExitPoints(dir = 'down', searchOrigin, candidateRect) { /** * User type definition for Point * @typeof {Object} Points * @property {Point} Points.entryPoint * @property {Point} Points.exitPoint */ const points = { entryPoint: { x: 0, y: 0 }, exitPoint: { x: 0, y: 0 } }; if (startingPoint) { points.exitPoint = searchOrigin; switch (dir) { case 'left': points.entryPoint.x = candidateRect.right; break; case 'up': points.entryPoint.y = candidateRect.bottom; break; case 'right': points.entryPoint.x = candidateRect.left; break; case 'down': points.entryPoint.y = candidateRect.top; break; } // Set orthogonal direction switch (dir) { case 'left': case 'right': if (startingPoint.y <= candidateRect.top) { points.entryPoint.y = candidateRect.top; } else if (startingPoint.y < candidateRect.bottom) { points.entryPoint.y = startingPoint.y; } else { points.entryPoint.y = candidateRect.bottom; } break; case 'up': case 'down': if (startingPoint.x <= candidateRect.left) { points.entryPoint.x = candidateRect.left; } else if (startingPoint.x < candidateRect.right) { points.entryPoint.x = startingPoint.x; } else { points.entryPoint.x = candidateRect.right; } break; } } else { // Set direction switch (dir) { case 'left': points.exitPoint.x = searchOrigin.left; points.entryPoint.x = (candidateRect.right < searchOrigin.left) ? candidateRect.right : searchOrigin.left; break; case 'up': points.exitPoint.y = searchOrigin.top; points.entryPoint.y = (candidateRect.bottom < searchOrigin.top) ? candidateRect.bottom : searchOrigin.top; break; case 'right': points.exitPoint.x = searchOrigin.right; points.entryPoint.x = (candidateRect.left > searchOrigin.right) ? candidateRect.left : searchOrigin.right; break; case 'down': points.exitPoint.y = searchOrigin.bottom; points.entryPoint.y = (candidateRect.top > searchOrigin.bottom) ? candidateRect.top : searchOrigin.bottom; break; } // Set orthogonal direction switch (dir) { case 'left': case 'right': if (isBelow(searchOrigin, candidateRect)) { points.exitPoint.y = searchOrigin.top; points.entryPoint.y = (candidateRect.bottom < searchOrigin.top) ? candidateRect.bottom : searchOrigin.top; } else if (isBelow(candidateRect, searchOrigin)) { points.exitPoint.y = searchOrigin.bottom; points.entryPoint.y = (candidateRect.top > searchOrigin.bottom) ? candidateRect.top : searchOrigin.bottom; } else { points.exitPoint.y = Math.max(searchOrigin.top, candidateRect.top); points.entryPoint.y = points.exitPoint.y; } break; case 'up': case 'down': if (isRightSide(searchOrigin, candidateRect)) { points.exitPoint.x = searchOrigin.left; points.entryPoint.x = (candidateRect.right < searchOrigin.left) ? candidateRect.right : searchOrigin.left; } else if (isRightSide(candidateRect, searchOrigin)) { points.exitPoint.x = searchOrigin.right; points.entryPoint.x = (candidateRect.left > searchOrigin.right) ? candidateRect.left : searchOrigin.right; } else { points.exitPoint.x = Math.max(searchOrigin.left, candidateRect.left); points.entryPoint.x = points.exitPoint.x; } break; } } return points; } /** * Find focusable elements within the container * @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance} * @function getIntersectionRect * @param rect1 {DOMRect} - The search origin which contains the exit point * @param rect2 {DOMRect} - One of candidates which contains the entry point * @returns {IntersectionArea} The intersection area between two elements. * * @typeof {Object} IntersectionArea * @property {Number} IntersectionArea.width * @property {Number} IntersectionArea.height */ function getIntersectionRect(rect1, rect2) { const intersection_rect = { width: 0, height: 0, area: 0 }; const new_location = [Math.max(rect1.left, rect2.left), Math.max(rect1.top, rect2.top)]; const new_max_point = [Math.min(rect1.right, rect2.right), Math.min(rect1.bottom, rect2.bottom)]; intersection_rect.width = Math.abs(new_location[0] - new_max_point[0]); intersection_rect.height = Math.abs(new_location[1] - new_max_point[1]); if (!(new_location[0] >= new_max_point[0] || new_location[1] >= new_max_point[1])) { // intersecting-cases intersection_rect.area = Math.sqrt(intersection_rect.width * intersection_rect.height); } return intersection_rect; } /** * Handle the spatial navigation behavior for HTMLInputElement, HTMLTextAreaElement * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input|HTMLInputElement (MDN)} * @function handlingEditableElement * @param e {Event} - keydownEvent * @returns {boolean} */ function handlingEditableElement(e) { const SPINNABLE_INPUT_TYPES = ['email', 'date', 'month', 'number', 'time', 'week']; const TEXT_INPUT_TYPES = ['password', 'text', 'search', 'tel', 'url', null]; const eventTarget = document.activeElement; const startPosition = eventTarget.selectionStart; const endPosition = eventTarget.selectionEnd; const focusNavigableArrowKey = { left: false, up: false, right: false, down: false, }; const dir = ARROW_KEY_CODE[e.keyCode]; if (dir === undefined) { return focusNavigableArrowKey; } if (SPINNABLE_INPUT_TYPES.includes(eventTarget.getAttribute('type')) && (dir === 'up' || dir === 'down')) { focusNavigableArrowKey[dir] = true; } else if (TEXT_INPUT_TYPES.includes(eventTarget.getAttribute('type')) || eventTarget.nodeName === 'TEXTAREA') { if (startPosition === endPosition) { // if there isn't any selected text if (startPosition === 0) { focusNavigableArrowKey.left = true; focusNavigableArrowKey.up = true; } if (endPosition === eventTarget.value.length) { focusNavigableArrowKey.right = true; focusNavigableArrowKey.down = true; } } } else { // HTMLDataListElement, HTMLSelectElement, HTMLOptGroup focusNavigableArrowKey[dir] = true; } return focusNavigableArrowKey; } /** * Get the DOMRect of an element * @function getBoundingClientRect * @param {Node} element * @returns {DOMRect} */ function getBoundingClientRect(element) { // memoization let rect = mapOfBoundRect && mapOfBoundRect.get(element); if (!rect) { const boundingClientRect = element.getBoundingClientRect(); rect = { top: Number(boundingClientRect.top.toFixed(2)), right: Number(boundingClientRect.right.toFixed(2)), bottom: Number(boundingClientRect.bottom.toFixed(2)), left: Number(boundingClientRect.left.toFixed(2)), width: Number(boundingClientRect.width.toFixed(2)), height: Number(boundingClientRect.height.toFixed(2)), }; mapOfBoundRect && mapOfBoundRect.set(element, rect); } return rect; } /** * Get the candidates which is fully inside the target element in visual * @param {Node} targetElement * @returns {sequence} overlappedCandidates */ function getOverlappedCandidates(targetElement) { const container = targetElement.getSpatialNavigationContainer(); const candidates = container.focusableAreas(); const overlappedCandidates = []; candidates.forEach((element) => { if ((targetElement !== element) && isEntirelyVisible(element, targetElement)) { overlappedCandidates.push(element); } }); return overlappedCandidates; } /** * Get the list of the experimental APIs * @function getExperimentalAPI */ function getExperimentalAPI() { function canScroll(container, dir) { return (isScrollable(container, dir) && !isScrollBoundary(container, dir)) || (!container.parentElement && !isHTMLScrollBoundary(container, dir)); } function findTarget(findCandidate, element, dir, option) { let eventTarget = element; let bestNextTarget = null; // 4 if (eventTarget === document || eventTarget === document.documentElement) { eventTarget = document.body || document.documentElement; } // 5 // At this point, spatialNavigationSearch can be applied. // If startingPoint is either a scroll container or the document, // find the best candidate within startingPoint if ((isContainer(eventTarget) || eventTarget.nodeName === 'BODY') && !(eventTarget.nodeName === 'INPUT')) { if (eventTarget.nodeName === 'IFRAME') eventTarget = eventTarget.contentDocument.body; const candidates = getSpatialNavigationCandidates(eventTarget, option); // 5-2 if (Array.isArray(candidates) && candidates.length > 0) { return findCandidate ? getFilteredSpatialNavigationCandidates(eventTarget, dir, candidates) : eventTarget.spatialNavigationSearch(dir, { candidates }); } if (canScroll(eventTarget, dir)) { return findCandidate ? [] : eventTarget; } } // 6 // Let container be the nearest ancestor of eventTarget let container = eventTarget.getSpatialNavigationContainer(); let parentContainer = (container.parentElement) ? container.getSpatialNavigationContainer() : null; // When the container is the viewport of a browsing context if (!parentContainer && (window.location !== window.parent.location)) { parentContainer = window.parent.document.documentElement; } // 7 while (parentContainer) { const candidates = filteredCandidates(eventTarget, getSpatialNavigationCandidates(container, option), dir, container); if (Array.isArray(candidates) && candidates.length > 0) { bestNextTarget = eventTarget.spatialNavigationSearch(dir, { candidates, container }); if (bestNextTarget) { return findCandidate ? candidates : bestNextTarget; } } // If there isn't any candidate and the best candidate among candidate: // 1) Scroll or 2) Find candidates of the ancestor container // 8 - if else if (canScroll(container, dir)) { return findCandidate ? [] : eventTarget; } else if (container === document || container === document.documentElement) { container = window.document.documentElement; // The page is in an iframe if (window.location !== window.parent.location) { // eventTarget needs to be reset because the position of the element in the IFRAME // is unuseful when the focus moves out of the iframe eventTarget = window.frameElement; container = window.parent.document.documentElement; if (container.parentElement) parentContainer = container.getSpatialNavigationContainer(); else { parentContainer = null; break; } } } else { // avoiding when spatnav container with tabindex=-1 if (isFocusable(container)) { eventTarget = container; } container = parentContainer; if (container.parentElement) parentContainer = container.getSpatialNavigationContainer(); else { parentContainer = null; break; } } } if (!parentContainer && container) { // Getting out from the current spatnav container const candidates = filteredCandidates(eventTarget, getSpatialNavigationCandidates(container, option), dir, container); // 9 if (Array.isArray(candidates) && candidates.length > 0) { bestNextTarget = eventTarget.spatialNavigationSearch(dir, { candidates, container }); if (bestNextTarget) { return findCandidate ? candidates : bestNextTarget; } } } if (canScroll(container, dir)) { bestNextTarget = eventTarget; return bestNextTarget; } } return { isContainer, isScrollContainer, isVisibleInScroller, findCandidates: findTarget.bind(null, true), findNextTarget: findTarget.bind(null, false), getDistanceFromTarget: (element, candidateElement, dir) => { if ((isContainer(element) || element.nodeName === 'BODY') && !(element.nodeName === 'INPUT')) { if (getSpatialNavigationCandidates(element).includes(candidateElement)) { return getInnerDistance(getBoundingClientRect(element), getBoundingClientRect(candidateElement), dir); } } return getDistance(getBoundingClientRect(element), getBoundingClientRect(candidateElement), dir); }, }; } /** * Makes to use the experimental APIs. * @function enableExperimentalAPIs * @param option {boolean} - If it is true, the experimental APIs can be used or it cannot. */ function enableExperimentalAPIs(option) { const currentKeyMode = window.__spatialNavigation__ && window.__spatialNavigation__.keyMode; window.__spatialNavigation__ = (option === false) ? getInitialAPIs() : Object.assign(getInitialAPIs(), getExperimentalAPI()); window.__spatialNavigation__.keyMode = currentKeyMode; Object.seal(window.__spatialNavigation__); } /** * Set the environment for using the spatial navigation polyfill. * @function getInitialAPIs */ function getInitialAPIs() { return { enableExperimentalAPIs, get keyMode() { return this._keymode ? this._keymode : 'ARROW'; }, set keyMode(mode) { this._keymode = (['SHIFTARROW', 'ARROW', 'NONE'].includes(mode)) ? mode : 'ARROW'; }, setStartingPoint(x, y) { startingPoint = (x && y) ? { x, y } : null; }, }; } initiateSpatialNavigation(); enableExperimentalAPIs(false); window.addEventListener('load', () => { spatialNavigationHandler(); }); }()); ================================================ FILE: frontend/js/ui.js ================================================ import packageJSON from '../../package.json'; function logIt(message) { const textareaConsoleLog = document.getElementById('textareaConsoleLog'); console.log(message); textareaConsoleLog.value += `${message}\n`; } /* eslint-disable func-names */ window.switchView = function (view) { const service = document.getElementById('service'); const settings = document.getElementById('settings'); const logs = document.getElementById('logs'); const about = document.getElementById('about'); const btnservice = document.getElementById('btnNavService'); const btnsettings = document.getElementById('btnNavSettings'); const btnlogs = document.getElementById('btnNavLogs'); const btnabout = document.getElementById('btnNavAbout'); const settingItemsAdv = document.getElementById('settingItemsAdv'); const settingItemsNormal = document.getElementById('settingItemsNormal'); const btnAdvanced = document.getElementById('btnSettingsAdvanced'); switch (view) { case 'service': service.style.display = 'block'; btnservice.style.background = 'white'; btnservice.style.color = 'black'; settings.style.display = 'none'; btnsettings.style.background = null; btnsettings.style.color = null; logs.style.display = 'none'; btnlogs.style.background = null; btnlogs.style.color = null; about.style.display = 'none'; btnabout.style.background = null; btnabout.style.color = null; break; case 'settings': service.style.display = 'none'; btnservice.style.background = null; btnservice.style.color = null; settings.style.display = 'block'; btnsettings.style.background = 'white'; btnsettings.style.color = 'black'; logs.style.display = 'none'; btnlogs.style.background = null; btnlogs.style.color = null; about.style.display = 'none'; btnabout.style.background = null; btnabout.style.color = null; // Open non advanced page btnAdvanced.style.background = null; btnAdvanced.style.color = null; settingItemsNormal.style.display = 'block'; settingItemsAdv.style.display = 'none'; break; case 'logs': service.style.display = 'none'; btnservice.style.background = null; btnservice.style.color = null; settings.style.display = 'none'; btnsettings.style.background = null; btnsettings.style.color = null; logs.style.display = 'block'; btnlogs.style.background = 'white'; btnlogs.style.color = 'black'; about.style.display = 'none'; btnabout.style.background = null; btnabout.style.color = null; break; case 'about': service.style.display = 'none'; btnservice.style.background = null; btnservice.style.color = null; settings.style.display = 'none'; btnsettings.style.background = null; btnsettings.style.color = null; logs.style.display = 'none'; btnlogs.style.background = null; btnlogs.style.color = null; about.style.display = 'block'; btnabout.style.background = 'white'; btnabout.style.color = 'black'; break; default: service.style.display = null; btnservice.style.background = null; btnservice.style.color = null; settings.style.display = null; logs.style.display = null; about.style.display = null; break; } }; window.resolutionChanged = function (elem) { document.getElementById('manualres').style.display = elem.value === 'manual' ? 'inline' : 'none'; }; window.socketCheckChanged = function (elem) { if (elem.checked === true) { document.getElementById('settingaddressport').style.display = 'none'; document.getElementById('settingsocket').style.display = 'flex'; } else { document.getElementById('settingaddressport').style.display = 'flex'; document.getElementById('settingsocket').style.display = 'none'; } }; window.socketSelectChanged = function (elem) { document.getElementById('manualsocket').style.display = elem.value === 'manual' ? 'inline' : 'none'; }; window.toggleAdvanced = function () { const settingItemsAdv = document.getElementById('settingItemsAdv'); const settingItemsNormal = document.getElementById('settingItemsNormal'); const btnAdvanced = document.getElementById('btnSettingsAdvanced'); if (settingItemsNormal.style.display === 'block') { btnAdvanced.style.background = 'white'; btnAdvanced.style.color = 'black'; settingItemsNormal.style.display = 'none'; settingItemsAdv.style.display = 'block'; } else { btnAdvanced.style.background = null; btnAdvanced.style.color = null; settingItemsNormal.style.display = 'block'; settingItemsAdv.style.display = 'none'; } }; window.switchLog = function (location) { const divConsoleLog = document.getElementById('consoleLog'); const divHyperionLog = document.getElementById('hyperionLog'); const btnLogSwitchPicCap = document.getElementById('btnLogSwitchPicCap'); const btnLogSwitchHyperion = document.getElementById('btnLogSwitchHyperion'); if (location === 'hyperion') { divConsoleLog.style.display = 'none'; divHyperionLog.style.display = 'block'; btnLogSwitchHyperion.style.background = 'white'; btnLogSwitchHyperion.style.color = 'black'; btnLogSwitchPicCap.style.background = null; btnLogSwitchPicCap.style.color = null; } else { divConsoleLog.style.display = 'block'; divHyperionLog.style.display = 'none'; btnLogSwitchPicCap.style.background = 'white'; btnLogSwitchPicCap.style.color = 'black'; btnLogSwitchHyperion.style.background = null; btnLogSwitchHyperion.style.color = null; } }; /* eslint-enable func-names */ function saveLightMode(color) { logIt(`Saving ${color} as light mode.`); localStorage.setItem('lightMode', color); } /* eslint-disable func-names */ window.switchLightMode = function (color) { const btnLightBlue = document.getElementById('btnLightBlue'); const btnLightDark = document.getElementById('btnLightDark'); const btnLightBlack = document.getElementById('btnLightBlack'); switch (color) { case 'blue': btnLightBlue.style.background = 'white'; btnLightBlue.style.color = 'black'; btnLightDark.style.background = null; btnLightDark.style.color = null; btnLightBlack.style.background = null; btnLightBlack.style.color = null; document.querySelectorAll('.darkMode, .blackMode').forEach((elem) => elem.classList.add('blueMode')); document.querySelectorAll('.darkMode').forEach((elem) => elem.classList.remove('darkMode')); document.querySelectorAll('.blackMode').forEach((elem) => elem.classList.remove('blackMode')); saveLightMode('blue'); break; case 'dark': btnLightBlue.style.background = null; btnLightBlue.style.color = null; btnLightDark.style.background = 'white'; btnLightDark.style.color = 'black'; btnLightBlack.style.background = null; btnLightBlack.style.color = null; document.querySelectorAll('.blueMode, .blackMode').forEach((elem) => elem.classList.add('darkMode')); document.querySelectorAll('.blueMode').forEach((elem) => elem.classList.remove('blueMode')); document.querySelectorAll('.blackMode').forEach((elem) => elem.classList.remove('blackMode')); saveLightMode('dark'); break; case 'black': btnLightBlue.style.background = null; btnLightBlue.style.color = null; btnLightDark.style.background = null; btnLightDark.style.color = null; btnLightBlack.style.background = 'white'; btnLightBlack.style.color = 'black'; document.querySelectorAll('.blueMode, .darkMode').forEach((elem) => elem.classList.add('blackMode')); document.querySelectorAll('.blueMode').forEach((elem) => elem.classList.remove('blueMode')); document.querySelectorAll('.darkMode').forEach((elem) => elem.classList.remove('darkMode')); saveLightMode('black'); break; default: logIt(`${color} not found. Using blue as default.`); /* eslint-disable no-undef */ switchLightMode('blue'); /* eslint-enable no-undef */ break; } }; function loadLightMode() { const lightMode = localStorage.getItem('lightMode'); /* eslint-disable no-undef */ switchLightMode(lightMode); /* eslint-enable no-undef */ } function getJSON(url, callback) { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'json'; xhr.onload = function () { const { status } = xhr; if (status === 200) { callback(null, xhr.response); } else { callback(status); } }; xhr.send(); } /* eslint-enable func-names */ function getContributors(owner, repo) { getJSON(`https://api.github.com/repos/${owner}/${repo}/contributors`, (err, data) => { if (err != null) { console.error(err); } else { const resp = data; let div = document.querySelector('.hyperionwebosContributors'); if (repo === 'piccap') { div = document.querySelector('.piccapContributors'); } const users = resp.map((u) => u.login); const avatars = resp.map((a) => a.avatar_url); let count = 0; let pos = 0; let last = document.createElement('ul'); users.forEach((user) => { const lielem = document.createElement('li'); lielem.setAttribute('id', `li${user}`); const pelem = document.createElement('p'); const imgelem = document.createElement('img'); imgelem.setAttribute('src', avatars[pos]); pos += 1; pelem.appendChild(imgelem); pelem.innerHTML += user; lielem.appendChild(pelem); if (count >= 3) { const brelem = document.createElement('br'); div.appendChild(brelem); last = document.createElement('ul'); div.appendChild(last); count = 0; } count += 1; last.appendChild(lielem); div.appendChild(last); }); } }); } getContributors('webosbrew', 'hyperion-webos'); getContributors('tbsniller', 'piccap'); window.addEventListener('load', () => { /* eslint-disable no-undef */ switchView('service'); switchLog('piccap'); loadLightMode(); /* eslint-enable no-undef */ const piccapVersion = packageJSON.version; document.getElementById('txtPicCapVersion').innerHTML = `v${piccapVersion}`; }); ================================================ FILE: frontend/webOSTVjs-1.2.4/LICENSE-2.0.txt ================================================ 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. ================================================ FILE: frontend/webOSTVjs-1.2.4/webOSTV-dev.js ================================================ window.webOSDev=function(e){var r={};function t(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,t),i.l=!0,i.exports}return t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:n})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(t.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var i in e)t.d(n,i,function(r){return e[r]}.bind(null,i));return n},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";function n(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r&&(n=n.filter((function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable}))),t.push.apply(t,n)}return t}function i(e){for(var r=1;r0&&void 0!==arguments[0]?arguments[0]:function(){},r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){},t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"";if(!this.cancelled){var i={};try{i=JSON.parse(n)}catch(e){i={errorCode:-1,errorText:n,returnValue:!1}}var o=i,u=o.errorCode,c=o.returnValue;u||!1===c?(i.returnValue=!1,r(i)):(i.returnValue=!0,e(i)),t(i),this.subscribe||this.cancel()}}},{key:"cancel",value:function(){this.cancelled=!0,null!==this.bridge&&(this.bridge.cancel(),this.bridge=null),this.ts&&c[this.ts]&&delete c[this.ts]}}])&&u(r.prototype,t),n&&u(r,n),e}(),s={BROWSER:"APP_BROWSER"},l=function(e){var r=e.id,t=void 0===r?"":r,n=e.params,i=void 0===n?{}:n,o=e.onSuccess,u=void 0===o?function(){}:o,c=e.onFailure,l=void 0===c?function(){}:c,d={id:t,params:i};s.BROWSER===t&&(d.params.target=i.target||"",d.params.fullMode=!0,d.id="com.webos.app.browser"),function(e){var r=e.parameters,t=e.onSuccess,n=e.onFailure;(new a).send({service:"luna://com.webos.applicationManager",method:"launch",parameters:r,onComplete:function(e){var r=e.returnValue,i=e.errorCode,o=e.errorText;return!0===r?t():n({errorCode:i,errorText:o})}})}({parameters:d,onSuccess:u,onFailure:l})},d=function(){var e={};if(window.PalmSystem&&""!==window.PalmSystem.launchParams)try{e=JSON.parse(window.PalmSystem.launchParams)}catch(e){console.error("JSON parsing error")}return e},f=function(){return window.PalmSystem&&window.PalmSystem.identifier?window.PalmSystem.identifier.split(" ")[0]:""};function v(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r&&(n=n.filter((function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable}))),t.push.apply(t,n)}return t}function p(e){for(var r=1;r0&&void 0!==arguments[0]?arguments[0]:function(){},r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};setTimeout((function(){return e(r)}),0)},j=function(e){return e.state===w&&""!==e.getClientId()},D=function(e,r){var t=r.errorCode,n=void 0===t?h.UNKNOWN_ERROR:t,i=r.errorText,o={errorCode:n,errorText:void 0===i?"Unknown error.":i};return e.setError(o),o},E={errorCode:h.CLIENT_NOT_LOADED,errorText:"DRM client is not loaded."},I=function(){function e(r){!function(e,r){if(!(e instanceof r))throw new TypeError("Cannot call a class as a function")}(this,e),this.clientId="",this.drmType=r,this.errorCode=h.NOT_ERROR,this.errorText="",this.state=O}var r,t,n;return r=e,(t=[{key:"getClientId",value:function(){return this.clientId}},{key:"getDrmType",value:function(){return this.drmType}},{key:"getErrorCode",value:function(){return this.errorCode}},{key:"getErrorText",value:function(){return this.errorText}},{key:"setError",value:function(e){var r=e.errorCode,t=e.errorText;this.errorCode=r,this.errorText=t}},{key:"isLoaded",value:function(e){var r=this,t=e.onSuccess,n=void 0===t?function(){}:t,i=e.onFailure,o=void 0===i?function(){}:i;S({method:"isLoaded",parameters:{appId:f()},onComplete:function(e){if(!0===e.returnValue){if(r.clientId=e.clientId||"",r.state=e.loadStatus?w:O,!0===e.loadStatus&&e.drmType!==r.drmType)return o(D(r,{errorCode:h.UNKNOWN_ERROR,errorText:"DRM types of set and loaded are not matched."}));var t=p({},e);return delete t.returnValue,n(t)}return o(D(r,e))}})}},{key:"load",value:function(e){var r=this,t=e.onSuccess,n=void 0===t?function(){}:t,i=e.onFailure,o=void 0===i?function(){}:i;if(this.state!==g&&this.state!==w){var u={appId:f(),drmType:this.drmType};this.state=g,S({method:"load",onComplete:function(e){return!0===e.returnValue?(r.clientId=e.clientId,r.state=w,n({clientId:r.clientId})):o(D(r,e))},parameters:u})}else T(n,{isLoaded:!0,clientId:this.clientId})}},{key:"unload",value:function(e){var r=this,t=e.onSuccess,n=void 0===t?function(){}:t,i=e.onFailure,o=void 0===i?function(){}:i;if(j(this)){var u={clientId:this.clientId};this.state=P,S({method:"unload",onComplete:function(e){return!0===e.returnValue?(r.clientId="",r.state=O,n()):o(D(r,e))},parameters:u})}else T(o,D(this,E))}},{key:"getRightsError",value:function(e){var r=this,t=e.onSuccess,n=void 0===t?function(){}:t,i=e.onFailure,o=void 0===i?function(){}:i;j(this)?S({method:"getRightsError",parameters:{clientId:this.clientId,subscribe:!0},onComplete:function(e){if(!0===e.returnValue){var t=p({},e);return delete t.returnValue,n(t)}return o(D(r,e))}}):T(o,D(this,E))}},{key:"sendDrmMessage",value:function(e){var r=this,t=e.msg,n=void 0===t?"":t,i=e.onSuccess,o=void 0===i?function(){}:i,u=e.onFailure,c=void 0===u?function(){}:u;if(j(this)){var a=function(e){var r="",t="";switch(e){case y.PLAYREADY:r="application/vnd.ms-playready.initiator+xml",t="urn:dvb:casystemid:19219";break;case y.WIDEVINE:r="application/widevine+xml",t="urn:dvb:casystemid:19156"}return{msgType:r,drmSystemId:t}}(this.drmType),s=p({clientId:this.clientId,msg:n},a);S({method:"sendDrmMessage",onComplete:function(e){if(!0===e.returnValue){var t=p({},e);return delete t.returnValue,o(t)}return c(D(r,e))},parameters:s})}else T(c,D(this,E))}}])&&m(r.prototype,t),n&&m(r,n),e}(),R={Error:h,Type:y},C=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return""===e?null:new I(e)};function N(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r&&(n=n.filter((function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable}))),t.push.apply(t,n)}return t}function _(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[r]=t,e}var x=function(e){var r=e.service,t=e.subscribe,n=e.onSuccess,i=e.onFailure;(new a).send({service:r,method:"getStatus",parameters:{subscribe:t},onComplete:function(e){var r=function(e){for(var r=1;r-1&&(c="palm"),x({service:"luna://com.".concat(c,".connectionmanager"),subscribe:u,onSuccess:t,onFailure:i})}},k=function(e){var r=e.onSuccess,t=void 0===r?function(){}:r,n=e.onFailure,i=void 0===n?function(){}:n;-1!==navigator.userAgent.indexOf("Chrome")?(new a).send({service:"luna://com.webos.service.sm",method:"deviceid/getIDs",parameters:{idType:["LGUDID"]},onComplete:function(e){if(!0!==e.returnValue)i({errorCode:e.errorCode,errorText:e.errorText});else{var r=e.idList.filter((function(e){return"LGUDID"===e.idType}))[0].idValue;t({id:r})}}}):setTimeout((function(){return i({errorCode:"ERROR.000",errorText:"Not supported."})}),0)}}]); ================================================ FILE: frontend/webOSTVjs-1.2.4/webOSTV.js ================================================ window.webOS=function(e){var n={};function r(t){if(n[t])return n[t].exports;var o=n[t]={i:t,l:!1,exports:{}};return e[t].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=n,r.d=function(e,n,t){r.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,n){if(1&n&&(e=r(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(r.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)r.d(t,o,function(n){return e[n]}.bind(null,o));return t},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=1)}([function(e){e.exports=JSON.parse('{"name":"webostv-js","version":"1.2.4","description":"","main":"index.js","scripts":{"belazy":"npm run lint && npm run docs && npm run release","build":"node scripts/build.js","build:dev":"node scripts/build.js develop","clean":"git clean -xdf","docs":"jsdoc -c jsdoc.json","lint":"eslint . --cache","release":"node scripts/release.js","test":"node scripts/test.js app","test:mocha":"node scripts/test.js mocha"},"repository":{"type":"git","url":"http://mod.lge.com/hub/tvsdk/webostv-js.git"},"keywords":[],"author":"LGE TV Lab","license":"Apache-2.0","dependencies":{"address":"^1.0.3","archiver":"^4.0.1","chalk":"^2.4.1","command-exists":"^1.2.7","fs-extra":"^8.1.0","jsdoc":"^3.5.5","webpack":"^4.10.1","webpack-dev-server":"^3.1.4","webpack-merge":"^4.1.2"},"devDependencies":{"@babel/cli":"^7.10.1","@babel/core":"^7.10.2","@babel/polyfill":"^7.10.1","@babel/preset-env":"^7.10.2","babel-loader":"^8.1.0","eslint":"^4.19.1","eslint-config-airbnb-base":"^12.1.0","eslint-loader":"^2.0.0","eslint-plugin-import":"^2.12.0","html-webpack-plugin":"^4.3.0","mocha":"^5.2.0","mocha-loader":"^1.1.3"}}')},function(e,n,r){"use strict";r.r(n),r.d(n,"deviceInfo",(function(){return P})),r.d(n,"fetchAppId",(function(){return t})),r.d(n,"fetchAppInfo",(function(){return i})),r.d(n,"fetchAppRootPath",(function(){return s})),r.d(n,"keyboard",(function(){return x})),r.d(n,"libVersion",(function(){return D})),r.d(n,"platformBack",(function(){return a})),r.d(n,"platform",(function(){return V})),r.d(n,"service",(function(){return p})),r.d(n,"systemInfo",(function(){return k}));var t=function(){return window.PalmSystem&&window.PalmSystem.identifier?window.PalmSystem.identifier.split(" ")[0]:""},o={},i=function(e,n){if(0===Object.keys(o).length){var r=function(n,r){if(!n&&r)try{o=JSON.parse(r),e&&e(o)}catch(n){console.error("Unable to parse appinfo.json file for",t()),e&&e()}else e&&e()},i=new window.XMLHttpRequest;i.onreadystatechange=function(){4===i.readyState&&(i.status>=200&&i.status<300||0===i.status?r(null,i.responseText):r({status:404}))};try{i.open("GET",n||"appinfo.json",!0),i.send(null)}catch(e){r({status:404})}}else e&&e(o)},s=function(){var e=window.location.href;if("baseURI"in window.document)e=window.document.baseURI;else{var n=window.document.getElementsByTagName("base");n.length>0&&(e=n[0].href)}var r=e.match(new RegExp(".*://[^#]*/"));return r?r[0]:""},a=function(){if(window.PalmSystem&&window.PalmSystem.platformBack)return window.PalmSystem.platformBack()};function c(e,n){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);n&&(t=t.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),r.push.apply(r,t)}return r}function l(e){for(var n=1;n0&&void 0!==arguments[0]?arguments[0]:function(){},n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){},r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},t=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"";if(!this.cancelled){var o={};try{o=JSON.parse(t)}catch(e){o={errorCode:-1,errorText:t,returnValue:!1}}var i=o,s=i.errorCode,a=i.returnValue;s||!1===a?(o.returnValue=!1,n(o)):(o.returnValue=!0,e(o)),r(o),this.subscribe||this.cancel()}}},{key:"cancel",value:function(){this.cancelled=!0,null!==this.bridge&&(this.bridge.cancel(),this.bridge=null),this.ts&&f[this.ts]&&delete f[this.ts]}}])&&u(n.prototype,r),t&&u(n,t),e}(),p={request:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=l({service:e},n);return(new m).send(r)}};function v(e){return(v="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var w={};if("object"===("undefined"==typeof window?"undefined":v(window))&&window.PalmSystem){if(window.navigator.userAgent.indexOf("SmartWatch")>-1)w.watch=!0;else if(window.navigator.userAgent.indexOf("SmartTV")>-1||window.navigator.userAgent.indexOf("Large Screen")>-1)w.tv=!0;else{try{var b=JSON.parse(window.PalmSystem.deviceInfo||"{}");if(b.platformVersionMajor&&b.platformVersionMinor){var h=Number(b.platformVersionMajor),y=Number(b.platformVersionMinor);h<3||3===h&&y<=0?w.legacy=!0:w.open=!0}}catch(e){w.open=!0}window.Mojo=window.Mojo||{relaunch:function(){}},window.PalmSystem.stageReady&&window.PalmSystem.stageReady()}if(window.navigator.userAgent.indexOf("Chr0me")>-1||window.navigator.userAgent.indexOf("Chrome")>-1){var g=window.navigator.userAgent.indexOf("Chr0me")>-1?window.navigator.userAgent.indexOf("Chr0me"):window.navigator.userAgent.indexOf("Chrome"),S=window.navigator.userAgent.slice(g).indexOf(" "),O=window.navigator.userAgent.slice(g+7,g+S).split(".");w.chrome=Number(O[0])}else w.chrome=0}else w.unknown=!0;var V=w,j={},P=function(e){if(0===Object.keys(j).length){try{var n=JSON.parse(window.PalmSystem.deviceInfo);j.modelName=n.modelName,j.version=n.platformVersion,j.versionMajor=n.platformVersionMajor,j.versionMinor=n.platformVersionMinor,j.versionDot=n.platformVersionDot,j.sdkVersion=n.platformVersion,j.screenWidth=n.screenWidth,j.screenHeight=n.screenHeight}catch(e){j.modelName="webOS Device"}j.screenHeight=j.screenHeight||window.screen.height,j.screenWidth=j.screenWidth||window.screen.width,V.tv&&(j.uhd=!1,j.uhd8K=!1,j.hdr10=!1,j.dolbyVision=!1,j.dolbyAtmos=!1,(new m).send({service:"luna://com.webos.service.config",method:"getConfigs",parameters:{configNames:["tv.model.modelname","tv.nyx.platformVersion","tv.nyx.firmwareVersion","tv.hw.panelResolution","tv.hw.displayType","tv.hw.ddrSize","tv.model.supportHDR","tv.config.supportDolbyHDRContents","tv.config.supportDolbyTVATMOS","tv.model.supportTemp8K"]},onComplete:function(n){if(n.configs&&(j.modelName=n.configs["tv.model.modelname"]||j.modelName,j.sdkVersion=n.configs["tv.nyx.platformVersion"]||j.sdkVersion,j.uhd="UD"===n.configs["tv.hw.panelResolution"]||"8K"===n.configs["tv.hw.panelResolution"],j.uhd8K="8K"===n.configs["tv.hw.panelResolution"]||!0===n.configs["tv.model.supportTemp8K"],j.oled="OLED"===n.configs["tv.hw.displayType"],j.ddrSize=n.configs["tv.hw.ddrSize"],j.hdr10=!0===n.configs["tv.model.supportHDR"],j.dolbyVision=!0===n.configs["tv.config.supportDolbyHDRContents"],j.dolbyAtmos=!0===n.configs["tv.config.supportDolbyTVATMOS"],n.configs["tv.nyx.firmwareVersion"]&&"0.0.0"!==n.configs["tv.nyx.firmwareVersion"]||(n.configs["tv.nyx.firmwareVersion"]=n.configs["tv.nyx.platformVersion"]),n.configs["tv.nyx.firmwareVersion"])){j.version=n.configs["tv.nyx.firmwareVersion"];for(var r=j.version.split("."),t=["versionMajor","versionMinor","versionDot"],o=0;o