Full Code of TBSniller/piccap for AI

main 930627851a41 cached
33 files
187.6 KB
48.6k tokens
96 symbols
1 requests
Download .txt
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.
<img src="https://user-images.githubusercontent.com/51515147/118692431-5b8bbb80-b80a-11eb-9ab2-ab001b5cee96.jpg" width="300" height="200" />
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.
<img src="https://user-images.githubusercontent.com/51515147/118692535-79592080-b80a-11eb-926d-e5fd76c859a3.jpg" width="400" height="500" />
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).
<img src="https://user-images.githubusercontent.com/51515147/118692379-43b43780-b80a-11eb-8dfc-9937718eb3eb.jpg" width="400" height="500" />
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. 
<img src="https://user-images.githubusercontent.com/51515147/118692578-8413b580-b80a-11eb-8613-8e5b7019bec2.jpg" width="400" height="500" />

#### 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:
<img src="https://user-images.githubusercontent.com/51515147/118692940-de147b00-b80a-11eb-9a4c-546f48545994.jpg" width="200" height="200" />

2. Get your power source, the Wagos, the powercables and connect them together:
<img src="https://user-images.githubusercontent.com/51515147/118692957-e371c580-b80a-11eb-9a01-862b02246ae0.jpg" width="200" height="200" />

#### 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
================================================
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PicCap - Hyperion Sender App</title>
    <script src="webOSTVjs-1.2.4/webOSTV.js" charset="utf-8"></script>
    <script src="webOSTVjs-1.2.4/webOSTV-dev.js" charset="utf-8"></script>
    <script type="text/javascript" src="./ui.bundled.js"></script>
    <script type="text/javascript" src="./servicecalls.bundled.js"></script>
    <script type="text/javascript" src="./spatialnavigation.bundled.js"></script>
    <link rel="stylesheet" href="./css/basicui.css" media="screen">
    <link rel="stylesheet" href="./css/blueui.css" media="screen">
    <link rel="stylesheet" href="./css/darkui.css" media="screen">
    <link rel="stylesheet" href="./css/blackui.css" media="screen">
   </head>
  <body>
    <nav>
      <div class="menu blueMode" id="menu">
        <div class="logo blueMode">
          <p>PicCap - Hyperion Sender App</p><p id="txtPicCapVersion">loading...</p>
        </div>
        <ul>
          <li><button onclick="switchView('service')" id="btnNavService">Service</button></li>
          <li><button onclick="switchView('settings')" id="btnNavSettings">Settings</button></li>
          <li><button onclick="switchView('logs')" id="btnNavLogs">Logs</button></li>
          <li><button onclick="switchView('about')" id="btnNavAbout">About</button></li>
        </ul>
      </div>
    </nav>
    <div class="background blueMode"></div>
    <div class="main blueMode">
      <div id="service" class="service blueMode">
        <div class="title blueMode"><p>hyperion-webos</p><p id="txtServiceVersion">loading..</p>
        </div>
        <div class="sub_title blueMode" id="txtServiceStatus">Loading status..</div>
        <div class="btns blueMode">
          <button onclick="serviceStart()" id="btnServiceStart">Start</button>
          <button onclick="serviceStop()" id="btnServiceStop">Stop</button>
        </div>
        <div class="btns blueMode">
          <button onclick="serviceReload()" id="btnServiceReload">Reload</button>
        </div>
      </div>
      
      <div id="settings" class="settings blueMode">
        <div class="settingItems settingItemsNormal blueMode" id="settingItemsNormal">
          <ul>
            <li><p><b>Service settings</b></p></li>
          </ul><br/>
          <ul>
            <li>
              <div class="checkboxes checklabel blueMode">
                <br>
               <label for="checkSettingsLocalSocket">Local socket<input type="checkbox" id="checkSettingsLocalSocket" onchange="socketCheckChanged(this)"/></label>
              </div>
            </li>
            <div class="settingsocket blueMode" id="settingsocket">
              <li>
                <p>Socket</p>
                <select name="selectSettingsSocket" id="selectSettingsSocket" onchange="socketSelectChanged(this)">
                  <option value="hyperhdr">/tmp/hyperhdr-domain</option>
                  <option value="manual">manual</option>
                </select>
              </li>
              <div class="manualsocket blueMode" id="manualsocket">
                <li>
                  <p>Socket path</p>
                  <input type="text" id="txtInputSettingsSocketPath"/>
                </li>
              </div>
            </div>
            <div class="settingaddressport blueMode" id="settingaddressport">
              <li>
                <p>Address</p>
                <input type="text" id="txtInputSettingsAddress"/>
              </li>
              <li>
                <p>Port</p>
                <input type="text" id="txtInputSettingsPort"/>
              </li>
            </div>
            <li>
              <p>Hyperion priority</p>
              <input type="text" id="txtInputSettingsPriority"/>
            </li>
          </ul><br/>
          <ul>
            <li>
              <p>Resolution</p>
                <select name="resolution" id="selectSettingsResolution" onchange="resolutionChanged(this)">
                  <option value="320x180">320x180</option>
                  <option value="256x144">256x144</option>
                  <option value="192x108">192x108</option>
                  <option value="128x78">128x78</option>
                  <option value="manual">Manual</option>
              </select>
            </li>
            <div class="manualres blueMode" id="manualres">
              <li>
                <p>Width</p>
                <input type="text" id="txtInputSettingsWidth"/>
              </li><br/>
              <li>
                <p>Height</p>
                <input type="text" id="txtInputSettingsHeight"/>
              </li>
            </div>
            <li>
              <p>Maximal FPS</p>
              <input type="text" id="txtInputSettingsFPS"/>
            </li>
          </ul><br/> 
          <ul>
            <li><p>_______________________________________________</p></li>
          </ul><br/>
          <ul>
            <li>
              <p>Video capture backend</p>
              <select name="selectSettingsVideoBackend" id="selectSettingsVideoBackend">
                <option value="auto">Automatic detection</option>
                <option value="libdile_vt">libdile_vt (WebOS 3.x+)</option>
                <option value="libvtcapture">libvtcapture (WebOS 5.x+)</option>
                <option value="disabled">disabled</option>
              </select>
            </li>
            <li>
              <p>Graphical capture backend</p>
              <select name="selectSettingsGraphicalBackend" id="selectSettingsGraphicalBackend">
                <option value="auto">Automatic detection</option>
                <option value="libgm">libgm (WebOS 3.x+)</option>
                <option value="libhalgal">libhalgal (WebOS 5.x+)</option>
                <option value="disabled">disabled</option>
              </select>
              </li>
              <li>
                <div class="checkboxes checklabel blueMode">
                  <label for="checkSettingsAutostart">Autostart<input type="checkbox" id="checkSettingsAutostart"/></label><br/>
                  <label for="checkSettingsVSync">VSync<input type="checkbox" id="checkSettingsVSync"/></label>
                  <label for="checkSettingsNV12">NV12<input type="checkbox" id="checkSettingsNV12"/></label>
                </div>
              </li>
            </ul><br/>
        </div>
        <div class="settingItems settingItemsAdv blueMode" id="settingItemsAdv">
          <ul>
           <li><p><b>Service advanced settings</b></p></li>
         </ul><br/><br/>
         <div class="dileVTCheckbox checklabel blueMode">
           <ul>
             <li>
               <label><b>DILE_VT quirks:</b></label>
             </li>
             <li>
               <div class="checkboxes blueMode">
                 <label for="checkSettingsQUIRK_DILE_VT_NO_FREEZE_CAPTURE">QUIRK_DILE_VT_NO_FREEZE_CAPTURE<input type="checkbox" id="checkSettingsQUIRK_DILE_VT_NO_FREEZE_CAPTURE"/></label><br/>
                 <label for="checkSettingsQUIRK_DILE_VT_CREATE_EX">QUIRK_DILE_VT_CREATE_EX<input type="checkbox" id="checkSettingsQUIRK_DILE_VT_CREATE_EX"/></label>
               </div>
             </li>
             <li>
               <div class="checkboxes blueMode">
                 <label for="checkSettingsQUIRK_DILE_VT_DUMP_LOCATION_2">QUIRK_DILE_VT_DUMP_LOCATION_2<input type="checkbox" id="checkSettingsQUIRK_DILE_VT_DUMP_LOCATION_2"/></label>
               </div>
             </li>
           </ul>
         </div>
         <ul>
           <li><p>_______________________________________________</p></li>
         </ul><br/><br/>
         <div class="vtCaptureCheckbox checklabel blueMode">
           <ul>
             <li>
               <label><b>vtCapture quirks:</b></label>
             </li>
             <li>
               <div class="checkboxes blueMode">
                 <label for="checkSettingsQUIRK_VTCAPTURE_FORCE_CAPTURE">QUIRK_VTCAPTURE_FORCE_CAPTURE<input type="checkbox" id="checkSettingsQUIRK_VTCAPTURE_FORCE_CAPTURE"/></label>
               </div>
             </li>
           </ul><br/>
         </div>
         <ul>
           <li><p>_______________________________________________</p></li>
         </ul><br/><br/>
         <div class="GlobalCheckbox checklabel blueMode">
           <ul>
             <li>
               <div class="checkboxes blueMode">
                 <label for="checkSettingsNoHDR">Disable HyperHDR SDR/HDR switch<input type="checkbox" id="checkSettingsNoHDR"/></label>
               </div>
             </li>
             <li>
               <div class="checkboxes blueMode">
                 <label for="checkSettingsNoPowerstate">Disable powerstate check<input type="checkbox" id="checkSettingsNoPowerstate"/></label>
               </div>
             </li>
           </ul><br/>
         </div>
       </div>
        <div class="btns blueMode">
          <button onclick="serviceSaveSettings()" id="btnSettingsSave">Save</button>
          <button onclick="serviceResetSettings()" id="btnSettingsReset">Reset</button>
          <button onclick="toggleAdvanced()" id="btnSettingsAdvanced">Advanced</button>
        </div>
        <div class="btns blueMode">
          <button onclick="tvReboot()" id="btnSettingsReboot">Reboot</button>
        </div>
      </div>
      
      <div id="logs" class="logs blueMode">
        <div class="upperTab blueMode">
          <p>Some very simple experimental feature to collect logs. Setup logging is needed after a reboot.<br/>Will be reworked in newer versions. Press the load button to get last 200 log entries.</p>
          <div class="logsBtns btns blueMode" id="logsBtns">
            <button id="btnLogSwitchPicCap" onclick="switchLog('piccap')">PicCap</button>
            <button id="btnLogSwitchHyperion" onclick="switchLog('hyperion')">Hyperion-WebOS</button>
            <button id="btnLogStartStop" onclick="startStopLogging()">Setup logging</button>
            <button id="btnLogReload" onclick="reloadHyperionLog()">Load last 200</button>
            <button id="btnKillHyperion" onclick="restartHyperion()">Full service restart</button>
          </div>
        </div>
        <div class="consoleLog logBox blueMode" id="consoleLog">
          <textarea id="textareaConsoleLog">Logs from PicCap
          </textarea>
        </div>
        <div class="hyperionLog logBox blueMode" id="hyperionLog">
          <textarea id="textareaHyperionLog">Logs from hyperion-webos - Log gathering must be started and reloaded manually.
          </textarea>
        </div>
      </div>
      
      <div id="about" class="About blueMode">
        <div class="sub_title blueMode">
          <p>Some info about this project</p>
        </div><br/>
        <div class="aboutItems blueMode">
          <ul>
            <li>
              <p>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.<br/>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.<br/>So currently as a workaround you can play your media using your PC, FireTV-Stick or Chromecast and still enjoy your LEDs.<br/><u>This app requires to be run as root and tries to do this at the first start using the Homebrew Channel.</u></p>
            </li>
          </ul>
          <ul>
            <li><p>_______________________________________________</p></li>
          </ul><br/>
          <ul>
            <li>
              <p><br/>Feel free to raise an issue or pull request, or come to the OpenLG-Discord, if you have some questions.</p>
            </li> 
          </ul><br/>
          <ul>
            <li><p>GitHub-Links: <a href="https://github.com/webosbrew/hyperion-webos">github.com/webosbrew/hyperion-webos</a> | <a href="https://github.com/TBSniller/piccap">github.com/TBSniller/piccap</a></p></li>
          </ul><br/>
           <ul>
            <li><p>OpenLG-Discord: <a href="https://discord.gg/9sqAgHVRhP">discord.gg/9sqAgHVRhP</a></p></li>
          </ul><br/>
          <ul>
            <li><p>_______________________________________________</p></li>
          </ul><br/>
          <ul>
            <li><p><br/>Some love to everyone who was, or still is involved into this project and of course the OpenLG-/Hyperion-Community! ♥</p></li>
          </ul><br/><br/>
          <ul>
            <li>
              <p style="text-align: left">hyperion-webos</p>
              <div class="hyperionwebosContributors avatars blueMode"></div>
            </li>
            <li>
              <p style="text-align: left">PicCap</p>
              <div class="piccapContributors avatars blueMode"></div>
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="status blueMode">
      <div class="lightMode blueMode">
        <button id="btnLightBlue" onclick="switchLightMode('blue')"><img src="assets/icon_lightModeBlue.png"></img></button>
        <button id="btnLightDark" onclick="switchLightMode('dark')"><img src="assets/icon_lightModeDark.png"></img></button>
        <button id="btnLightBlack" onclick="switchLightMode('black')"><img src="assets/icon_lightModeBlack.png"></img></button>
      </div>
      <div class="info blueMode">
        <p>State: </p>
        <p id="txtInfoState">Loading..</p>
        
        <p> | Receiver: </p>
        <p id="txtInfoReceiver">n/a</p>
        
        <!--- UI --->
        <p> | UI: </p>
        <p id="txtInfoUI">n/a</p>
        
        <!--- Video --->
        <p> | Video: </p>
        <p id="txtInfoVideo">n/a</p>
        
        <p> | FPS: </p>
        <p id="txtInfoFPS">n/a</p>
      </div>
    </div>
  </body>
</html>


================================================
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<Node>} 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<Node>} - 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<Node>} - 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<Node>} - 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<Node>} 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 <HTML>
    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<Node>} - 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<Node>} - 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<Node>} - 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<Node>} 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<Node>}  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;r<arguments.length;r++){var t=null!=arguments[r]?arguments[r]:{};r%2?n(Object(t),!0).forEach((function(r){o(e,r,t[r])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(t)):n(Object(t)).forEach((function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(t,r))}))}return e}function o(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[r]=t,e}function u(e,r){for(var t=0;t<r.length;t++){var n=r[t];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}t.r(r),t.d(r,"APP",(function(){return s})),t.d(r,"connection",(function(){return V})),t.d(r,"DRM",(function(){return R})),t.d(r,"drmAgent",(function(){return C})),t.d(r,"launch",(function(){return l})),t.d(r,"launchParams",(function(){return d})),t.d(r,"LGUDID",(function(){return k}));var c={},a=function(){function e(){!function(e,r){if(!(e instanceof r))throw new TypeError("Cannot call a class as a function")}(this,e),this.bridge=null,this.cancelled=!1,this.subscribe=!1}var r,t,n;return r=e,(t=[{key:"send",value:function(e){var r=e.service,t=void 0===r?"":r,n=e.method,o=void 0===n?"":n,u=e.parameters,a=void 0===u?{}:u,s=e.onSuccess,l=void 0===s?function(){}:s,d=e.onFailure,f=void 0===d?function(){}:d,v=e.onComplete,p=void 0===v?function(){}:v,b=e.subscribe,m=void 0!==b&&b;if(!window.PalmServiceBridge){var h={errorCode:-1,errorText:"PalmServiceBridge is not found.",returnValue:!1};return f(h),p(h),console.error("PalmServiceBridge is not found."),this}this.ts&&c[this.ts]&&delete c[this.ts];var y,O=i({},a);return this.subscribe=m,this.subscribe&&(O.subscribe=this.subscribe),O.subscribe&&(this.subscribe=O.subscribe),this.ts=Date.now(),c[this.ts]=this,this.bridge=new PalmServiceBridge,this.bridge.onservicecallback=this.callback.bind(this,l,f,p),this.bridge.call(("/"!==(y=t).slice(-1)&&(y+="/"),y+o),JSON.stringify(O)),this}},{key:"callback",value:function(){var e=arguments.length>0&&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;r<arguments.length;r++){var t=null!=arguments[r]?arguments[r]:{};r%2?v(Object(t),!0).forEach((function(r){b(e,r,t[r])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(t)):v(Object(t)).forEach((function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(t,r))}))}return e}function b(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[r]=t,e}function m(e,r){for(var t=0;t<r.length;t++){var n=r[t];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}var h={NOT_ERROR:-1,CLIENT_NOT_LOADED:0,VENDOR_ERROR:500,API_NOT_SUPPORTED:501,WRONG_CLIENT_ID:502,KEY_NOT_FOUND:503,INVALID_PARAMS:504,UNSUPPORTED_DRM_TYPE:505,INVALID_KEY_FORMAT:506,INVALID_TIME_INFO:507,UNKNOWN_ERROR:599},y={PLAYREADY:"playready",WIDEVINE:"widevine"},O=0,g=1,w=2,P=3,S=function(e){var r=e.method,t=e.parameters,n=e.onComplete;(new a).send({service:"luna://com.webos.service.drm",onComplete:n,method:r,parameters:t})},T=function(){var e=arguments.length>0&&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<arguments.length;r++){var t=null!=arguments[r]?arguments[r]:{};r%2?N(Object(t),!0).forEach((function(r){_(e,r,t[r])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(t)):N(Object(t)).forEach((function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(t,r))}))}return e}({},e);if(delete r.returnValue,!0===e.returnValue)return delete r.subscribe,void n(r);delete r.returnValue,i(r)}})},V={getStatus:function(e){var r=e.onSuccess,t=void 0===r?function(){}:r,n=e.onFailure,i=void 0===n?function(){}:n,o=e.subscribe,u=void 0!==o&&o,c="webos.service";navigator.userAgent.indexOf("537.41")>-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;n<arguments.length;n++){var r=null!=arguments[n]?arguments[n]:{};n%2?c(Object(r),!0).forEach((function(n){d(e,n,r[n])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):c(Object(r)).forEach((function(n){Object.defineProperty(e,n,Object.getOwnPropertyDescriptor(r,n))}))}return e}function d(e,n,r){return n in e?Object.defineProperty(e,n,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[n]=r,e}function u(e,n){for(var r=0;r<n.length;r++){var t=n[r];t.enumerable=t.enumerable||!1,t.configurable=!0,"value"in t&&(t.writable=!0),Object.defineProperty(e,t.key,t)}}var f={},m=function(){function e(){!function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this,e),this.bridge=null,this.cancelled=!1,this.subscribe=!1}var n,r,t;return n=e,(r=[{key:"send",value:function(e){var n=e.service,r=void 0===n?"":n,t=e.method,o=void 0===t?"":t,i=e.parameters,s=void 0===i?{}:i,a=e.onSuccess,c=void 0===a?function(){}:a,d=e.onFailure,u=void 0===d?function(){}:d,m=e.onComplete,p=void 0===m?function(){}:m,v=e.subscribe,w=void 0!==v&&v;if(!window.PalmServiceBridge){var b={errorCode:-1,errorText:"PalmServiceBridge is not found.",returnValue:!1};return u(b),p(b),console.error("PalmServiceBridge is not found."),this}this.ts&&f[this.ts]&&delete f[this.ts];var h,y=l({},s);return this.subscribe=w,this.subscribe&&(y.subscribe=this.subscribe),y.subscribe&&(this.subscribe=y.subscribe),this.ts=Date.now(),f[this.ts]=this,this.bridge=new PalmServiceBridge,this.bridge.onservicecallback=this.callback.bind(this,c,u,p),this.bridge.call(("/"!==(h=r).slice(-1)&&(h+="/"),h+o),JSON.stringify(y)),this}},{key:"callback",value:function(){var e=arguments.length>0&&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<t.length;o+=1)try{j[t[o]]=parseInt(r[o],10)}catch(e){j[t[o]]=r[o]}}!n.returnValue||n.missingConfigs?n.missingConfigs&&1===n.missingConfigs.length&&"tv.model.supportTemp8K"===n.missingConfigs[0]?e(j):(new m).send({service:"luna://com.webos.service.tv.systemproperty",method:"getSystemInfo",parameters:{keys:["firmwareVersion","modelName","sdkVersion","UHD","OLED","ddrSize"]},onComplete:function(n){if(j.modelName=n.modelName||j.modelName,j.sdkVersion=n.sdkVersion||j.sdkVersion,j.uhd=n.UHD?"true"===n.UHD:j.uhd,j.oled=n.OLED?"true"===n.OLED:j.oled,j.ddrSize=n.ddrSize||j.ddrSize,n.firmwareVersion&&"0.0.0"!==n.firmwareVersion||(n.firmwareVersion=n.sdkVersion),n.firmwareVersion){j.version=n.firmwareVersion;for(var r=j.version.split("."),t=["versionMajor","versionMinor","versionDot"],o=0;o<t.length;o+=1)try{j[t[o]]=parseInt(r[o],10)}catch(e){j[t[o]]=r[o]}}e(j)}}):e(j)}}))}else e(j)},x={isShowing:function(){return PalmSystem&&PalmSystem.isKeyboardVisible}},k=function(){var e={};if(window.PalmSystem){if(window.PalmSystem.country){var n=JSON.parse(window.PalmSystem.country);e.country=n.country,e.smartServiceCountry=n.smartServiceCountry}window.PalmSystem.timeZone&&(e.timezone=window.PalmSystem.timeZone)}return e},D=r(0).version}]);

================================================
FILE: package.json
================================================
{
  "name": "org.webosbrew.piccap",
  "version": "0.5.2",
  "description": "PicCap - Hyperion Sender App",
  "main": "index.html",
  "scripts": {
    "lint-frontend": "eslint frontend/js/",
    "lint-frontend-fix": "eslint --fix frontend/js/",
    "lint-backend": "cd ./hyperion-webos && python lint/run-clang-format.py --extensions \"c,h,cpp\" --color auto --recursive ./src",
    "lint-backend-fix": "cd ./hyperion-webos && python lint/run-clang-format.py --extensions \"c,h,cpp\" --color auto --recursive --inplace true ./src",
    "clean": "rm -rf ./build/*; rm -rf ./hyperion-webos/build/*",
    "build-frontend": "mkdir -p ./build/piccap-frontend && webpack",
    "build-backend": "mkdir -p ./build/hyperion-webos && mkdir -p ./hyperion-webos/build && cd ./hyperion-webos/build && cmake -DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN_FILE .. && make hyperion-webos gm_backend halgal_backend dile_vt_backend vtcapture_backend && cd ../.. && cp ./hyperion-webos/build/hyperion-webos ./build/hyperion-webos/ && cp ./hyperion-webos/build/libdile_vt_backend.so ./build/hyperion-webos/ && cp ./hyperion-webos/build/libgm_backend.so ./build/hyperion-webos/ && cp ./hyperion-webos/build/libhalgal_backend.so ./build/hyperion-webos/ && cp ./hyperion-webos/build/libvtcapture_backend.so ./build/hyperion-webos/ && cp ./servicefiles/* ./build/hyperion-webos && chmod +x ./build/hyperion-webos/hyperion-webos ./build/hyperion-webos/piccapautostart",
    "build-all": "mkdir -p ./build/piccap-frontend && webpack && mkdir -p ./build/hyperion-webos && mkdir -p ./hyperion-webos/build && cd ./hyperion-webos/build && cmake -DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN_FILE .. && make && cd ../.. && cp ./hyperion-webos/build/hyperion-webos ./build/hyperion-webos/ && cp ./hyperion-webos/build/libdile_vt_backend.so ./build/hyperion-webos/ && cp ./hyperion-webos/build/libgm_backend.so ./build/hyperion-webos/ && cp ./hyperion-webos/build/libhalgal_backend.so ./build/hyperion-webos/ && cp ./hyperion-webos/build/libvtcapture_backend.so ./build/hyperion-webos/ && cp ./servicefiles/* ./build/hyperion-webos && chmod +x ./build/hyperion-webos/hyperion-webos ./build/hyperion-webos/piccapautostart",
    "package": "ares-package ./build/piccap-frontend ./build/hyperion-webos -o ./build",
    "deploy": "ares-install org.webosbrew.piccap_${npm_package_version}_all.ipk",
    "launch": "ares-launch org.webosbrew.piccap"
  },
  "browserslist": [
    "chrome 38"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@babel/preset-env": "^7.18.10",
    "bluebird": "^3.7.2",
    "core-js": "^3.24.1",
    "regenerator-runtime": "^0.13.9",
    "webos-service": "git+https://github.com/webosose/nodejs-module-webos-service.git"
  },
  "devDependencies": {
    "@babel/cli": "^7.18.10",
    "@babel/core": "^7.18.10",
    "@babel/runtime-corejs3": "^7.18.9",
    "@webosose/ares-cli": "^2.3.1",
    "babel-loader": "^8.2.5",
    "copy-webpack-plugin": "^10.2.4",
    "eslint": "^8.22.0",
    "eslint-config-airbnb-base": "^15.0.0",
    "eslint-plugin-compat": "^4.0.2",
    "eslint-plugin-import": "^2.26.0",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0"
  }
}


================================================
FILE: servicefiles/piccapautostart
================================================
#!/bin/bash
luna-send -n 1 -f luna://org.webosbrew.piccap.service/isRunning '{}' &
exit 0


================================================
FILE: servicefiles/services.json
================================================
{
  "id" : "org.webosbrew.piccap.service",
  "description":"Native PicCap hyperion-webos service",
  "engine":"native",
  "executable":"hyperion-webos",
  "services": [{
    "name": "org.webosbrew.piccap.service",
    "description": "Native PicCap hyperion-webos service"
  }]
}

================================================
FILE: servicefiles/setuplegacylogging.sh
================================================
#!/bin/bash
luna-send -n 1 -f luna://com.webos.pmlogd/setdevlogstatus '{"recordDevLogs":true}'
luna-send -n 1 -f luna://com.webos.service.config/setConfigs '{"configs": {"system.collectDevLogs": true}}'
PmLogCtl set hyperion-webos debug

================================================
FILE: webpack.config.js
================================================
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  entry: {
    ui: path.resolve(__dirname, './frontend/js/ui.js'),
    servicecalls: path.resolve(__dirname, './frontend/js/servicecalls.js'),
    spatialnavigation: path.resolve(__dirname, './frontend/js/spatial-navigation.js'),
  },
  target: ['web', 'es5'],
  mode: 'production',
  devtool: false,
  module: {
    rules: [
      {
        test: /\.js$/,
        // exclude: /node_modules/,
        exclude: /core-js/,
        use: ['babel-loader'],
      },
    ],
  },
  resolve: {
    extensions: ['.js'],
  },
  output: {
    path: path.resolve(__dirname, './build/piccap-frontend/'),
    filename: './[name].bundled.js',
  },
  performance: {
    maxAssetSize: 1000000
  },
  plugins: [
    new CopyPlugin({
      patterns: [{ context: 'frontend', from: '**', to: '.' }],
    }),
  ],
};
Download .txt
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
Download .txt
SYMBOL INDEX (96 symbols across 6 files)

FILE: frontend/js/domrect-polyfill.js
  function number (line 7) | function number(v) {
  function different (line 11) | function different(u, v) {
  function DOMRect (line 15) | function DOMRect(xArg, yArg, wArg, hArg) {

FILE: frontend/js/servicecalls.js
  function logIt (line 15) | function logIt(message) {
  function onHBExec (line 21) | function onHBExec(result) {
  function killHyperion (line 29) | function killHyperion() {
  function makeServiceRoot (line 47) | function makeServiceRoot() {
  function onCheckRootStatus (line 75) | function onCheckRootStatus(result) {
  function checkRoot (line 98) | function checkRoot() {
  function getStatus (line 127) | function getStatus() {
  function getSettings (line 160) | function getSettings() {
  function saveSettings (line 255) | function saveSettings(config) {
  method onSuccess (line 412) | onSuccess(result) {
  function onServiceCallback (line 453) | function onServiceCallback(result) {

FILE: frontend/js/spatial-navigation.js
  function initiateSpatialNavigation (line 39) | function initiateSpatialNavigation() {
  function spatialNavigationHandler (line 87) | function spatialNavigationHandler() {
  function navigate (line 165) | function navigate(dir) {
  function focusingController (line 256) | function focusingController(bestCandidate, dir) {
  function scrollingController (line 289) | function scrollingController(container, dir) {
  function getSpatialNavigationCandidates (line 315) | function getSpatialNavigationCandidates(container, option = { mode: 'vis...
  function getFilteredSpatialNavigationCandidates (line 350) | function getFilteredSpatialNavigationCandidates(element, dir, candidates...
  function spatialNavigationSearch (line 372) | function spatialNavigationSearch(dir, args) {
  function filteredCandidates (line 458) | function filteredCandidates(currentElm, candidates, dir, container) {
  function selectBestCandidate (line 504) | function selectBestCandidate(currentElm, candidates, dir) {
  function selectBestCandidateFromEdge (line 535) | function selectBestCandidateFromEdge(currentElm, candidates, dir) {
  function getClosestElement (line 549) | function getClosestElement(currentElm, candidates, dir, distanceFunction) {
  function getSpatialNavigationContainer (line 590) | function getSpatialNavigationContainer() {
  function getScrollContainer (line 614) | function getScrollContainer(element) {
  function focusableAreas (line 645) | function focusableAreas(option = { mode: 'visible' }) {
  function createSpatNavEvents (line 659) | function createSpatNavEvents(eventType, containerElement, currentElement...
  function readCssVar (line 677) | function readCssVar(element, varName) {
  function isCSSSpatNavContain (line 687) | function isCSSSpatNavContain(element) {
  function getCSSSpatNavAction (line 697) | function getCSSSpatNavAction(element) {
  function navigateChain (line 710) | function navigateChain(eventTarget, container, parentContainer, dir, opt...
  function findSearchOrigin (line 758) | function findSearchOrigin() {
  function moveScroll (line 796) | function moveScroll(element, dir, offset = 0) {
  function isContainer (line 813) | function isContainer(element) {
  function isDelegableContainer (line 827) | function isDelegableContainer(element) {
  function isScrollContainer (line 838) | function isScrollContainer(element) {
  function isScrollable (line 856) | function isScrollable(element, dir) { // element, dir
  function isOverflow (line 890) | function isOverflow(element, dir) {
  function isHTMLScrollBoundary (line 917) | function isHTMLScrollBoundary(element, dir) {
  function isScrollBoundary (line 943) | function isScrollBoundary(element, dir) {
  function isVisibleInScroller (line 968) | function isVisibleInScroller(element) {
  function isFocusable (line 998) | function isFocusable(element) {
  function isAtagWithoutHref (line 1010) | function isAtagWithoutHref(element) {
  function isActuallyDisabled (line 1023) | function isActuallyDisabled(element) {
  function isExpresslyInert (line 1035) | function isExpresslyInert(element) {
  function isBeingRendered (line 1051) | function isBeingRendered(element) {
  function isVisible (line 1064) | function isVisible(element) {
  function isEntirelyVisible (line 1074) | function isEntirelyVisible(element, container) {
  function isVisibleStyleProperty (line 1094) | function isVisibleStyleProperty(element) {
  function hitTest (line 1109) | function hitTest(element) {
  function isInside (line 1143) | function isInside(containerRect, childRect) {
  function isOutside (line 1160) | function isOutside(rect1, rect2, dir) {
  function isRightSide (line 1176) | function isRightSide(rect1, rect2) {
  function isBelow (line 1181) | function isBelow(rect1, rect2) {
  function isAligned (line 1186) | function isAligned(rect1, rect2, dir) {
  function getDistanceFromPoint (line 1210) | function getDistanceFromPoint(point, element, dir) {
  function getInnerDistance (line 1233) | function getInnerDistance(rect1, rect2, dir) {
  function getDistance (line 1251) | function getDistance(searchOrigin, candidateRect, dir) {
  function getEuclideanDistance (line 1321) | function getEuclideanDistance(rect1, rect2, dir) {
  function getAbsoluteDistance (line 1342) | function getAbsoluteDistance(rect1, rect2, dir) {
  function getEntryAndExitPoints (line 1359) | function getEntryAndExitPoints(dir = 'down', searchOrigin, candidateRect) {
  function getIntersectionRect (line 1478) | function getIntersectionRect(rect1, rect2) {
  function handlingEditableElement (line 1502) | function handlingEditableElement(e) {
  function getBoundingClientRect (line 1544) | function getBoundingClientRect(element) {
  function getOverlappedCandidates (line 1567) | function getOverlappedCandidates(targetElement) {
  function getExperimentalAPI (line 1585) | function getExperimentalAPI() {
  function enableExperimentalAPIs (line 1715) | function enableExperimentalAPIs(option) {
  function getInitialAPIs (line 1726) | function getInitialAPIs() {

FILE: frontend/js/ui.js
  function logIt (line 3) | function logIt(message) {
  function saveLightMode (line 172) | function saveLightMode(color) {
  function loadLightMode (line 229) | function loadLightMode() {
  function getJSON (line 236) | function getJSON(url, callback) {
  function getContributors (line 255) | function getContributors(owner, repo) {

FILE: frontend/webOSTVjs-1.2.4/webOSTV-dev.js
  function t (line 1) | function t(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{...
  function n (line 1) | function n(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){va...
  function i (line 1) | function i(e){for(var r=1;r<arguments.length;r++){var t=null!=arguments[...
  function o (line 1) | function o(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enume...
  function u (line 1) | function u(e,r){for(var t=0;t<r.length;t++){var n=r[t];n.enumerable=n.en...
  function e (line 1) | function e(){!function(e,r){if(!(e instanceof r))throw new TypeError("Ca...
  function v (line 1) | function v(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){va...
  function p (line 1) | function p(e){for(var r=1;r<arguments.length;r++){var t=null!=arguments[...
  function b (line 1) | function b(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enume...
  function m (line 1) | function m(e,r){for(var t=0;t<r.length;t++){var n=r[t];n.enumerable=n.en...
  function e (line 1) | function e(r){!function(e,r){if(!(e instanceof r))throw new TypeError("C...
  function N (line 1) | function N(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){va...
  function _ (line 1) | function _(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enume...

FILE: frontend/webOSTVjs-1.2.4/webOSTV.js
  function r (line 1) | function r(t){if(n[t])return n[t].exports;var o=n[t]={i:t,l:!1,exports:{...
  function c (line 1) | function c(e,n){var r=Object.keys(e);if(Object.getOwnPropertySymbols){va...
  function l (line 1) | function l(e){for(var n=1;n<arguments.length;n++){var r=null!=arguments[...
  function d (line 1) | function d(e,n,r){return n in e?Object.defineProperty(e,n,{value:r,enume...
  function u (line 1) | function u(e,n){for(var r=0;r<n.length;r++){var t=n[r];t.enumerable=t.en...
  function e (line 1) | function e(){!function(e,n){if(!(e instanceof n))throw new TypeError("Ca...
  function v (line 1) | function v(e){return(v="function"==typeof Symbol&&"symbol"==typeof Symbo...
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (200K chars).
[
  {
    "path": ".eslintignore",
    "chars": 141,
    "preview": "node_modules/\nhyperion-webos/\nbuild/\ndist/\nservicenative/\n.vscode/\n.github/\nfrontend/js/spatial-navigation.js\nfrontend/j"
  },
  {
    "path": ".eslintrc.js",
    "chars": 337,
    "preview": "module.exports = {\n  env: {\n    browser: true,\n  },\n  extends: [\n    'airbnb-base',\n    'plugin:compat/recommended'\n  ],"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 198,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: \"Community: OpenLGTV Discord\"\n    url: https://discord.gg/9sqAgHVRh"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/piccap_bug.yml",
    "chars": 2890,
    "preview": "name: \"PicCap bug report\"\ndescription: \"PicCap UI does not work as intendend\"\nlabels: [\"bug\"]\nbody:\n  - type: checkboxes"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/piccap_documentation.yml",
    "chars": 1644,
    "preview": "name: \"PicCap documentation change\"\ndescription: \"We need to change or add some missing information in our documentation"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/piccap_feature.yml",
    "chars": 1601,
    "preview": "name: \"PicCap feature request\"\ndescription: \"This new UI feature does not exists and would be nice to have\"\nlabels: [\"fe"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/piccap_question.yml",
    "chars": 1450,
    "preview": "name: \"PicCap question\"\ndescription: \"I can't find an answer to my question\"\nlabels: [\"question\"]\nbody:\n  - type: checkb"
  },
  {
    "path": ".github/workflows/build_piccap.yml",
    "chars": 1919,
    "preview": "name: Build PicCap with hyperion-webos and create IPK\n\non:\n  push:\n  pull_request:\n  workflow_dispatch:\n\nenv:\n  TOOLCHAI"
  },
  {
    "path": ".gitignore",
    "chars": 51,
    "preview": "build\n.vscode/*\n!.vscode/settings.json\nnode_modules"
  },
  {
    "path": ".gitmodules",
    "chars": 103,
    "preview": "[submodule \"hyperion-webos\"]\n\tpath = hyperion-webos\n\turl = https://github.com/webosbrew/hyperion-webos\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 139,
    "preview": "{\n    \"cmake.sourceDirectory\": \"${workspaceFolder}/hyperion-webos\",\n    \"cmake.buildDirectory\": \"${workspaceFolder}/hype"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2021 webOS Brew\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 7044,
    "preview": "# PicCap - Hyperion Sender App | Ambilight for LG WebOS TVs \n\n  \n## What's this?\n\n### PicCap?    \nPicCap is an frontend "
  },
  {
    "path": "babel.config.json",
    "chars": 261,
    "preview": "{\n    \"presets\": [\n        [\n            \"@babel/preset-env\",\n            {\n                \"targets\": {\n               "
  },
  {
    "path": "docs/DIY_Ambilight.md",
    "chars": 4714,
    "preview": "## Requirements (Maybe differs for your country)\n\n- Raspberrry Pi 3 or 4 + Stuff (MicroSD, Power, maybe a Case,..)\n- 5V "
  },
  {
    "path": "frontend/appinfo.json",
    "chars": 418,
    "preview": "{\n  \"id\": \"org.webosbrew.piccap\",\n  \"version\": \"0.5.2\",\n  \"vendor\": \"Homebrew\",\n  \"type\": \"web\",\n  \"main\": \"index.html\","
  },
  {
    "path": "frontend/css/basicui.css",
    "chars": 4190,
    "preview": "*{\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n  font-family: 'Poppins',sans-serif;\n}\n\n::selection{\n  backgroun"
  },
  {
    "path": "frontend/css/blackui.css",
    "chars": 2600,
    "preview": ".menu.blackMode {\n  background: black;\n  border-bottom: 2px solid grey;\n}\n\n.menu .logo.blackMode p{\n  color: #fff;\n}\n\n.m"
  },
  {
    "path": "frontend/css/blueui.css",
    "chars": 2373,
    "preview": ".menu.blueMode {\n  background: #0d47a1;\n}\n\n.menu .logo.blueMode p{\n  color: #fff;\n}\n.menu.blueMode ul li button{\n  color"
  },
  {
    "path": "frontend/css/darkui.css",
    "chars": 2373,
    "preview": ".menu.darkMode {\n  background: #303030;\n}\n\n.menu .logo.darkMode p{\n  color: #fff;\n}\n.menu.darkMode ul li button{\n  color"
  },
  {
    "path": "frontend/index.html",
    "chars": 14132,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\" dir=\"ltr\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width="
  },
  {
    "path": "frontend/js/domrect-polyfill.js",
    "chars": 2080,
    "preview": "//\n// https://raw.githubusercontent.com/Financial-Times/polyfill-library/c25c30e4463bef60fba1213ecb697f3e3f253d7b/polyfi"
  },
  {
    "path": "frontend/js/servicecalls.js",
    "chars": 18885,
    "preview": "require('core-js/stable');\n\nconst availableQuirks = {\n  // DILE_VT\n  QUIRK_DILE_VT_CREATE_EX: '0x1',\n  QUIRK_DILE_VT_NO_"
  },
  {
    "path": "frontend/js/spatial-navigation.js",
    "chars": 73107,
    "preview": "//\n// https://raw.githubusercontent.com/WICG/spatial-navigation/183f0146b6741007e46fa64ab0950447defdf8af/polyfill/spatia"
  },
  {
    "path": "frontend/js/ui.js",
    "chars": 10476,
    "preview": "import packageJSON from '../../package.json';\n\nfunction logIt(message) {\n  const textareaConsoleLog = document.getElemen"
  },
  {
    "path": "frontend/webOSTVjs-1.2.4/LICENSE-2.0.txt",
    "chars": 11325,
    "preview": "Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licens"
  },
  {
    "path": "frontend/webOSTVjs-1.2.4/webOSTV-dev.js",
    "chars": 11360,
    "preview": "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"
  },
  {
    "path": "frontend/webOSTVjs-1.2.4/webOSTV.js",
    "chars": 10631,
    "preview": "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]"
  },
  {
    "path": "package.json",
    "chars": 3141,
    "preview": "{\n  \"name\": \"org.webosbrew.piccap\",\n  \"version\": \"0.5.2\",\n  \"description\": \"PicCap - Hyperion Sender App\",\n  \"main\": \"in"
  },
  {
    "path": "servicefiles/piccapautostart",
    "chars": 90,
    "preview": "#!/bin/bash\nluna-send -n 1 -f luna://org.webosbrew.piccap.service/isRunning '{}' &\nexit 0\n"
  },
  {
    "path": "servicefiles/services.json",
    "chars": 278,
    "preview": "{\n  \"id\" : \"org.webosbrew.piccap.service\",\n  \"description\":\"Native PicCap hyperion-webos service\",\n  \"engine\":\"native\",\n"
  },
  {
    "path": "servicefiles/setuplegacylogging.sh",
    "chars": 236,
    "preview": "#!/bin/bash\nluna-send -n 1 -f luna://com.webos.pmlogd/setdevlogstatus '{\"recordDevLogs\":true}'\nluna-send -n 1 -f luna://"
  },
  {
    "path": "webpack.config.js",
    "chars": 895,
    "preview": "const path = require('path');\nconst CopyPlugin = require('copy-webpack-plugin');\n\nmodule.exports = {\n  entry: {\n    ui: "
  }
]

About this extraction

This page contains the full source code of the TBSniller/piccap GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (187.6 KB), approximately 48.6k tokens, and a symbol index with 96 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!