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





================================================
FILE: babel.config.json
================================================
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "38"
},
"useBuiltIns": "entry",
"corejs": "3"
}
]
]
}
================================================
FILE: docs/DIY_Ambilight.md
================================================
## Requirements (Maybe differs for your country)
- Raspberrry Pi 3 or 4 + Stuff (MicroSD, Power, maybe a Case,..)
- 5V 8A Power Source (I am using this: https://www.amazon.de/borui-uk-Netzteil-MeanWell-LPV-60-5-Schaltnetzteil/dp/B017U7JGGY)
- Old Powercord
- WS2801 LED Stripes (I am using this: https://www.amazon.de/gp/product/B072B6561S/ You can get them cheaper on AliExpress + Measure up your TV for real cm needed)
- Jumping Wires (Like: https://www.amazon.de/gp/product/B00OK74ABO)
- Few Wagos (Like: https://www.amazon.de/Wago-Verbindungsklemme-221-413-50-St%C3%BCck/dp/B00JB3U9CG)
- Soldering iron and solder
- Datacable (I recommend these, if you have a long way from TV to Pi, because they are shieded: https://www.amazon.de/Busleitung-Datenleitung-Datenkabel-Installationsbusleitung-Telekommunikationskabel/dp/B071W5TQPY/ otherwise use the next one cables)
- Power cables (LED | Like: https://www.amazon.de/WITTKOWARE-Sortiment-Schaltdr%C3%A4hte-5mm-10m/dp/B075V79HGF/)
## Hardware
## TV preparing
1. Measure up each side of your TV and cut the stripe at the edges.
2. There are arrows under the sticky tape. Make sure all of them are looking in the same direction: -> -> not -><-. Data and clock cannot flow if they do not go in one direction.
3. Tape them on your TV. You have two options. Begin your input stripe in one of the corners, or let it begin anywhere. You can set it later in the settings of hyperion.
4. Use your soldering iron and powercables to connect the corners. You also have to connect (!!ONLY!!) GND and V+ with the end and the beginning of your stripe, to prevent powerloss over a long ledstripe.
You can also use some edge connectors (like: https://www.amazon.de/dp/B08C2M18XF), but in this case they are a way to small and you have to cut them a bit (Trust me you would not do this.. took me 4 hours to get this sh\*\* working).
5. Get your datacable or another powercables ready. Connect them to CLK(CK) and SI(DI) on your input stripe. They have to be as long, as your way to your Pi is. After that you can use the jumpercables to solder one end with the ledstripe cable and use the other end as GPIO connector. I got flickering issues (caused by the powernet) so I decided to wiggle the data cables with aluminum foil. Flickering disappeard. You can avoid this if you buy the datacable right at first.
#### Power source preparing
--MAKE SURE YOU KNOW WHAT YOU ARE DOING! IF NOT ASK AN ELECTRICAN! THIS EXAMPLE IS FOR GERMANY!! --
1. Get your power source, the Wagos, a cutted old powercode and connect it together:
2. Get your power source, the Wagos, the powercables and connect them together:
#### Led+Pi connection
1. Connect GND(Black) and V+(Red) with your LED-Stripe
2. Connect GND(Black) also with a GND Pin of the Pi (eg. Pin 9 | **Pi 3/4**) You can find a plan here: https://www.elektronik-kompendium.de/sites/raspberry-pi/fotos/raspberry-pi-15b.jpg (Pi 3 and 4 are the same. **Pi 2 is different!**)
3. Connect CLK(CK) of your input stripe with Pin 23
4. Connect SI(DI) of your input stripe with Pin 19
Here is a very nice picture, how to look after all: https://digitalewelt.at/wp-content/uploads/2018/02/Ambilight-Projekt-Verkabelung-LED-Netzteil.jpg
## Software
- Add **dtparam=spi=on** to the end of your /boot/config.txt and reboot
- Installation and usage guide: https://docs.hyperion-project.org/en/user/Installation.html#requirements
- After installation you have to setup the WS2801s. Therefore in Hyperion go to Configuration -> Led-Hardware -> Controller type: **WS2801**, SPI path: **/dev/spidev0.0**, , Baudrate: **1000000**. Change RGB byte order if you experience wrong colors. Mine are on **RGB**
- Count your LEDs and pass them to the LED-Layout tab.
## Credits
Build it myself using this guide: https://digitalewelt.at/ambilight-mit-dem-raspberry-pi-3-4k-schritt-fuer-schritt-anleitung/
================================================
FILE: frontend/appinfo.json
================================================
{
"id": "org.webosbrew.piccap",
"version": "0.5.2",
"vendor": "Homebrew",
"type": "web",
"main": "index.html",
"title": "PicCap",
"appDescription": "Hyperion Sender App - Ambilight for WebOS",
"icon": "assets/logo_small.png",
"largeIcon": "assets/logo_big.png",
"splashBackground": "assets/splash_1080p.png",
"bgImage": "assets/splash_1080p.png",
"iconColor": "#000000",
"supportGIP": true
}
================================================
FILE: frontend/css/basicui.css
================================================
*{
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Poppins',sans-serif;
}
::selection{
background: #fff;
color: #000;
}
.menu .logo p{
display: inline;
}
nav{
position: fixed;
width: 100%;
z-index: 12;
}
.menu{
margin: auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 100px 10px;
transition: all 0.3s ease;
}
.menu .logo p{
text-decoration: none;
font-size: 35px;
font-weight: 600;
}
.menu ul{
display: inline-flex;
}
.menu ul li{
list-style: none;
margin-left: 7px;
}
.menu ul li:first-child{
margin-left: 0;
}
.menu ul li button{
text-decoration: none;
font-size: 18px;
font-weight: 500;
padding: 8px 15px;
transition: all 0.3s ease;
}
.background{
position: absolute;
height: 100%;
width: 100%;
transition: all 0.3s ease;
}
.main{
position: absolute;
top: 52%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
padding: 0 20px;
text-align: center;
}
.main .title{
font-size: 55px;
font-weight: 600;
}
.main .title p{
display: inline;
}
.main .sub_title{
font-size: 32px;
font-weight: 600;
}
.main .btns{
margin-top: 20px;
}
.main .btns button{
height: 55px;
width: 170px;
margin: 0 10px;
font-size: 20px;
font-weight: 500;
padding: 0 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.service {
display: block;
}
.settings {
display: none;
}
.logs {
display: none;
}
.about {
display: none;
}
.status {
position: fixed;
bottom: 0;
width: 100%;
padding: 10px 10px;
transition: all 0.3s ease;
}
.status .lightMode{
display: inline-table;
}
.status .info {
display: inline-table;
float: right;
margin-top: 4px;
}
.status .info p {
text-decoration: none;
display: inline;
text-align: right;
font-size: 17px;
}
.lightMode button {
display: inline-block;
padding: 1px 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
margin: 0 1px;
transition: all 0.3s ease;
}
.lightMode img {
width: 20px;
height: 20px;
margin: 0px 0px -3px 0px;
}
#txtPicCapVersion {
color: #fff;
font-size: 11px;
font-weight: 600;
}
#txtServiceVersion {
color: #fff;
font-size: 11px;
font-weight: 600;
}
.settings {
margin-left: 15%;
margin-right: 15%;
}
.advSettingitems {
display: none;
}
.settingItemsAdv {
display: none;
}
.settingItemsNormal {
display: block;
}
.settingItems {
text-align: center;
}
.settingItems p{
font-size: 20px;
font-weight: 500;
margin-bottom: 2px;
}
.settingItems ul{
display: inline-flex;
margin-bottom: 20px;
}
.settingItems ul li{
text-align: left;
list-style: none;
margin-left: 10px;
margin-right: 10px;
}
.settingItems input[type="text"]{
border-style: none;
border-radius: 5px;
height: 30px;
min-width: 150px;
padding: 0 20px;
font-size: 15px;
}
.checkboxes{
text-align: right;
margin-left: 20px;
}
.checklabel label{
font-size: 19px;
font-weight: 500;
}
.settingItems input[type="checkbox"]{
height: 25px;
width: 25px;
padding: 0 20px;
vertical-align: middle;
margin-left: 7px;
margin-bottom: 4px;
}
.settingItems select{
height: 30px;
min-width: 230px;
padding: 0 20px;
font-size: 15px;
}
.manualres {
display: none;
}
.settingsocket {
display: none
}
.manualsocket {
display: none
}
.settingaddressport {
display: flex
}
.avatars img{
width: 30px;
height: 30px;
border-radius: 100%;
margin-right: 10px;
}
.avatars p{
display: flex;
align-items:center;
}
.aboutItems {
text-align: center;
}
.aboutItems p{
font-size: 20px;
font-weight: 500;
margin-bottom: 15px;
text-align: center;
}
.aboutItems ul{
display: inline-flex;
margin-right: 20px;
}
.aboutItems ul li{
text-align: left;
list-style: none;
margin-right: 10px;
}
.upperTab p{
font-size: 20px;
font-weight: 500;
color: #fff;
margin-bottom: 15px;
text-align: center;
}
.logsBtns {
display: block;
}
.upperTab .btns button{
width: 200px;
margin-bottom: 10px;
}
.logBox textarea{
resize: none;
width: 95%;
height: 500px;
}
.consoleLog {
display: block;
height: 95%;
}
.hyperionLog {
display: none;
}
================================================
FILE: frontend/css/blackui.css
================================================
.menu.blackMode {
background: black;
border-bottom: 2px solid grey;
}
.menu .logo.blackMode p{
color: #fff;
}
.menu.blackMode ul li button{
color: #fff;
border-radius: 5px;
border-style: none;
background: none;
}
.menu.blackMode ul li button:hover{
background: #fff;
color: black;
}
.background.blackMode{
background: black;
}
.main .title.blackMode{
color: #fff;
}
.main .sub_title.blackMode{
color: #fff;
}
.main .btns.blackMode button{
border-radius: 5px;
border: 2px solid white;
outline: none;
background: none;
color: #fff;
}
.btns.blackMode button:hover{
background: white;
color: black;
}
.status.blackMode {
background: black;
border-top: 2px solid grey;
}
.status .info.blackMode p {
color: #fff;
}
.lightMode.blackMode button {
outline: none;
border: 2px solid white;
border-radius: 5px;
background-color: #0d0d0d;
}
.lightMode.blackMode button:hover{
background-color: white;
color: black;
}
.lightMode.blackMode button:focus {
background-color: white;
color: black;
border: 2px solid black;
outline: 1px solid white;
}
.menu.blackMode :focus {
border-style: solid;
border-radius: 3px;
border: 3px solid black;
outline: 1px solid white;
}
.main.blackMode button:focus {
background: white;
color: black;
border: 2px solid black;
outline: 1px solid white;
}
.main.blackMode button:active {
background: lightgray;
}
.settingItems.blackMode p{
color: #fff;
}
.checklabel.blackMode label{
color: #fff;
}
.settingItems.blackMode input[type="checkbox"]{
border-style: none;
border-radius: 5px;
}
.settingItems.blackMode select{
border-style: none;
border-radius: 5px;
background-color: white;
}
.settingItems.blackMode :focus{
border-style: solid;
border: 2px solid black;
outline: 1px solid white;
}
.settingItems.blackMode button:active{
background: lightgray;
}
.settingItems.blackMode input:focus{
border-style: solid;
border-radius: 2px;
border: 2px solid black;
outline: 1px solid white;
}
.checkboxes.blackMode input[type="checkbox"]:focus{
outline: 2px solid black;
outline: 1px solid white;
}
.aboutItems.blackMode p{
color: #fff;
}
.aboutItems.blackMode a{
color: white;
}
.aboutItems.blackMode :focus {
border-style: solid;
border-radius: 3px;
border: 3px solid white;
}
.upperTab.blackMode p{
color: #fff;
}
.logBox.blackMode textarea{
color: white;
background-color: black;
border: 1px solid white;
}
.logBox.blackMode textarea:focus{
background-color: black;
border: 2px solid white;
outline: 1px solid white;
}
================================================
FILE: frontend/css/blueui.css
================================================
.menu.blueMode {
background: #0d47a1;
}
.menu .logo.blueMode p{
color: #fff;
}
.menu.blueMode ul li button{
color: #fff;
border-radius: 5px;
border-style: none;
background: none;
}
.menu.blueMode ul li button:hover{
background: #fff;
color: black;
}
.background.blueMode{
background: #1e88e5;
}
.main .title.blueMode{
color: #fff;
}
.main .sub_title.blueMode{
color: #fff;
}
.main .btns.blueMode button{
border-radius: 5px;
border: 2px solid white;
outline: none;
background: none;
color: #fff;
}
.btns.blueMode button:hover{
background: white;
color: black;
}
.status.blueMode {
background: #1565c0;
}
.status .info.blueMode p {
color: #fff;
}
.lightMode.blueMode button {
outline: none;
border: 2px solid white;
border-radius: 5px;
background-color: transparent;
}
.lightMode.blueMode button:hover{
background-color: white;
color: black;
}
.lightMode.blueMode button:focus {
background-color: white;
color: black;
border: 2px solid black;
}
.menu.blueMode :focus {
border-style: solid;
border-radius: 3px;
border: 3px solid black;
}
.main.blueMode button:focus {
background: white;
color: black;
border: 2px solid black;
}
.main.blueMode button:active {
background: lightgray;
}
.settingItems.blueMode p{
color: #fff;
}
.checklabel.blueMode label{
color: #fff;
}
.settingItems.blueMode input[type="checkbox"]{
border-style: none;
border-radius: 5px;
}
.settingItems.blueMode select{
border-style: none;
border-radius: 5px;
background-color: white;
}
.settingItems.blueMode :focus{
border-style: solid;
border-radius: 2px;
border: 2px solid black;
}
.settingItems.blueMode button:active{
background: lightgray;
}
.settingItems.blueMode input:focus{
border-style: solid;
border-radius: 2px;
border: 2px solid black;
}
.checkboxes.blueMode input[type="checkbox"]:focus{
outline: 2px solid black;
}
.aboutItems.blueMode p{
color: #fff;
}
.aboutItems.blueMode a{
color: white;
}
.aboutItems.blueMode :focus {
border-style: solid;
border-radius: 3px;
border: 3px solid white;
}
.upperTab.blueMode p{
color: #fff;
}
.logBox.blueMode textarea{
color: white;
background-color: #1e88e5;
border: 1px solid white;
}
.logBox.blueMode textarea:focus{
background-color: #1e88e5;
border: 2px solid white;
outline: 1px solid white;
}
================================================
FILE: frontend/css/darkui.css
================================================
.menu.darkMode {
background: #303030;
}
.menu .logo.darkMode p{
color: #fff;
}
.menu.darkMode ul li button{
color: #fff;
border-radius: 5px;
border-style: none;
background: none;
}
.menu.darkMode ul li button:hover{
background: #fff;
color: black;
}
.background.darkMode{
background: #424242;
}
.main .title.darkMode{
color: #fff;
}
.main .sub_title.darkMode{
color: #fff;
}
.main .btns.darkMode button{
border-radius: 5px;
border: 2px solid white;
outline: none;
background: none;
color: #fff;
}
.btns.darkMode button:hover{
background: white;
color: black;
}
.status.darkMode {
background: #212121;
}
.status .info.darkMode p {
color: #fff;
}
.lightMode.darkMode button {
outline: none;
border: 2px solid white;
border-radius: 5px;
background-color: transparent;
}
.lightMode.darkMode button:hover{
background-color: white;
color: black;
}
.lightMode.darkMode button:focus {
background-color: white;
color: black;
border: 2px solid black;
}
.menu.darkMode :focus {
border-style: solid;
border-radius: 3px;
border: 3px solid black;
}
.main.darkMode button:focus {
background: white;
color: black;
border: 2px solid black;
}
.main.darkMode button:active {
background: lightgray;
}
.settingItems.darkMode p{
color: #fff;
}
.checklabel.darkMode label{
color: #fff;
}
.settingItems.darkMode input[type="checkbox"]{
border-style: none;
border-radius: 5px;
}
.settingItems.darkMode select{
border-style: none;
border-radius: 5px;
background-color: white;
}
.settingItems.darkMode :focus{
border-style: solid;
border-radius: 2px;
border: 2px solid black;
}
.settingItems.darkMode button:active{
background: lightgray;
}
.settingItems.darkMode input:focus{
border-style: solid;
border-radius: 2px;
border: 2px solid black;
}
.checkboxes.darkMode input[type="checkbox"]:focus{
outline: 2px solid black;
}
.aboutItems.darkMode p{
color: #fff;
}
.aboutItems.darkMode a{
color: white;
}
.aboutItems.darkMode :focus {
border-style: solid;
border-radius: 3px;
border: 3px solid white;
}
.upperTab.darkMode p{
color: #fff;
}
.logBox.darkMode textarea{
color: white;
background-color: #424242;
border: 1px solid white;
}
.logBox.darkMode textarea:focus{
background-color: #424242;
border: 2px solid white;
outline: 1px solid white;
}
================================================
FILE: frontend/index.html
================================================
PicCap - Hyperion Sender App
hyperion-webos
loading..
Loading status..
Service settings
Socket
Socket path
Address
Port
Hyperion priority
Resolution
Width
Height
Maximal FPS
_______________________________________________
Video capture backend
Graphical capture backend
Service advanced settings
_______________________________________________
_______________________________________________
Some very simple experimental feature to collect logs. Setup logging is needed after a reboot. Will be reworked in newer versions. Press the load button to get last 200 log entries.
Some info about this project
PicCap is the frontend app, which you have installed on your TV and you can see here, to make things as easy as possible. It ships and controls the seperated hyperion-webos background service, which controls the capture interfaces on your TV based on reverse engineering, proccesses the output and sends the resulting low quality image data to a receiver like Hyperion's flatbuffer server. On newer TVs there is no official way for capturing DRM-protected content like Netflix or Amazon. This restriction doesn't take place for content comming from an HDMI input. So currently as a workaround you can play your media using your PC, FireTV-Stick or Chromecast and still enjoy your LEDs. This app requires to be run as root and tries to do this at the first start using the Homebrew Channel.
_______________________________________________
Feel free to raise an issue or pull request, or come to the OpenLG-Discord, if you have some questions.
Some love to everyone who was, or still is involved into this project and of course the OpenLG-/Hyperion-Community! ♥
hyperion-webos
PicCap
State:
Loading..
| Receiver:
n/a
| UI:
n/a
| Video:
n/a
| FPS:
n/a
================================================
FILE: frontend/js/domrect-polyfill.js
================================================
//
// https://raw.githubusercontent.com/Financial-Times/polyfill-library/c25c30e4463bef60fba1213ecb697f3e3f253d7b/polyfills/DOMRect/polyfill.js
// License: MIT
//
(function (global) {
function number(v) {
return v === undefined ? 0 : Number(v);
}
function different(u, v) {
return u !== v && !(isNaN(u) && isNaN(v));
}
function DOMRect(xArg, yArg, wArg, hArg) {
var x, y, width, height, left, right, top, bottom;
x = number(xArg);
y = number(yArg);
width = number(wArg);
height = number(hArg);
Object.defineProperties(this, {
x: {
get: function () { return x; },
set: function (newX) {
if (different(x, newX)) {
x = newX;
left = right = undefined;
}
},
enumerable: true
},
y: {
get: function () { return y; },
set: function (newY) {
if (different(y, newY)) {
y = newY;
top = bottom = undefined;
}
},
enumerable: true
},
width: {
get: function () { return width; },
set: function (newWidth) {
if (different(width, newWidth)) {
width = newWidth;
left = right = undefined;
}
},
enumerable: true
},
height: {
get: function () { return height; },
set: function (newHeight) {
if (different(height, newHeight)) {
height = newHeight;
top = bottom = undefined;
}
},
enumerable: true
},
left: {
get: function () {
if (left === undefined) {
left = x + Math.min(0, width);
}
return left;
},
enumerable: true
},
right: {
get: function () {
if (right === undefined) {
right = x + Math.max(0, width);
}
return right;
},
enumerable: true
},
top: {
get: function () {
if (top === undefined) {
top = y + Math.min(0, height);
}
return top;
},
enumerable: true
},
bottom: {
get: function () {
if (bottom === undefined) {
bottom = y + Math.max(0, height);
}
return bottom;
},
enumerable: true
}
});
}
global.DOMRect = DOMRect;
}(self));
================================================
FILE: frontend/js/servicecalls.js
================================================
require('core-js/stable');
const availableQuirks = {
// DILE_VT
QUIRK_DILE_VT_CREATE_EX: '0x1',
QUIRK_DILE_VT_NO_FREEZE_CAPTURE: '0x2',
QUIRK_DILE_VT_DUMP_LOCATION_2: '0x4',
// vtCapture
QUIRK_VTCAPTURE_FORCE_CAPTURE: '0x100',
};
let isRoot = false;
let rootingInProgress = false;
function logIt(message) {
const textareaConsoleLog = document.getElementById('textareaConsoleLog');
console.log(message);
textareaConsoleLog.value += `${message}\n`;
}
function onHBExec(result) {
if (result.returnValue === true) {
logIt(`HBChannel exec returned. stdout: ${result.stdoutString} stderr: ${result.stderrString}`);
} else {
logIt(`HBChannel exec failed! Code: ${result.errorCode}`);
}
}
function killHyperion() {
document.getElementById('txtInfoState').innerHTML = 'Killing service..';
logIt('Calling HBChannel exec to kill hyperion-webos');
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.hbchannel.service',
{
method: 'exec',
parameters: {
command: 'kill -9 $(pidof hyperion-webos)',
},
onSuccess: onHBExec,
onFailure: onHBExec,
},
);
/* eslint-enable no-undef */
}
function makeServiceRoot() {
logIt('Rooting..');
document.getElementById('txtInfoState').innerHTML = 'Rooting app and service..';
logIt('Calling HBChannel exec to elevate app and service');
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.hbchannel.service',
{
method: 'exec',
parameters: {
command: '/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service org.webosbrew.piccap; /media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service org.webosbrew.piccap.service',
},
onSuccess(result) {
onHBExec(result);
logIt('Elevation completed - killing service process..');
document.getElementById('txtInfoState').innerHTML = 'Killing service..';
killHyperion();
},
onFailure: onHBExec,
},
);
/* eslint-enable no-undef */
document.getElementById('txtInfoState').innerHTML = 'Finished making root processing';
}
let checkRootStatusIntervalID = null;
function onCheckRootStatus(result) {
if (result.returnValue === true) {
if (result.elevated) {
logIt('PicCap-Service returned rooted!');
document.getElementById('txtInfoState').innerHTML = 'Running as root';
isRoot = true;
clearInterval(checkRootStatusIntervalID);
rootingInProgress = false;
} else {
if (rootingInProgress === false) {
logIt('Rooting not in progress yet.');
makeServiceRoot();
rootingInProgress = true;
}
logIt('PicCap-Service returned not rooted yet! Will check again soon.');
document.getElementById('txtInfoState').innerHTML = 'Not running as root. Service elevation in progress..';
}
} else {
logIt(`Getting root-status from PicCap-Service failed! Will try again. Code: ${result.errorCode}`);
document.getElementById('txtInfoState').innerHTML = 'PicCap-Service status failed!';
}
}
function checkRoot() {
document.getElementById('txtServiceStatus').innerHTML = 'Processing root check';
logIt('Starting loop for PicCap-Service to get root-status');
let firstInterval = true;
checkRootStatusIntervalID = window.setInterval(() => {
logIt('Calling PicCap-Service to get root-status');
document.getElementById('txtInfoState').innerHTML = 'Checking root status';
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.piccap.service',
{
method: 'status',
parameters: {},
onSuccess: onCheckRootStatus,
onFailure: onCheckRootStatus,
},
);
/* eslint-enable no-undef */
if (rootingInProgress === false && isRoot === false && firstInterval === false) {
logIt('Not rooted and rooting not in progress yet.');
makeServiceRoot();
rootingInProgress = true;
}
firstInterval = false;
}, 3000);
}
function getStatus() {
document.getElementById('txtInfoState').innerHTML = 'Getting status info..';
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.piccap.service',
{
method: 'status',
parameters: {},
onSuccess(result) {
if (result.returnValue === true) {
document.getElementById('txtServiceVersion').innerHTML = result.version;
document.getElementById('txtServiceStatus').innerHTML = result.isRunning ? 'Capturing' : 'Not capturing';
document.getElementById('txtInfoReceiver').innerHTML = result.connected ? 'Connected' : 'Disconnected';
document.getElementById('txtInfoVideo').innerHTML = result.videoRunning ? `Capturing with ${result.videoBackend}` : 'Not capturing';
document.getElementById('txtInfoUI').innerHTML = result.uiRunning ? `Capturing with ${result.uiBackend}` : 'Not capturing';
document.getElementById('txtInfoFPS').innerHTML = result.framerate.toFixed(2); /* Round to 2 decimal points */
document.getElementById('txtInfoState').innerHTML = 'Status info refreshed';
} else {
logIt('Getting status info from PicCap-Service failed! Return value false!');
document.getElementById('txtInfoState').innerHTML = 'Getting status info failed!';
}
},
onFailure(result) {
logIt(`Getting status info from PicCap-Service failed! Code: ${result.errorCode}`);
document.getElementById('txtInfoState').innerHTML = 'Getting status info failed!';
},
},
);
/* eslint-enable no-undef */
}
function getSettings() {
document.getElementById('txtInfoState').innerHTML = 'Loading settings..';
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.piccap.service',
{
method: 'getSettings',
parameters: {},
onSuccess(result) {
if (result.returnValue === true) {
document.getElementById('selectSettingsVideoBackend').value = result.novideo === true ? 'disabled' : result.backend || 'auto';
document.getElementById('selectSettingsGraphicalBackend').value = result.nogui === true ? 'disabled' : result.uibackend || 'auto';
document.getElementById('checkSettingsLocalSocket').checked = result['unix-socket'];
socketCheckChanged(document.getElementById('checkSettingsLocalSocket'));
if (result.address.includes('/')) {
switch (result.address) {
case '/tmp/hyperhdr-domain':
document.getElementById('selectSettingsSocket').value = 'hyperhdr';
break;
default:
document.getElementById('selectSettingsSocket').value = 'manual';
document.getElementById('txtInputSettingsAddress').value = result.address;
}
document.getElementById('txtInputSettingsAddress').value = '127.0.0.1';
socketSelectChanged(document.getElementById('selectSettingsSocket'));
} else {
document.getElementById('txtInputSettingsAddress').value = result.address || '127.0.0.1';
}
document.getElementById('txtInputSettingsPort').value = result.port;
document.getElementById('txtInputSettingsPriority').value = result.priority;
document.getElementById('txtInputSettingsFPS').value = result.fps;
// Process Height/Width for easier selection
switch (result.width * result.height) {
case 57600:
document.getElementById('selectSettingsResolution').value = '320x180';
break;
case 36864:
document.getElementById('selectSettingsResolution').value = '256x144';
break;
case 20736:
document.getElementById('selectSettingsResolution').value = '192x108';
break;
case 9984:
document.getElementById('selectSettingsResolution').value = '128x78';
break;
default:
document.getElementById('selectSettingsResolution').value = 'manual';
document.getElementById('txtInputSettingsWidth').value = result.width;
document.getElementById('txtInputSettingsHeight').value = result.height;
break;
}
Object.keys(availableQuirks).forEach((quirk) => {
logIt(`Processing: ${quirk}`);
const quirkval = availableQuirks[quirk];
/* eslint-disable eqeqeq */
if ((result.quirks & quirkval) == quirkval) {
logIt(`Quirk ${quirk} enabled!`);
document.getElementById(`checkSettings${quirk}`).checked = true;
}
/* eslint-enable eqeqeq */
});
document.getElementById('checkSettingsVSync').checked = result.vsync;
document.getElementById('checkSettingsAutostart').checked = result.autostart;
document.getElementById('checkSettingsNoHDR').checked = result.nohdr;
document.getElementById('checkSettingsNoPowerstate').checked = result.nopowerstate;
document.getElementById('checkSettingsNV12').checked = result.nv12;
logIt('Loading settings done!');
document.getElementById('txtInfoState').innerHTML = 'Settings loaded';
} else {
logIt('Getting settings from PicCap-Service failed! Return value false!');
document.getElementById('txtInfoState').innerHTML = 'Getting settings failed!';
}
},
onFailure(result) {
logIt(`Getting settings from PicCap-Service failed! Code: ${result.errorCode}`);
document.getElementById('txtInfoState').innerHTML = 'Getting settings failed!';
},
},
);
/* eslint-enable no-undef */
}
window.restartHyperion = () => {
document.getElementById('txtInfoState').innerHTML = 'Killing hyperion.. Will be started again through status loop';
killHyperion();
};
function saveSettings(config) {
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.piccap.service',
{
method: 'setSettings',
parameters: config,
onSuccess(result) {
if (result.returnValue === true) {
logIt('Saving settings success!');
document.getElementById('txtInfoState').innerHTML = 'Save settings success!';
getSettings();
} else {
logIt('Save settings for PicCap-Service failed! Return value false!');
document.getElementById('txtInfoState').innerHTML = 'Save settings failed!';
}
},
onFailure(result) {
logIt(`Save settings for PicCap-Service failed! Code: ${result.errorCode}`);
document.getElementById('txtInfoState').innerHTML = 'Sace settings failed!';
},
},
);
/* eslint-enable no-undef */
}
window.serviceResetSettings = () => {
document.getElementById('txtInfoState').innerHTML = 'Loading default settings..';
const config = {
backend: 'auto',
uibackend: 'auto',
novideo: false,
nogui: false,
address: '',
port: 19400,
priority: 150,
fps: 0,
width: 320,
height: 180,
quirks: 0,
vsync: true,
autostart: false,
nv12: false,
};
logIt(config);
document.getElementById('txtInfoState').innerHTML = 'Sending default settings..';
saveSettings(config);
};
window.serviceSaveSettings = () => {
document.getElementById('txtInfoState').innerHTML = 'Collecting settings..';
let quirkcalc = 0;
Object.keys(availableQuirks).forEach((quirk) => {
logIt(`Processing quirk: ${quirk}`);
const quirkval = availableQuirks[quirk];
logIt(`Quirk val: ${quirkval}`);
if (document.getElementById(`checkSettings${quirk}`).checked === true) {
quirkcalc |= quirkval;
logIt(`Quirkcalc: ${quirkcalc}`);
}
});
let width;
let height;
switch (document.getElementById('selectSettingsResolution').value) {
case '320x180':
width = 320;
height = 180;
break;
case '256x144':
width = 256;
height = 144;
break;
case '192x108':
width = 192;
height = 108;
break;
case '128x78':
width = 128;
height = 78;
break;
case 'manual':
width = parseInt(document.getElementById('txtInputSettingsWidth').value, 10);
height = parseInt(document.getElementById('txtInputSettingsHeight').value, 10);
break;
default:
width = 320;
height = 180;
break;
}
let address;
if (document.getElementById('checkSettingsLocalSocket').checked === true) {
switch (document.getElementById('selectSettingsSocket').value) {
case 'hyperhdr':
address = '/tmp/hyperhdr-domain';
break;
case 'manual':
address = document.getElementById('txtInputSettingsSocketPath').value;
break;
default:
address = undefined;
logIt('Address wasnt found!');
break;
}
} else {
address = document.getElementById('txtInputSettingsAddress').value;
}
const config = {
backend: document.getElementById('selectSettingsVideoBackend').value === 'disabled' ? 'auto' : document.getElementById('selectSettingsVideoBackend').value,
uibackend: document.getElementById('selectSettingsGraphicalBackend').value === 'disabled' ? 'auto' : document.getElementById('selectSettingsGraphicalBackend').value,
novideo: document.getElementById('selectSettingsVideoBackend').value === 'disabled',
nogui: document.getElementById('selectSettingsGraphicalBackend').value === 'disabled',
'unix-socket': document.getElementById('checkSettingsLocalSocket').checked,
address,
port: parseInt(document.getElementById('txtInputSettingsPort').value, 10) || undefined,
priority: parseInt(document.getElementById('txtInputSettingsPriority').value, 10) || undefined,
fps: parseInt(document.getElementById('txtInputSettingsFPS').value, 10) || 0,
width: width || undefined,
height: height || undefined,
quirks: quirkcalc,
vsync: document.getElementById('checkSettingsVSync').checked,
autostart: document.getElementById('checkSettingsAutostart').checked,
nohdr: document.getElementById('checkSettingsNoHDR').checked,
nopowerstate: document.getElementById('checkSettingsNoPowerstate').checked,
nv12: document.getElementById('checkSettingsNV12').checked,
};
logIt(`Config: ${JSON.stringify(config)}`);
document.getElementById('txtInfoState').innerHTML = 'Sending settings..';
saveSettings(config);
};
window.reloadHyperionLog = () => {
logIt('Calling HBCHannel to get latest 200 hyperion-webos log lines.');
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.hbchannel.service',
{
method: 'exec',
parameters: {
command: 'grep hyperion-webos /var/log/messages | tail -n200',
},
onSuccess(result) {
onHBExec(result);
const textareaHyperionLog = document.getElementById('textareaHyperionLog');
textareaHyperionLog.value += `${result.stdoutString}\r\n`;
},
onFailure: onHBExec,
},
);
/* eslint-enable no-undef */
};
// Using this function to setup logging for now.
// Future start/stop of currently not implemented hyperion-webos log method.
window.startStopLogging = () => {
logIt('Setup logging using HBChannel');
document.getElementById('txtInfoState').innerHTML = 'Calling HBChannel for log setup';
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.hbchannel.service',
{
method: 'exec',
parameters: {
command: '/media/developer/apps/usr/palm/services/org.webosbrew.piccap.service/setuplegacylogging.sh',
},
onSuccess: onHBExec,
onFailure: onHBExec,
},
);
/* eslint-enable no-undef */
/*
// Future Stuff
const btnLogStartStop = document.getElementById('btnLogStartStop');
if (!loggingStarted) {
loggingStarted = true;
btnLogStartStop.innerHTML = 'Stop logging';
} else {
loggingStarted = false;
btnLogStartStop.innerHTML = 'Start logging';
} */
};
function onServiceCallback(result) {
if (result.returnValue === true) {
logIt('Servicecall returned successfully.');
document.getElementById('txtInfoState').innerHTML = 'Servicecall success!';
} else {
logIt(`Servicecall failed! Code: ${result.errorCode}`);
document.getElementById('txtInfoState').innerHTML = 'Servicecall failed!';
}
}
window.serviceStart = () => {
logIt('Start clicked');
try {
document.getElementById('txtServiceStatus').innerHTML = 'Starting service...';
document.getElementById('txtInfoState').innerHTML = 'Sending start command';
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.piccap.service',
{
method: 'start',
parameters: {},
onSuccess: onServiceCallback,
onFailure: onServiceCallback,
},
);
/* eslint-enable no-undef */
} catch (err) {
document.getElementById('txtServiceStatus').innerHTML = `Failed: ${JSON.stringify(err)}`;
}
document.getElementById('txtInfoState').innerHTML = 'Start command send';
};
window.serviceStop = () => {
logIt('Stop clicked');
try {
document.getElementById('txtServiceStatus').innerHTML = 'Stopping service...';
document.getElementById('txtInfoState').innerHTML = 'Sending stop command';
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.piccap.service',
{
method: 'stop',
parameters: {},
onSuccess: onServiceCallback,
onFailure: onServiceCallback,
},
);
/* eslint-enable no-undef */
} catch (err) {
document.getElementById('txtServiceStatus').innerHTML = `Failed: ${JSON.stringify(err)}`;
}
document.getElementById('txtInfoState').innerHTML = 'Stop command send';
};
window.serviceReload = () => {
logIt('Reload clicked');
getSettings();
};
window.tvReboot = () => {
logIt('Trying to reboot TV using HBChannel..');
document.getElementById('txtInfoState').innerHTML = 'Rebooting TV..';
/* eslint-disable no-undef */
webOS.service.request(
'luna://org.webosbrew.hbchannel.service',
{
method: 'reboot',
parameters: {},
onSuccess: onServiceCallback,
onFailure: onServiceCallback,
},
);
/* eslint-enable no-undef */
};
window.addEventListener('load', () => {
logIt('Startup of PicCap...');
checkRoot();
logIt('Starting load settings loop...');
const getStatusIntervalID = window.setInterval(() => {
if (isRoot === true) {
logIt('Loading settings, we are rooted.');
getSettings();
clearInterval(getStatusIntervalID);
}
}, 3000);
logIt('Starting status loop...');
setInterval(() => {
getStatus();
}, 4000);
});
================================================
FILE: frontend/js/spatial-navigation.js
================================================
//
// https://raw.githubusercontent.com/WICG/spatial-navigation/183f0146b6741007e46fa64ab0950447defdf8af/polyfill/spatial-navigation-polyfill.js
// License: MIT
// Line 97-104: Added checkbox behavior
//
/* Spatial Navigation Polyfill
*
* It follows W3C official specification
* https://drafts.csswg.org/css-nav-1/
*
* Copyright (c) 2018-2019 LG Electronics Inc.
* https://github.com/WICG/spatial-navigation/polyfill
*
* Licensed under the MIT license (MIT)
*/
import 'core-js';
import './domrect-polyfill';
(function () {
// The polyfill must not be executed, if it's already enabled via browser engine or browser extensions.
if ('navigate' in window) {
return;
}
const ARROW_KEY_CODE = {
37: 'left', 38: 'up', 39: 'right', 40: 'down',
};
const TAB_KEY_CODE = 9;
const ENTER_KEY_CODE = 13;
let mapOfBoundRect = null;
let startingPoint = null; // Saves spatial navigation starting point
const savedSearchOrigin = { element: null, rect: null }; // Saves previous search origin
let searchOriginRect = null; // Rect of current search origin
/**
* Initiate the spatial navigation features of the polyfill.
* @function initiateSpatialNavigation
*/
function initiateSpatialNavigation() {
/*
* Bind the standards APIs to be exposed to the window object for authors
*/
window.navigate = navigate;
window.Element.prototype.spatialNavigationSearch = spatialNavigationSearch;
window.Element.prototype.focusableAreas = focusableAreas;
window.Element.prototype.getSpatialNavigationContainer = getSpatialNavigationContainer;
/*
* CSS.registerProperty() from the Properties and Values API
* Reference: https://drafts.css-houdini.org/css-properties-values-api/#the-registerproperty-function
*/
if (window.CSS && CSS.registerProperty) {
if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-contain') === '') {
CSS.registerProperty({
name: '--spatial-navigation-contain',
syntax: 'auto | contain',
inherits: false,
initialValue: 'auto',
});
}
if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-action') === '') {
CSS.registerProperty({
name: '--spatial-navigation-action',
syntax: 'auto | focus | scroll',
inherits: false,
initialValue: 'auto',
});
}
if (window.getComputedStyle(document.documentElement).getPropertyValue('--spatial-navigation-function') === '') {
CSS.registerProperty({
name: '--spatial-navigation-function',
syntax: 'normal | grid',
inherits: false,
initialValue: 'normal',
});
}
}
}
/**
* Add event handlers for the spatial navigation behavior.
* This function defines which input methods trigger the spatial navigation behavior.
* @function spatialNavigationHandler
*/
function spatialNavigationHandler() {
/*
* keydown EventListener :
* If arrow key pressed, get the next focusing element and send it to focusing controller
*/
window.addEventListener('keydown', (e) => {
const currentKeyMode = (parent && parent.__spatialNavigation__.keyMode) || window.__spatialNavigation__.keyMode;
const eventTarget = document.activeElement;
const dir = ARROW_KEY_CODE[e.keyCode];
if (e.keyCode === TAB_KEY_CODE) {
startingPoint = null;
}
// Added checkbox checking check
if ((e.keyCode === ENTER_KEY_CODE) && ('checkbox'.includes(eventTarget.getAttribute('type')))) {
if (eventTarget.checked == true) {
eventTarget.checked = false;
} else {
eventTarget.checked = true;
}
}
if (!currentKeyMode
|| (currentKeyMode === 'NONE')
|| ((currentKeyMode === 'SHIFTARROW') && !e.shiftKey)
|| ((currentKeyMode === 'ARROW') && e.shiftKey)
|| (e.ctrlKey || e.metaKey || e.altKey)) return;
if (!e.defaultPrevented) {
let focusNavigableArrowKey = {
left: true, up: true, right: true, down: true,
};
// Edge case (text input, area) : Don't move focus, just navigate cursor in text area
if ((eventTarget.nodeName === 'INPUT') || eventTarget.nodeName === 'TEXTAREA') {
focusNavigableArrowKey = handlingEditableElement(e);
}
if (focusNavigableArrowKey[dir]) {
e.preventDefault();
mapOfBoundRect = new Map();
navigate(dir);
mapOfBoundRect = null;
startingPoint = null;
}
}
});
/*
* mouseup EventListener :
* If the mouse click a point in the page, the point will be the starting point.
* NOTE: Let UA set the spatial navigation starting point based on click
*/
document.addEventListener('mouseup', (e) => {
startingPoint = { x: e.clientX, y: e.clientY };
});
/*
* focusin EventListener :
* When the element get the focus, save it and its DOMRect for resetting the search origin
* if it disappears.
*/
window.addEventListener('focusin', (e) => {
if (e.target !== window) {
savedSearchOrigin.element = e.target;
savedSearchOrigin.rect = e.target.getBoundingClientRect();
}
});
}
/**
* Enable the author to trigger spatial navigation programmatically, as if the user had done so manually.
* @see {@link https://drafts.csswg.org/css-nav-1/#dom-window-navigate}
* @function navigate
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
*/
function navigate(dir) {
// spatial navigation steps
// 1
const searchOrigin = findSearchOrigin();
let eventTarget = searchOrigin;
let elementFromPosition = null;
// 2 Optional step, UA defined starting point
if (startingPoint) {
// if there is a starting point, set eventTarget as the element from position for getting the spatnav container
elementFromPosition = document.elementFromPoint(startingPoint.x, startingPoint.y);
// Use starting point if the starting point isn't inside the focusable element (but not container)
// * Starting point is meaningfull when:
// 1) starting point is inside the spatnav container
// 2) starting point is inside the non-focusable element
if (elementFromPosition === null) {
elementFromPosition = document.body;
}
if (isFocusable(elementFromPosition) && !isContainer(elementFromPosition)) {
startingPoint = null;
} else if (isContainer(elementFromPosition)) {
eventTarget = elementFromPosition;
} else {
eventTarget = elementFromPosition.getSpatialNavigationContainer();
}
}
// 4
if (eventTarget === document || eventTarget === document.documentElement) {
eventTarget = document.body || document.documentElement;
}
// 5
// At this point, spatialNavigationSearch can be applied.
// If startingPoint is either a scroll container or the document,
// find the best candidate within startingPoint
let container = null;
if ((isContainer(eventTarget) || eventTarget.nodeName === 'BODY') && !(eventTarget.nodeName === 'INPUT')) {
if (eventTarget.nodeName === 'IFRAME') {
eventTarget = eventTarget.contentDocument.documentElement;
}
container = eventTarget;
let bestInsideCandidate = null;
// 5-2
if ((document.activeElement === searchOrigin)
|| (document.activeElement === document.body) && (searchOrigin === document.documentElement)) {
if (getCSSSpatNavAction(eventTarget) === 'scroll') {
if (scrollingController(eventTarget, dir)) return;
} else if (getCSSSpatNavAction(eventTarget) === 'focus') {
bestInsideCandidate = eventTarget.spatialNavigationSearch(dir, { container: eventTarget, candidates: getSpatialNavigationCandidates(eventTarget, { mode: 'all' }) });
if (focusingController(bestInsideCandidate, dir)) return;
} else if (getCSSSpatNavAction(eventTarget) === 'auto') {
bestInsideCandidate = eventTarget.spatialNavigationSearch(dir, { container: eventTarget });
if (focusingController(bestInsideCandidate, dir) || scrollingController(eventTarget, dir)) return;
}
} else {
// when the previous search origin became offscreen
container = container.getSpatialNavigationContainer();
}
}
// 6
// Let container be the nearest ancestor of eventTarget
container = eventTarget.getSpatialNavigationContainer();
let parentContainer = (container.parentElement) ? container.getSpatialNavigationContainer() : null;
// When the container is the viewport of a browsing context
if (!parentContainer && (window.location !== window.parent.location)) {
parentContainer = window.parent.document.documentElement;
}
if (getCSSSpatNavAction(container) === 'scroll') {
if (scrollingController(container, dir)) return;
} else if (getCSSSpatNavAction(container) === 'focus') {
navigateChain(eventTarget, container, parentContainer, dir, 'all');
} else if (getCSSSpatNavAction(container) === 'auto') {
navigateChain(eventTarget, container, parentContainer, dir, 'visible');
}
}
/**
* Move the focus to the best candidate or do nothing.
* @function focusingController
* @param bestCandidate {Node} - The best candidate of the spatial navigation
* @param dir {SpatialNavigationDirection}- The directional information for the spatial navigation (e.g. LRUD)
* @returns {boolean}
*/
function focusingController(bestCandidate, dir) {
// 10 & 11
// When bestCandidate is found
if (bestCandidate) {
// When bestCandidate is a focusable element and not a container : move focus
/*
* [event] navbeforefocus : Fired before spatial or sequential navigation changes the focus.
*/
if (!createSpatNavEvents('beforefocus', bestCandidate, null, dir)) return true;
const container = bestCandidate.getSpatialNavigationContainer();
if ((container !== window) && (getCSSSpatNavAction(container) === 'focus')) {
bestCandidate.focus();
} else {
bestCandidate.focus({ preventScroll: true });
}
startingPoint = null;
return true;
}
// When bestCandidate is not found within the scrollport of a container: Nothing
return false;
}
/**
* Directionally scroll the scrollable spatial navigation container if it can be manually scrolled more.
* @function scrollingController
* @param container {Node} - The spatial navigation container which can scroll
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {boolean}
*/
function scrollingController(container, dir) {
// If there is any scrollable area among parent elements and it can be manually scrolled, scroll the document
if (isScrollable(container, dir) && !isScrollBoundary(container, dir)) {
moveScroll(container, dir);
return true;
}
// If the spatnav container is document and it can be scrolled, scroll the document
if (!container.parentElement && !isHTMLScrollBoundary(container, dir)) {
moveScroll(container.ownerDocument.documentElement, dir);
return true;
}
return false;
}
/**
* Find the candidates within a spatial navigation container include delegable container.
* This function does not search inside delegable container or focusable container.
* In other words, this return candidates set is not included focusable elements inside delegable container or focusable container.
*
* @function getSpatialNavigationCandidates
* @param container {Node} - The spatial navigation container
* @param option {FocusableAreasOptions} - 'mode' attribute takes 'visible' or 'all' for searching the boundary of focusable elements.
* Default value is 'visible'.
* @returns {sequence} candidate elements within the container
*/
function getSpatialNavigationCandidates(container, option = { mode: 'visible' }) {
let candidates = [];
if (container.childElementCount > 0) {
if (!container.parentElement) {
container = container.getElementsByTagName('body')[0] || document.body;
}
const { children } = container;
for (const elem of children) {
if (isDelegableContainer(elem)) {
candidates.push(elem);
} else if (isFocusable(elem)) {
candidates.push(elem);
if (!isContainer(elem) && elem.childElementCount) {
candidates = candidates.concat(getSpatialNavigationCandidates(elem, { mode: 'all' }));
}
} else if (elem.childElementCount) {
candidates = candidates.concat(getSpatialNavigationCandidates(elem, { mode: 'all' }));
}
}
}
return (option.mode === 'all') ? candidates : candidates.filter(isVisible);
}
/**
* Find the candidates among focusable elements within a spatial navigation container from the search origin (currently focused element)
* depending on the directional information.
* @function getFilteredSpatialNavigationCandidates
* @param element {Node} - The currently focused element which is defined as 'search origin' in the spec
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @param candidates {sequence} - The candidates for spatial navigation without the directional information
* @param container {Node} - The spatial navigation container
* @returns {Node} The candidates for spatial navigation considering the directional information
*/
function getFilteredSpatialNavigationCandidates(element, dir, candidates, container) {
const targetElement = element;
// Removed below line due to a bug. (iframe body rect is sometime weird.)
// const targetElement = (element.nodeName === 'IFRAME') ? element.contentDocument.body : element;
// If the container is unknown, get the closest container from the element
container = container || targetElement.getSpatialNavigationContainer();
// If the candidates is unknown, find candidates
// 5-1
candidates = (!candidates || candidates.length <= 0) ? getSpatialNavigationCandidates(container) : candidates;
return filteredCandidates(targetElement, candidates, dir, container);
}
/**
* Find the best candidate among the candidates within the container from the search origin (currently focused element)
* @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-spatialnavigationsearch}
* @function spatialNavigationSearch
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @param candidates {sequence} - The candidates for spatial navigation
* @param container {Node} - The spatial navigation container
* @returns {Node} The best candidate which will gain the focus
*/
function spatialNavigationSearch(dir, args) {
const targetElement = this;
let internalCandidates = [];
let externalCandidates = [];
const insideOverlappedCandidates = getOverlappedCandidates(targetElement);
let bestTarget;
// Set default parameter value
if (!args) args = {};
const defaultContainer = targetElement.getSpatialNavigationContainer();
let defaultCandidates = getSpatialNavigationCandidates(defaultContainer);
const container = args.container || defaultContainer;
if (args.container && (defaultContainer.contains(args.container))) {
defaultCandidates = defaultCandidates.concat(getSpatialNavigationCandidates(container));
}
const candidates = (args.candidates && args.candidates.length > 0)
? args.candidates.filter((candidate) => container.contains(candidate))
: defaultCandidates.filter((candidate) => container.contains(candidate) && (container !== candidate));
// Find the best candidate
// 5
// If startingPoint is either a scroll container or the document,
// find the best candidate within startingPoint
if (candidates && candidates.length > 0) {
// Divide internal or external candidates
candidates.forEach((candidate) => {
if (candidate !== targetElement) {
(targetElement.contains(candidate) && targetElement !== candidate ? internalCandidates : externalCandidates).push(candidate);
}
});
// include overlapped element to the internalCandidates
const fullyOverlapped = insideOverlappedCandidates.filter((candidate) => !internalCandidates.includes(candidate));
const overlappedContainer = candidates.filter((candidate) => (isContainer(candidate) && isEntirelyVisible(targetElement, candidate)));
const overlappedByParent = overlappedContainer.map((elm) => elm.focusableAreas()).flat().filter((candidate) => candidate !== targetElement);
internalCandidates = internalCandidates.concat(fullyOverlapped).filter((candidate) => container.contains(candidate));
externalCandidates = externalCandidates.concat(overlappedByParent).filter((candidate) => container.contains(candidate));
// Filter external Candidates
if (externalCandidates.length > 0) {
externalCandidates = getFilteredSpatialNavigationCandidates(targetElement, dir, externalCandidates, container);
}
// If there isn't search origin element but search orgin rect exist (search origin isn't in the layout case)
if (searchOriginRect) {
bestTarget = selectBestCandidate(targetElement, getFilteredSpatialNavigationCandidates(targetElement, dir, internalCandidates, container), dir);
}
if ((internalCandidates && internalCandidates.length > 0) && !(targetElement.nodeName === 'INPUT')) {
bestTarget = selectBestCandidateFromEdge(targetElement, internalCandidates, dir);
}
bestTarget = bestTarget || selectBestCandidate(targetElement, externalCandidates, dir);
if (bestTarget && isDelegableContainer(bestTarget)) {
// if best target is delegable container, then find descendants candidate inside delegable container.
const innerTarget = getSpatialNavigationCandidates(bestTarget, { mode: 'all' });
const descendantsBest = innerTarget.length > 0 ? targetElement.spatialNavigationSearch(dir, { candidates: innerTarget, container: bestTarget }) : null;
if (descendantsBest) {
bestTarget = descendantsBest;
} else if (!isFocusable(bestTarget)) {
// if there is no target inside bestTarget and delegable container is not focusable,
// then try to find another best target without curren best target.
candidates.splice(candidates.indexOf(bestTarget), 1);
bestTarget = candidates.length ? targetElement.spatialNavigationSearch(dir, { candidates, container }) : null;
}
}
return bestTarget;
}
return null;
}
/**
* Get the filtered candidate among candidates.
* @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate}
* @function filteredCandidates
* @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
* @param candidates {sequence} - The candidates for spatial navigation
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @param container {Node} - The spatial navigation container
* @returns {sequence} The filtered candidates which are not the search origin and not in the given spatial navigation direction from the search origin
*/
// TODO: Need to fix filtering the candidates with more clean code
function filteredCandidates(currentElm, candidates, dir, container) {
const originalContainer = currentElm.getSpatialNavigationContainer();
let eventTargetRect;
// If D(dir) is null, let candidates be the same as visibles
if (dir === undefined) return candidates;
// Offscreen handling when originalContainer is not
if (originalContainer.parentElement && container !== originalContainer && !isVisible(currentElm)) {
eventTargetRect = getBoundingClientRect(originalContainer);
} else {
eventTargetRect = searchOriginRect || getBoundingClientRect(currentElm);
}
/*
* Else, let candidates be the subset of the elements in visibles
* whose principal box’s geometric center is within the closed half plane
* whose boundary goes through the geometric center of starting point and is perpendicular to D.
*/
if ((isContainer(currentElm) || currentElm.nodeName === 'BODY') && !(currentElm.nodeName === 'INPUT')) {
return candidates.filter((candidate) => {
const candidateRect = getBoundingClientRect(candidate);
return container.contains(candidate)
&& ((currentElm.contains(candidate) && isInside(eventTargetRect, candidateRect) && candidate !== currentElm)
|| isOutside(candidateRect, eventTargetRect, dir));
});
}
return candidates.filter((candidate) => {
const candidateRect = getBoundingClientRect(candidate);
const candidateBody = (candidate.nodeName === 'IFRAME') ? candidate.contentDocument.body : null;
return container.contains(candidate)
&& candidate !== currentElm && candidateBody !== currentElm
&& isOutside(candidateRect, eventTargetRect, dir)
&& !isInside(eventTargetRect, candidateRect);
});
}
/**
* Select the best candidate among given candidates.
* @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate}
* @function selectBestCandidate
* @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
* @param candidates {sequence} - The candidates for spatial navigation
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {Node} The best candidate which will gain the focus
*/
function selectBestCandidate(currentElm, candidates, dir) {
const container = currentElm.getSpatialNavigationContainer();
const spatialNavigationFunction = getComputedStyle(container).getPropertyValue('--spatial-navigation-function');
const currentTargetRect = searchOriginRect || getBoundingClientRect(currentElm);
let distanceFunction;
let alignedCandidates;
switch (spatialNavigationFunction) {
case 'grid':
alignedCandidates = candidates.filter((elm) => isAligned(currentTargetRect, getBoundingClientRect(elm), dir));
if (alignedCandidates.length > 0) {
candidates = alignedCandidates;
}
distanceFunction = getAbsoluteDistance;
break;
default:
distanceFunction = getDistance;
break;
}
return getClosestElement(currentElm, candidates, dir, distanceFunction);
}
/**
* Select the best candidate among candidates by finding the closet candidate from the edge of the currently focused element (search origin).
* @see {@link https://drafts.csswg.org/css-nav-1/#select-the-best-candidate (Step 5)}
* @function selectBestCandidateFromEdge
* @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
* @param candidates {sequence} - The candidates for spatial navigation
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {Node} The best candidate which will gain the focus
*/
function selectBestCandidateFromEdge(currentElm, candidates, dir) {
if (startingPoint) return getClosestElement(currentElm, candidates, dir, getDistanceFromPoint);
return getClosestElement(currentElm, candidates, dir, getInnerDistance);
}
/**
* Select the closest candidate from the currently focused element (search origin) among candidates by using the distance function.
* @function getClosestElement
* @param currentElm {Node} - The currently focused element which is defined as 'search origin' in the spec
* @param candidates {sequence} - The candidates for spatial navigation
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @param distanceFunction {function} - The distance function which measures the distance from the search origin to each candidate
* @returns {Node} The candidate which is the closest one from the search origin
*/
function getClosestElement(currentElm, candidates, dir, distanceFunction) {
let eventTargetRect = null;
if ((window.location !== window.parent.location) && (currentElm.nodeName === 'BODY' || currentElm.nodeName === 'HTML')) {
// If the eventTarget is iframe, then get rect of it based on its containing document
// Set the iframe's position as (0,0) because the rects of elements inside the iframe don't know the real iframe's position.
eventTargetRect = window.frameElement.getBoundingClientRect();
eventTargetRect.x = 0;
eventTargetRect.y = 0;
} else {
eventTargetRect = searchOriginRect || currentElm.getBoundingClientRect();
}
let minDistance = Number.POSITIVE_INFINITY;
let minDistanceElements = [];
if (candidates) {
for (let i = 0; i < candidates.length; i++) {
const distance = distanceFunction(eventTargetRect, getBoundingClientRect(candidates[i]), dir);
// If the same distance, the candidate will be selected in the DOM order
if (distance < minDistance) {
minDistance = distance;
minDistanceElements = [candidates[i]];
} else if (distance === minDistance) {
minDistanceElements.push(candidates[i]);
}
}
}
if (minDistanceElements.length === 0) return null;
return (minDistanceElements.length > 1 && distanceFunction === getAbsoluteDistance)
? getClosestElement(currentElm, minDistanceElements, dir, getEuclideanDistance) : minDistanceElements[0];
}
/**
* Get container of an element.
* @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-getspatialnavigationcontainer}
* @module Element
* @function getSpatialNavigationContainer
* @returns {Node} The spatial navigation container
*/
function getSpatialNavigationContainer() {
let container = this;
do {
if (!container.parentElement) {
if (window.location !== window.parent.location) {
container = window.parent.document.documentElement;
} else {
container = window.document.documentElement;
}
break;
} else {
container = container.parentElement;
}
} while (!isContainer(container));
return container;
}
/**
* Get nearest scroll container of an element.
* @function getScrollContainer
* @param Element
* @returns {Node} The spatial navigation container
*/
function getScrollContainer(element) {
let scrollContainer = element;
do {
if (!scrollContainer.parentElement) {
if (window.location !== window.parent.location) {
scrollContainer = window.parent.document.documentElement;
} else {
scrollContainer = window.document.documentElement;
}
break;
} else {
scrollContainer = scrollContainer.parentElement;
}
} while (!isScrollContainer(scrollContainer) || !isVisible(scrollContainer));
if (scrollContainer === document || scrollContainer === document.documentElement) {
scrollContainer = window;
}
return scrollContainer;
}
/**
* Find focusable elements within the spatial navigation container.
* @see {@link https://drafts.csswg.org/css-nav-1/#dom-element-focusableareas}
* @function focusableAreas
* @param option {FocusableAreasOptions} - 'mode' attribute takes 'visible' or 'all' for searching the boundary of focusable elements.
* Default value is 'visible'.
* @returns {sequence} All focusable elements or only visible focusable elements within the container
*/
function focusableAreas(option = { mode: 'visible' }) {
const container = this.parentElement ? this : document.body;
const focusables = Array.prototype.filter.call(container.getElementsByTagName('*'), isFocusable);
return (option.mode === 'all') ? focusables : focusables.filter(isVisible);
}
/**
* Create the NavigationEvent: navbeforefocus, navnotarget
* @see {@link https://drafts.csswg.org/css-nav-1/#events-navigationevent}
* @function createSpatNavEvents
* @param option {string} - Type of the navigation event (beforefocus, notarget)
* @param element {Node} - The target element of the event
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
*/
function createSpatNavEvents(eventType, containerElement, currentElement, direction) {
if (['beforefocus', 'notarget'].includes(eventType)) {
const data = {
causedTarget: currentElement,
dir: direction,
};
const triggeredEvent = new CustomEvent(`nav${eventType}`, { bubbles: true, cancelable: true, detail: data });
return containerElement.dispatchEvent(triggeredEvent);
}
}
/**
* Get the value of the CSS custom property of the element
* @function readCssVar
* @param element {Node}
* @param varName {string} - The name of the css custom property without '--'
* @returns {string} The value of the css custom property
*/
function readCssVar(element, varName) {
return element.style.getPropertyValue(`--${varName}`).trim();
}
/**
* Decide whether or not the 'contain' value is given to 'spatial-navigation-contain' css property of an element
* @function isCSSSpatNavContain
* @param element {Node}
* @returns {boolean}
*/
function isCSSSpatNavContain(element) {
return readCssVar(element, 'spatial-navigation-contain') === 'contain';
}
/**
* Return the value of 'spatial-navigation-action' css property of an element
* @function getCSSSpatNavAction
* @param element {Node} - would be the spatial navigation container
* @returns {string} auto | focus | scroll
*/
function getCSSSpatNavAction(element) {
return readCssVar(element, 'spatial-navigation-action') || 'auto';
}
/**
* Only move the focus with spatial navigation. Manually scrolling isn't available.
* @function navigateChain
* @param eventTarget {Node} - currently focused element
* @param container {SpatialNavigationContainer} - container
* @param parentContainer {SpatialNavigationContainer} - parent container
* @param option - visible || all
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
*/
function navigateChain(eventTarget, container, parentContainer, dir, option) {
let currentOption = { candidates: getSpatialNavigationCandidates(container, { mode: option }), container };
while (parentContainer) {
if (focusingController(eventTarget.spatialNavigationSearch(dir, currentOption), dir)) {
return;
} if ((option === 'visible') && scrollingController(container, dir)) return;
if (!createSpatNavEvents('notarget', container, eventTarget, dir)) return;
// find the container
if (container === document || container === document.documentElement) {
if (window.location !== window.parent.location) {
// The page is in an iframe. eventTarget needs to be reset because the position of the element in the iframe
eventTarget = window.frameElement;
container = eventTarget.ownerDocument.documentElement;
}
} else {
container = parentContainer;
}
currentOption = { candidates: getSpatialNavigationCandidates(container, { mode: option }), container };
const nextContainer = container.getSpatialNavigationContainer();
if (nextContainer !== container) {
parentContainer = nextContainer;
} else {
parentContainer = null;
}
}
currentOption = { candidates: getSpatialNavigationCandidates(container, { mode: option }), container };
// Behavior after 'navnotarget' - Getting out from the current spatnav container
if ((!parentContainer && container) && focusingController(eventTarget.spatialNavigationSearch(dir, currentOption), dir)) return;
if (!createSpatNavEvents('notarget', currentOption.container, eventTarget, dir)) return;
if ((getCSSSpatNavAction(container) === 'auto') && (option === 'visible')) {
if (scrollingController(container, dir)) return;
}
}
/**
* Find search origin
* @see {@link https://drafts.csswg.org/css-nav-1/#nav}
* @function findSearchOrigin
* @returns {Node} The search origin for the spatial navigation
*/
function findSearchOrigin() {
let searchOrigin = document.activeElement;
if (!searchOrigin || (searchOrigin === document.body && !document.querySelector(':focus'))) {
// When the previous search origin lost its focus by blur: (1) disable attribute (2) visibility: hidden
if (savedSearchOrigin.element && (searchOrigin !== savedSearchOrigin.element)) {
const elementStyle = window.getComputedStyle(savedSearchOrigin.element, null);
const invisibleStyle = ['hidden', 'collapse'];
if (savedSearchOrigin.element.disabled || invisibleStyle.includes(elementStyle.getPropertyValue('visibility'))) {
searchOrigin = savedSearchOrigin.element;
return searchOrigin;
}
}
searchOrigin = document.documentElement;
}
// When the previous search origin lost its focus by blur: (1) display:none () element size turned into zero
if (savedSearchOrigin.element
&& ((getBoundingClientRect(savedSearchOrigin.element).height === 0) || (getBoundingClientRect(savedSearchOrigin.element).width === 0))) {
searchOriginRect = savedSearchOrigin.rect;
}
if (!isVisibleInScroller(searchOrigin)) {
const scroller = getScrollContainer(searchOrigin);
if (scroller && ((scroller === window) || (getCSSSpatNavAction(scroller) === 'auto'))) return scroller;
}
return searchOrigin;
}
/**
* Move the scroll of an element depending on the given spatial navigation directrion
* (Assume that User Agent defined distance is '40px')
* @see {@link https://drafts.csswg.org/css-nav-1/#directionally-scroll-an-element}
* @function moveScroll
* @param element {Node} - The scrollable element
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @param offset {Number} - The explicit amount of offset for scrolling. Default value is 0.
*/
function moveScroll(element, dir, offset = 0) {
if (element) {
switch (dir) {
case 'left': element.scrollLeft -= (40 + offset); break;
case 'right': element.scrollLeft += (40 + offset); break;
case 'up': element.scrollTop -= (40 + offset); break;
case 'down': element.scrollTop += (40 + offset); break;
}
}
}
/**
* Decide whether an element is container or not.
* @function isContainer
* @param element {Node} element
* @returns {boolean}
*/
function isContainer(element) {
return (!element.parentElement)
|| (element.nodeName === 'IFRAME')
|| (isScrollContainer(element))
|| (isCSSSpatNavContain(element));
}
/**
* Decide whether an element is delegable container or not.
* NOTE: THIS IS NON-NORMATIVE API.
* @function isDelegableContainer
* @param element {Node} element
* @returns {boolean}
*/
function isDelegableContainer(element) {
return readCssVar(element, 'spatial-navigation-contain') === 'delegable';
}
/**
* Decide whether an element is a scrollable container or not.
* @see {@link https://drafts.csswg.org/css-overflow-3/#scroll-container}
* @function isScrollContainer
* @param element {Node}
* @returns {boolean}
*/
function isScrollContainer(element) {
const elementStyle = window.getComputedStyle(element, null);
const overflowX = elementStyle.getPropertyValue('overflow-x');
const overflowY = elementStyle.getPropertyValue('overflow-y');
return !!(((overflowX !== 'visible' && overflowX !== 'clip' && isOverflow(element, 'left'))
|| (overflowY !== 'visible' && overflowY !== 'clip' && isOverflow(element, 'down'))));
}
/**
* Decide whether this element is scrollable or not.
* NOTE: If the value of 'overflow' is given to either 'visible', 'clip', or 'hidden', the element isn't scrollable.
* If the value is 'hidden', the element can be only programmically scrollable. (https://drafts.csswg.org/css-overflow-3/#valdef-overflow-hidden)
* @function isScrollable
* @param element {Node}
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {boolean}
*/
function isScrollable(element, dir) { // element, dir
if (element && typeof element === 'object') {
if (dir && typeof dir === 'string') { // parameter: dir, element
if (isOverflow(element, dir)) {
// style property
const elementStyle = window.getComputedStyle(element, null);
const overflowX = elementStyle.getPropertyValue('overflow-x');
const overflowY = elementStyle.getPropertyValue('overflow-y');
switch (dir) {
case 'left':
/* falls through */
case 'right':
return (overflowX !== 'visible' && overflowX !== 'clip' && overflowX !== 'hidden');
case 'up':
/* falls through */
case 'down':
return (overflowY !== 'visible' && overflowY !== 'clip' && overflowY !== 'hidden');
}
}
return false;
} // parameter: element
return (element.nodeName === 'HTML' || element.nodeName === 'BODY')
|| (isScrollContainer(element) && isOverflow(element));
}
}
/**
* Decide whether an element is overflow or not.
* @function isOverflow
* @param element {Node}
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {boolean}
*/
function isOverflow(element, dir) {
if (element && typeof element === 'object') {
if (dir && typeof dir === 'string') { // parameter: element, dir
switch (dir) {
case 'left':
/* falls through */
case 'right':
return (element.scrollWidth > element.clientWidth);
case 'up':
/* falls through */
case 'down':
return (element.scrollHeight > element.clientHeight);
}
} else { // parameter: element
return (element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight);
}
return false;
}
}
/**
* Decide whether the scrollbar of the browsing context reaches to the end or not.
* @function isHTMLScrollBoundary
* @param element {Node} - The top browsing context
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {boolean}
*/
function isHTMLScrollBoundary(element, dir) {
let result = false;
switch (dir) {
case 'left':
result = element.scrollLeft === 0;
break;
case 'right':
result = (element.scrollWidth - element.scrollLeft - element.clientWidth) === 0;
break;
case 'up':
result = element.scrollTop === 0;
break;
case 'down':
result = (element.scrollHeight - element.scrollTop - element.clientHeight) === 0;
break;
}
return result;
}
/**
* Decide whether the scrollbar of an element reaches to the end or not.
* @function isScrollBoundary
* @param element {Node}
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {boolean}
*/
function isScrollBoundary(element, dir) {
if (isScrollable(element, dir)) {
const winScrollY = element.scrollTop;
const winScrollX = element.scrollLeft;
const height = element.scrollHeight - element.clientHeight;
const width = element.scrollWidth - element.clientWidth;
switch (dir) {
case 'left': return (winScrollX === 0);
case 'right': return (Math.abs(winScrollX - width) <= 1);
case 'up': return (winScrollY === 0);
case 'down': return (Math.abs(winScrollY - height) <= 1);
}
}
return false;
}
/**
* Decide whether an element is inside the scorller viewport or not
*
* @function isVisibleInScroller
* @param element {Node}
* @returns {boolean}
*/
function isVisibleInScroller(element) {
const elementRect = element.getBoundingClientRect();
const nearestScroller = getScrollContainer(element);
let scrollerRect = null;
if (nearestScroller !== window) {
scrollerRect = getBoundingClientRect(nearestScroller);
} else {
scrollerRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight);
}
if (isInside(scrollerRect, elementRect, 'left') && isInside(scrollerRect, elementRect, 'down')) return true;
return false;
}
/**
* Decide whether an element is focusable for spatial navigation.
* 1. If element is the browsing context (document, iframe), then it's focusable,
* 2. If the element is scrollable container (regardless of scrollable axis), then it's focusable,
* 3. The value of tabIndex >= 0, then it's focusable,
* 4. If the element is disabled, it isn't focusable,
* 5. If the element is expressly inert, it isn't focusable,
* 6. Whether the element is being rendered or not.
*
* @function isFocusable
* @param element {Node}
* @returns {boolean}
*
* @see {@link https://html.spec.whatwg.org/multipage/interaction.html#focusable-area}
*/
function isFocusable(element) {
if ((element.tabIndex < 0) || isAtagWithoutHref(element) || isActuallyDisabled(element) || isExpresslyInert(element) || !isBeingRendered(element)) return false;
if ((!element.parentElement) || (isScrollable(element) && isOverflow(element)) || (element.tabIndex >= 0)) return true;
}
/**
* Decide whether an element is a tag without href attribute or not.
*
* @function isAtagWithoutHref
* @param element {Node}
* @returns {boolean}
*/
function isAtagWithoutHref(element) {
return (element.tagName === 'A' && element.getAttribute('href') === null && element.getAttribute('tabIndex') === null);
}
/**
* Decide whether an element is actually disabled or not.
*
* @function isActuallyDisabled
* @param element {Node}
* @returns {boolean}
*
* @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled}
*/
function isActuallyDisabled(element) {
if (['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTGROUP', 'OPTION', 'FIELDSET'].includes(element.tagName)) return (element.disabled);
return false;
}
/**
* Decide whether the element is expressly inert or not.
* @see {@link https://html.spec.whatwg.org/multipage/interaction.html#expressly-inert}
* @function isExpresslyInert
* @param element {Node}
* @returns {boolean}
*/
function isExpresslyInert(element) {
return ((element.inert) && (!element.ownerDocument.documentElement.inert));
}
/**
* Decide whether the element is being rendered or not.
* 1. If an element has the style as "visibility: hidden | collapse" or "display: none", it is not being rendered.
* 2. If an element has the style as "opacity: 0", it is not being rendered.(that is, invisible).
* 3. If width and height of an element are explicitly set to 0, it is not being rendered.
* 4. If a parent element is hidden, an element itself is not being rendered.
* (CSS visibility property and display property are inherited.)
* @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered}
* @function isBeingRendered
* @param element {Node}
* @returns {boolean}
*/
function isBeingRendered(element) {
if (!isVisibleStyleProperty(element.parentElement)) return false;
if (!isVisibleStyleProperty(element) || (element.style.opacity === '0')
|| (window.getComputedStyle(element).height === '0px' || window.getComputedStyle(element).width === '0px')) return false;
return true;
}
/**
* Decide whether this element is partially or completely visible to user agent.
* @function isVisible
* @param element {Node}
* @returns {boolean}
*/
function isVisible(element) {
return (!element.parentElement) || (isVisibleStyleProperty(element) && hitTest(element));
}
/**
* Decide whether this element is completely visible in this viewport for the arrow direction.
* @function isEntirelyVisible
* @param element {Node}
* @returns {boolean}
*/
function isEntirelyVisible(element, container) {
const rect = getBoundingClientRect(element);
const containerElm = container || element.getSpatialNavigationContainer();
const containerRect = getBoundingClientRect(containerElm);
// FIXME: when element is bigger than container?
const entirelyVisible = !((rect.left < containerRect.left)
|| (rect.right > containerRect.right)
|| (rect.top < containerRect.top)
|| (rect.bottom > containerRect.bottom));
return entirelyVisible;
}
/**
* Decide the style property of this element is specified whether it's visible or not.
* @function isVisibleStyleProperty
* @param element {CSSStyleDeclaration}
* @returns {boolean}
*/
function isVisibleStyleProperty(element) {
const elementStyle = window.getComputedStyle(element, null);
const thisVisibility = elementStyle.getPropertyValue('visibility');
const thisDisplay = elementStyle.getPropertyValue('display');
const invisibleStyle = ['hidden', 'collapse'];
return (thisDisplay !== 'none' && !invisibleStyle.includes(thisVisibility));
}
/**
* Decide whether this element is entirely or partially visible within the viewport.
* @function hitTest
* @param element {Node}
* @returns {boolean}
*/
function hitTest(element) {
const elementRect = getBoundingClientRect(element);
if (element.nodeName !== 'IFRAME' && (elementRect.top < 0 || elementRect.left < 0
|| elementRect.top > element.ownerDocument.documentElement.clientHeight || elementRect.left > element.ownerDocument.documentElement.clientWidth)) return false;
let offsetX = parseInt(element.offsetWidth) / 10;
let offsetY = parseInt(element.offsetHeight) / 10;
offsetX = isNaN(offsetX) ? 1 : offsetX;
offsetY = isNaN(offsetY) ? 1 : offsetY;
const hitTestPoint = {
// For performance, just using the three point(middle, leftTop, rightBottom) of the element for hit testing
middle: [(elementRect.left + elementRect.right) / 2, (elementRect.top + elementRect.bottom) / 2],
leftTop: [elementRect.left + offsetX, elementRect.top + offsetY],
rightBottom: [elementRect.right - offsetX, elementRect.bottom - offsetY],
};
for (const point in hitTestPoint) {
const elemFromPoint = element.ownerDocument.elementFromPoint(...hitTestPoint[point]);
if (element === elemFromPoint || element.contains(elemFromPoint)) {
return true;
}
}
return false;
}
/**
* Decide whether a child element is entirely or partially Included within container visually.
* @function isInside
* @param containerRect {DOMRect}
* @param childRect {DOMRect}
* @returns {boolean}
*/
function isInside(containerRect, childRect) {
const rightEdgeCheck = (containerRect.left < childRect.right && containerRect.right >= childRect.right);
const leftEdgeCheck = (containerRect.left <= childRect.left && containerRect.right > childRect.left);
const topEdgeCheck = (containerRect.top <= childRect.top && containerRect.bottom > childRect.top);
const bottomEdgeCheck = (containerRect.top < childRect.bottom && containerRect.bottom >= childRect.bottom);
return (rightEdgeCheck || leftEdgeCheck) && (topEdgeCheck || bottomEdgeCheck);
}
/**
* Decide whether this element is entirely or partially visible within the viewport.
* Note: rect1 is outside of rect2 for the dir
* @function isOutside
* @param rect1 {DOMRect}
* @param rect2 {DOMRect}
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {boolean}
*/
function isOutside(rect1, rect2, dir) {
switch (dir) {
case 'left':
return isRightSide(rect2, rect1);
case 'right':
return isRightSide(rect1, rect2);
case 'up':
return isBelow(rect2, rect1);
case 'down':
return isBelow(rect1, rect2);
default:
return false;
}
}
/* rect1 is right of rect2 */
function isRightSide(rect1, rect2) {
return rect1.left >= rect2.right || (rect1.left >= rect2.left && rect1.right > rect2.right && rect1.bottom > rect2.top && rect1.top < rect2.bottom);
}
/* rect1 is below of rect2 */
function isBelow(rect1, rect2) {
return rect1.top >= rect2.bottom || (rect1.top >= rect2.top && rect1.bottom > rect2.bottom && rect1.left < rect2.right && rect1.right > rect2.left);
}
/* rect1 is completely aligned or partially aligned for the direction */
function isAligned(rect1, rect2, dir) {
switch (dir) {
case 'left':
/* falls through */
case 'right':
return rect1.bottom > rect2.top && rect1.top < rect2.bottom;
case 'up':
/* falls through */
case 'down':
return rect1.right > rect2.left && rect1.left < rect2.right;
default:
return false;
}
}
/**
* Get distance between the search origin and a candidate element along the direction when candidate element is inside the search origin.
* @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance}
* @function getDistanceFromPoint
* @param point {Point} - The search origin
* @param element {DOMRect} - A candidate element
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {Number} The euclidian distance between the spatial navigation container and an element inside it
*/
function getDistanceFromPoint(point, element, dir) {
point = startingPoint;
// Get exit point, entry point -> {x: '', y: ''};
const points = getEntryAndExitPoints(dir, point, element);
// Find the points P1 inside the border box of starting point and P2 inside the border box of candidate
// that minimize the distance between these two points
const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x);
const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y);
// The result is euclidian distance between P1 and P2.
return Math.sqrt(P1 ** 2 + P2 ** 2);
}
/**
* Get distance between the search origin and a candidate element along the direction when candidate element is inside the search origin.
* @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance}
* @function getInnerDistance
* @param rect1 {DOMRect} - The search origin
* @param rect2 {DOMRect} - A candidate element
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {Number} The euclidean distance between the spatial navigation container and an element inside it
*/
function getInnerDistance(rect1, rect2, dir) {
const baseEdgeForEachDirection = {
left: 'right', right: 'left', up: 'bottom', down: 'top',
};
const baseEdge = baseEdgeForEachDirection[dir];
return Math.abs(rect1[baseEdge] - rect2[baseEdge]);
}
/**
* Get the distance between the search origin and a candidate element considering the direction.
* @see {@link https://drafts.csswg.org/css-nav-1/#calculating-the-distance}
* @function getDistance
* @param searchOrigin {DOMRect | Point} - The search origin
* @param candidateRect {DOMRect} - A candidate element
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {Number} The distance scoring between two elements
*/
function getDistance(searchOrigin, candidateRect, dir) {
const kOrthogonalWeightForLeftRight = 30;
const kOrthogonalWeightForUpDown = 2;
let orthogonalBias = 0;
let alignBias = 0;
const alignWeight = 5.0;
// Get exit point, entry point -> {x: '', y: ''};
const points = getEntryAndExitPoints(dir, searchOrigin, candidateRect);
// Find the points P1 inside the border box of starting point and P2 inside the border box of candidate
// that minimize the distance between these two points
const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x);
const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y);
// A: The euclidean distance between P1 and P2.
const A = Math.sqrt(P1 ** 2 + P2 ** 2);
let B; let
C;
// B: The absolute distance in the direction which is orthogonal to dir between P1 and P2, or 0 if dir is null.
// C: The intersection edges between a candidate and the starting point.
// D: The square root of the area of intersection between the border boxes of candidate and starting point
const intersectionRect = getIntersectionRect(searchOrigin, candidateRect);
const D = intersectionRect.area;
switch (dir) {
case 'left':
/* falls through */
case 'right':
// If two elements are aligned, add align bias
// else, add orthogonal bias
if (isAligned(searchOrigin, candidateRect, dir)) alignBias = Math.min(intersectionRect.height / searchOrigin.height, 1);
else orthogonalBias = (searchOrigin.height / 2);
B = (P2 + orthogonalBias) * kOrthogonalWeightForLeftRight;
C = alignWeight * alignBias;
break;
case 'up':
/* falls through */
case 'down':
// If two elements are aligned, add align bias
// else, add orthogonal bias
if (isAligned(searchOrigin, candidateRect, dir)) alignBias = Math.min(intersectionRect.width / searchOrigin.width, 1);
else orthogonalBias = (searchOrigin.width / 2);
B = (P1 + orthogonalBias) * kOrthogonalWeightForUpDown;
C = alignWeight * alignBias;
break;
default:
B = 0;
C = 0;
break;
}
return (A + B - C - D);
}
/**
* Get the euclidean distance between the search origin and a candidate element considering the direction.
* @function getEuclideanDistance
* @param rect1 {DOMRect} - The search origin
* @param rect2 {DOMRect} - A candidate element
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {Number} The distance scoring between two elements
*/
function getEuclideanDistance(rect1, rect2, dir) {
// Get exit point, entry point
const points = getEntryAndExitPoints(dir, rect1, rect2);
// Find the points P1 inside the border box of starting point and P2 inside the border box of candidate
// that minimize the distance between these two points
const P1 = Math.abs(points.entryPoint.x - points.exitPoint.x);
const P2 = Math.abs(points.entryPoint.y - points.exitPoint.y);
// Return the euclidean distance between P1 and P2.
return Math.sqrt(P1 ** 2 + P2 ** 2);
}
/**
* Get the absolute distance between the search origin and a candidate element considering the direction.
* @function getAbsoluteDistance
* @param rect1 {DOMRect} - The search origin
* @param rect2 {DOMRect} - A candidate element
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD)
* @returns {Number} The distance scoring between two elements
*/
function getAbsoluteDistance(rect1, rect2, dir) {
// Get exit point, entry point
const points = getEntryAndExitPoints(dir, rect1, rect2);
// Return the absolute distance in the dir direction between P1 and P.
return ((dir === 'left') || (dir === 'right'))
? Math.abs(points.entryPoint.x - points.exitPoint.x) : Math.abs(points.entryPoint.y - points.exitPoint.y);
}
/**
* Get entry point and exit point of two elements considering the direction.
* @function getEntryAndExitPoints
* @param dir {SpatialNavigationDirection} - The directional information for the spatial navigation (e.g. LRUD). Default value for dir is 'down'.
* @param searchOrigin {DOMRect | Point} - The search origin which contains the exit point
* @param candidateRect {DOMRect} - One of candidates which contains the entry point
* @returns {Points} The exit point from the search origin and the entry point from a candidate
*/
function getEntryAndExitPoints(dir = 'down', searchOrigin, candidateRect) {
/**
* User type definition for Point
* @typeof {Object} Points
* @property {Point} Points.entryPoint
* @property {Point} Points.exitPoint
*/
const points = { entryPoint: { x: 0, y: 0 }, exitPoint: { x: 0, y: 0 } };
if (startingPoint) {
points.exitPoint = searchOrigin;
switch (dir) {
case 'left':
points.entryPoint.x = candidateRect.right;
break;
case 'up':
points.entryPoint.y = candidateRect.bottom;
break;
case 'right':
points.entryPoint.x = candidateRect.left;
break;
case 'down':
points.entryPoint.y = candidateRect.top;
break;
}
// Set orthogonal direction
switch (dir) {
case 'left':
case 'right':
if (startingPoint.y <= candidateRect.top) {
points.entryPoint.y = candidateRect.top;
} else if (startingPoint.y < candidateRect.bottom) {
points.entryPoint.y = startingPoint.y;
} else {
points.entryPoint.y = candidateRect.bottom;
}
break;
case 'up':
case 'down':
if (startingPoint.x <= candidateRect.left) {
points.entryPoint.x = candidateRect.left;
} else if (startingPoint.x < candidateRect.right) {
points.entryPoint.x = startingPoint.x;
} else {
points.entryPoint.x = candidateRect.right;
}
break;
}
} else {
// Set direction
switch (dir) {
case 'left':
points.exitPoint.x = searchOrigin.left;
points.entryPoint.x = (candidateRect.right < searchOrigin.left) ? candidateRect.right : searchOrigin.left;
break;
case 'up':
points.exitPoint.y = searchOrigin.top;
points.entryPoint.y = (candidateRect.bottom < searchOrigin.top) ? candidateRect.bottom : searchOrigin.top;
break;
case 'right':
points.exitPoint.x = searchOrigin.right;
points.entryPoint.x = (candidateRect.left > searchOrigin.right) ? candidateRect.left : searchOrigin.right;
break;
case 'down':
points.exitPoint.y = searchOrigin.bottom;
points.entryPoint.y = (candidateRect.top > searchOrigin.bottom) ? candidateRect.top : searchOrigin.bottom;
break;
}
// Set orthogonal direction
switch (dir) {
case 'left':
case 'right':
if (isBelow(searchOrigin, candidateRect)) {
points.exitPoint.y = searchOrigin.top;
points.entryPoint.y = (candidateRect.bottom < searchOrigin.top) ? candidateRect.bottom : searchOrigin.top;
} else if (isBelow(candidateRect, searchOrigin)) {
points.exitPoint.y = searchOrigin.bottom;
points.entryPoint.y = (candidateRect.top > searchOrigin.bottom) ? candidateRect.top : searchOrigin.bottom;
} else {
points.exitPoint.y = Math.max(searchOrigin.top, candidateRect.top);
points.entryPoint.y = points.exitPoint.y;
}
break;
case 'up':
case 'down':
if (isRightSide(searchOrigin, candidateRect)) {
points.exitPoint.x = searchOrigin.left;
points.entryPoint.x = (candidateRect.right < searchOrigin.left) ? candidateRect.right : searchOrigin.left;
} else if (isRightSide(candidateRect, searchOrigin)) {
points.exitPoint.x = searchOrigin.right;
points.entryPoint.x = (candidateRect.left > searchOrigin.right) ? candidateRect.left : searchOrigin.right;
} else {
points.exitPoint.x = Math.max(searchOrigin.left, candidateRect.left);
points.entryPoint.x = points.exitPoint.x;
}
break;
}
}
return points;
}
/**
* Find focusable elements within the container
* @see {@link https://drafts.csswg.org/css-nav-1/#find-the-shortest-distance}
* @function getIntersectionRect
* @param rect1 {DOMRect} - The search origin which contains the exit point
* @param rect2 {DOMRect} - One of candidates which contains the entry point
* @returns {IntersectionArea} The intersection area between two elements.
*
* @typeof {Object} IntersectionArea
* @property {Number} IntersectionArea.width
* @property {Number} IntersectionArea.height
*/
function getIntersectionRect(rect1, rect2) {
const intersection_rect = { width: 0, height: 0, area: 0 };
const new_location = [Math.max(rect1.left, rect2.left), Math.max(rect1.top, rect2.top)];
const new_max_point = [Math.min(rect1.right, rect2.right), Math.min(rect1.bottom, rect2.bottom)];
intersection_rect.width = Math.abs(new_location[0] - new_max_point[0]);
intersection_rect.height = Math.abs(new_location[1] - new_max_point[1]);
if (!(new_location[0] >= new_max_point[0] || new_location[1] >= new_max_point[1])) {
// intersecting-cases
intersection_rect.area = Math.sqrt(intersection_rect.width * intersection_rect.height);
}
return intersection_rect;
}
/**
* Handle the spatial navigation behavior for HTMLInputElement, HTMLTextAreaElement
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input|HTMLInputElement (MDN)}
* @function handlingEditableElement
* @param e {Event} - keydownEvent
* @returns {boolean}
*/
function handlingEditableElement(e) {
const SPINNABLE_INPUT_TYPES = ['email', 'date', 'month', 'number', 'time', 'week'];
const TEXT_INPUT_TYPES = ['password', 'text', 'search', 'tel', 'url', null];
const eventTarget = document.activeElement;
const startPosition = eventTarget.selectionStart;
const endPosition = eventTarget.selectionEnd;
const focusNavigableArrowKey = {
left: false, up: false, right: false, down: false,
};
const dir = ARROW_KEY_CODE[e.keyCode];
if (dir === undefined) {
return focusNavigableArrowKey;
}
if (SPINNABLE_INPUT_TYPES.includes(eventTarget.getAttribute('type'))
&& (dir === 'up' || dir === 'down')) {
focusNavigableArrowKey[dir] = true;
} else if (TEXT_INPUT_TYPES.includes(eventTarget.getAttribute('type')) || eventTarget.nodeName === 'TEXTAREA') {
if (startPosition === endPosition) { // if there isn't any selected text
if (startPosition === 0) {
focusNavigableArrowKey.left = true;
focusNavigableArrowKey.up = true;
}
if (endPosition === eventTarget.value.length) {
focusNavigableArrowKey.right = true;
focusNavigableArrowKey.down = true;
}
}
} else { // HTMLDataListElement, HTMLSelectElement, HTMLOptGroup
focusNavigableArrowKey[dir] = true;
}
return focusNavigableArrowKey;
}
/**
* Get the DOMRect of an element
* @function getBoundingClientRect
* @param {Node} element
* @returns {DOMRect}
*/
function getBoundingClientRect(element) {
// memoization
let rect = mapOfBoundRect && mapOfBoundRect.get(element);
if (!rect) {
const boundingClientRect = element.getBoundingClientRect();
rect = {
top: Number(boundingClientRect.top.toFixed(2)),
right: Number(boundingClientRect.right.toFixed(2)),
bottom: Number(boundingClientRect.bottom.toFixed(2)),
left: Number(boundingClientRect.left.toFixed(2)),
width: Number(boundingClientRect.width.toFixed(2)),
height: Number(boundingClientRect.height.toFixed(2)),
};
mapOfBoundRect && mapOfBoundRect.set(element, rect);
}
return rect;
}
/**
* Get the candidates which is fully inside the target element in visual
* @param {Node} targetElement
* @returns {sequence} overlappedCandidates
*/
function getOverlappedCandidates(targetElement) {
const container = targetElement.getSpatialNavigationContainer();
const candidates = container.focusableAreas();
const overlappedCandidates = [];
candidates.forEach((element) => {
if ((targetElement !== element) && isEntirelyVisible(element, targetElement)) {
overlappedCandidates.push(element);
}
});
return overlappedCandidates;
}
/**
* Get the list of the experimental APIs
* @function getExperimentalAPI
*/
function getExperimentalAPI() {
function canScroll(container, dir) {
return (isScrollable(container, dir) && !isScrollBoundary(container, dir))
|| (!container.parentElement && !isHTMLScrollBoundary(container, dir));
}
function findTarget(findCandidate, element, dir, option) {
let eventTarget = element;
let bestNextTarget = null;
// 4
if (eventTarget === document || eventTarget === document.documentElement) {
eventTarget = document.body || document.documentElement;
}
// 5
// At this point, spatialNavigationSearch can be applied.
// If startingPoint is either a scroll container or the document,
// find the best candidate within startingPoint
if ((isContainer(eventTarget) || eventTarget.nodeName === 'BODY') && !(eventTarget.nodeName === 'INPUT')) {
if (eventTarget.nodeName === 'IFRAME') eventTarget = eventTarget.contentDocument.body;
const candidates = getSpatialNavigationCandidates(eventTarget, option);
// 5-2
if (Array.isArray(candidates) && candidates.length > 0) {
return findCandidate ? getFilteredSpatialNavigationCandidates(eventTarget, dir, candidates) : eventTarget.spatialNavigationSearch(dir, { candidates });
}
if (canScroll(eventTarget, dir)) {
return findCandidate ? [] : eventTarget;
}
}
// 6
// Let container be the nearest ancestor of eventTarget
let container = eventTarget.getSpatialNavigationContainer();
let parentContainer = (container.parentElement) ? container.getSpatialNavigationContainer() : null;
// When the container is the viewport of a browsing context
if (!parentContainer && (window.location !== window.parent.location)) {
parentContainer = window.parent.document.documentElement;
}
// 7
while (parentContainer) {
const candidates = filteredCandidates(eventTarget, getSpatialNavigationCandidates(container, option), dir, container);
if (Array.isArray(candidates) && candidates.length > 0) {
bestNextTarget = eventTarget.spatialNavigationSearch(dir, { candidates, container });
if (bestNextTarget) {
return findCandidate ? candidates : bestNextTarget;
}
}
// If there isn't any candidate and the best candidate among candidate:
// 1) Scroll or 2) Find candidates of the ancestor container
// 8 - if
else if (canScroll(container, dir)) {
return findCandidate ? [] : eventTarget;
} else if (container === document || container === document.documentElement) {
container = window.document.documentElement;
// The page is in an iframe
if (window.location !== window.parent.location) {
// eventTarget needs to be reset because the position of the element in the IFRAME
// is unuseful when the focus moves out of the iframe
eventTarget = window.frameElement;
container = window.parent.document.documentElement;
if (container.parentElement) parentContainer = container.getSpatialNavigationContainer();
else {
parentContainer = null;
break;
}
}
} else {
// avoiding when spatnav container with tabindex=-1
if (isFocusable(container)) {
eventTarget = container;
}
container = parentContainer;
if (container.parentElement) parentContainer = container.getSpatialNavigationContainer();
else {
parentContainer = null;
break;
}
}
}
if (!parentContainer && container) {
// Getting out from the current spatnav container
const candidates = filteredCandidates(eventTarget, getSpatialNavigationCandidates(container, option), dir, container);
// 9
if (Array.isArray(candidates) && candidates.length > 0) {
bestNextTarget = eventTarget.spatialNavigationSearch(dir, { candidates, container });
if (bestNextTarget) {
return findCandidate ? candidates : bestNextTarget;
}
}
}
if (canScroll(container, dir)) {
bestNextTarget = eventTarget;
return bestNextTarget;
}
}
return {
isContainer,
isScrollContainer,
isVisibleInScroller,
findCandidates: findTarget.bind(null, true),
findNextTarget: findTarget.bind(null, false),
getDistanceFromTarget: (element, candidateElement, dir) => {
if ((isContainer(element) || element.nodeName === 'BODY') && !(element.nodeName === 'INPUT')) {
if (getSpatialNavigationCandidates(element).includes(candidateElement)) {
return getInnerDistance(getBoundingClientRect(element), getBoundingClientRect(candidateElement), dir);
}
}
return getDistance(getBoundingClientRect(element), getBoundingClientRect(candidateElement), dir);
},
};
}
/**
* Makes to use the experimental APIs.
* @function enableExperimentalAPIs
* @param option {boolean} - If it is true, the experimental APIs can be used or it cannot.
*/
function enableExperimentalAPIs(option) {
const currentKeyMode = window.__spatialNavigation__ && window.__spatialNavigation__.keyMode;
window.__spatialNavigation__ = (option === false) ? getInitialAPIs() : Object.assign(getInitialAPIs(), getExperimentalAPI());
window.__spatialNavigation__.keyMode = currentKeyMode;
Object.seal(window.__spatialNavigation__);
}
/**
* Set the environment for using the spatial navigation polyfill.
* @function getInitialAPIs
*/
function getInitialAPIs() {
return {
enableExperimentalAPIs,
get keyMode() { return this._keymode ? this._keymode : 'ARROW'; },
set keyMode(mode) { this._keymode = (['SHIFTARROW', 'ARROW', 'NONE'].includes(mode)) ? mode : 'ARROW'; },
setStartingPoint(x, y) { startingPoint = (x && y) ? { x, y } : null; },
};
}
initiateSpatialNavigation();
enableExperimentalAPIs(false);
window.addEventListener('load', () => {
spatialNavigationHandler();
});
}());
================================================
FILE: frontend/js/ui.js
================================================
import packageJSON from '../../package.json';
function logIt(message) {
const textareaConsoleLog = document.getElementById('textareaConsoleLog');
console.log(message);
textareaConsoleLog.value += `${message}\n`;
}
/* eslint-disable func-names */
window.switchView = function (view) {
const service = document.getElementById('service');
const settings = document.getElementById('settings');
const logs = document.getElementById('logs');
const about = document.getElementById('about');
const btnservice = document.getElementById('btnNavService');
const btnsettings = document.getElementById('btnNavSettings');
const btnlogs = document.getElementById('btnNavLogs');
const btnabout = document.getElementById('btnNavAbout');
const settingItemsAdv = document.getElementById('settingItemsAdv');
const settingItemsNormal = document.getElementById('settingItemsNormal');
const btnAdvanced = document.getElementById('btnSettingsAdvanced');
switch (view) {
case 'service':
service.style.display = 'block';
btnservice.style.background = 'white';
btnservice.style.color = 'black';
settings.style.display = 'none';
btnsettings.style.background = null;
btnsettings.style.color = null;
logs.style.display = 'none';
btnlogs.style.background = null;
btnlogs.style.color = null;
about.style.display = 'none';
btnabout.style.background = null;
btnabout.style.color = null;
break;
case 'settings':
service.style.display = 'none';
btnservice.style.background = null;
btnservice.style.color = null;
settings.style.display = 'block';
btnsettings.style.background = 'white';
btnsettings.style.color = 'black';
logs.style.display = 'none';
btnlogs.style.background = null;
btnlogs.style.color = null;
about.style.display = 'none';
btnabout.style.background = null;
btnabout.style.color = null;
// Open non advanced page
btnAdvanced.style.background = null;
btnAdvanced.style.color = null;
settingItemsNormal.style.display = 'block';
settingItemsAdv.style.display = 'none';
break;
case 'logs':
service.style.display = 'none';
btnservice.style.background = null;
btnservice.style.color = null;
settings.style.display = 'none';
btnsettings.style.background = null;
btnsettings.style.color = null;
logs.style.display = 'block';
btnlogs.style.background = 'white';
btnlogs.style.color = 'black';
about.style.display = 'none';
btnabout.style.background = null;
btnabout.style.color = null;
break;
case 'about':
service.style.display = 'none';
btnservice.style.background = null;
btnservice.style.color = null;
settings.style.display = 'none';
btnsettings.style.background = null;
btnsettings.style.color = null;
logs.style.display = 'none';
btnlogs.style.background = null;
btnlogs.style.color = null;
about.style.display = 'block';
btnabout.style.background = 'white';
btnabout.style.color = 'black';
break;
default:
service.style.display = null;
btnservice.style.background = null;
btnservice.style.color = null;
settings.style.display = null;
logs.style.display = null;
about.style.display = null;
break;
}
};
window.resolutionChanged = function (elem) {
document.getElementById('manualres').style.display = elem.value === 'manual' ? 'inline' : 'none';
};
window.socketCheckChanged = function (elem) {
if (elem.checked === true) {
document.getElementById('settingaddressport').style.display = 'none';
document.getElementById('settingsocket').style.display = 'flex';
} else {
document.getElementById('settingaddressport').style.display = 'flex';
document.getElementById('settingsocket').style.display = 'none';
}
};
window.socketSelectChanged = function (elem) {
document.getElementById('manualsocket').style.display = elem.value === 'manual' ? 'inline' : 'none';
};
window.toggleAdvanced = function () {
const settingItemsAdv = document.getElementById('settingItemsAdv');
const settingItemsNormal = document.getElementById('settingItemsNormal');
const btnAdvanced = document.getElementById('btnSettingsAdvanced');
if (settingItemsNormal.style.display === 'block') {
btnAdvanced.style.background = 'white';
btnAdvanced.style.color = 'black';
settingItemsNormal.style.display = 'none';
settingItemsAdv.style.display = 'block';
} else {
btnAdvanced.style.background = null;
btnAdvanced.style.color = null;
settingItemsNormal.style.display = 'block';
settingItemsAdv.style.display = 'none';
}
};
window.switchLog = function (location) {
const divConsoleLog = document.getElementById('consoleLog');
const divHyperionLog = document.getElementById('hyperionLog');
const btnLogSwitchPicCap = document.getElementById('btnLogSwitchPicCap');
const btnLogSwitchHyperion = document.getElementById('btnLogSwitchHyperion');
if (location === 'hyperion') {
divConsoleLog.style.display = 'none';
divHyperionLog.style.display = 'block';
btnLogSwitchHyperion.style.background = 'white';
btnLogSwitchHyperion.style.color = 'black';
btnLogSwitchPicCap.style.background = null;
btnLogSwitchPicCap.style.color = null;
} else {
divConsoleLog.style.display = 'block';
divHyperionLog.style.display = 'none';
btnLogSwitchPicCap.style.background = 'white';
btnLogSwitchPicCap.style.color = 'black';
btnLogSwitchHyperion.style.background = null;
btnLogSwitchHyperion.style.color = null;
}
};
/* eslint-enable func-names */
function saveLightMode(color) {
logIt(`Saving ${color} as light mode.`);
localStorage.setItem('lightMode', color);
}
/* eslint-disable func-names */
window.switchLightMode = function (color) {
const btnLightBlue = document.getElementById('btnLightBlue');
const btnLightDark = document.getElementById('btnLightDark');
const btnLightBlack = document.getElementById('btnLightBlack');
switch (color) {
case 'blue':
btnLightBlue.style.background = 'white';
btnLightBlue.style.color = 'black';
btnLightDark.style.background = null;
btnLightDark.style.color = null;
btnLightBlack.style.background = null;
btnLightBlack.style.color = null;
document.querySelectorAll('.darkMode, .blackMode').forEach((elem) => elem.classList.add('blueMode'));
document.querySelectorAll('.darkMode').forEach((elem) => elem.classList.remove('darkMode'));
document.querySelectorAll('.blackMode').forEach((elem) => elem.classList.remove('blackMode'));
saveLightMode('blue');
break;
case 'dark':
btnLightBlue.style.background = null;
btnLightBlue.style.color = null;
btnLightDark.style.background = 'white';
btnLightDark.style.color = 'black';
btnLightBlack.style.background = null;
btnLightBlack.style.color = null;
document.querySelectorAll('.blueMode, .blackMode').forEach((elem) => elem.classList.add('darkMode'));
document.querySelectorAll('.blueMode').forEach((elem) => elem.classList.remove('blueMode'));
document.querySelectorAll('.blackMode').forEach((elem) => elem.classList.remove('blackMode'));
saveLightMode('dark');
break;
case 'black':
btnLightBlue.style.background = null;
btnLightBlue.style.color = null;
btnLightDark.style.background = null;
btnLightDark.style.color = null;
btnLightBlack.style.background = 'white';
btnLightBlack.style.color = 'black';
document.querySelectorAll('.blueMode, .darkMode').forEach((elem) => elem.classList.add('blackMode'));
document.querySelectorAll('.blueMode').forEach((elem) => elem.classList.remove('blueMode'));
document.querySelectorAll('.darkMode').forEach((elem) => elem.classList.remove('darkMode'));
saveLightMode('black');
break;
default:
logIt(`${color} not found. Using blue as default.`);
/* eslint-disable no-undef */
switchLightMode('blue');
/* eslint-enable no-undef */
break;
}
};
function loadLightMode() {
const lightMode = localStorage.getItem('lightMode');
/* eslint-disable no-undef */
switchLightMode(lightMode);
/* eslint-enable no-undef */
}
function getJSON(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'json';
xhr.onload = function () {
const { status } = xhr;
if (status === 200) {
callback(null, xhr.response);
} else {
callback(status);
}
};
xhr.send();
}
/* eslint-enable func-names */
function getContributors(owner, repo) {
getJSON(`https://api.github.com/repos/${owner}/${repo}/contributors`, (err, data) => {
if (err != null) {
console.error(err);
} else {
const resp = data;
let div = document.querySelector('.hyperionwebosContributors');
if (repo === 'piccap') {
div = document.querySelector('.piccapContributors');
}
const users = resp.map((u) => u.login);
const avatars = resp.map((a) => a.avatar_url);
let count = 0;
let pos = 0;
let last = document.createElement('ul');
users.forEach((user) => {
const lielem = document.createElement('li');
lielem.setAttribute('id', `li${user}`);
const pelem = document.createElement('p');
const imgelem = document.createElement('img');
imgelem.setAttribute('src', avatars[pos]);
pos += 1;
pelem.appendChild(imgelem);
pelem.innerHTML += user;
lielem.appendChild(pelem);
if (count >= 3) {
const brelem = document.createElement('br');
div.appendChild(brelem);
last = document.createElement('ul');
div.appendChild(last);
count = 0;
}
count += 1;
last.appendChild(lielem);
div.appendChild(last);
});
}
});
}
getContributors('webosbrew', 'hyperion-webos');
getContributors('tbsniller', 'piccap');
window.addEventListener('load', () => {
/* eslint-disable no-undef */
switchView('service');
switchLog('piccap');
loadLightMode();
/* eslint-enable no-undef */
const piccapVersion = packageJSON.version;
document.getElementById('txtPicCapVersion').innerHTML = `v${piccapVersion}`;
});
================================================
FILE: frontend/webOSTVjs-1.2.4/LICENSE-2.0.txt
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: frontend/webOSTVjs-1.2.4/webOSTV-dev.js
================================================
window.webOSDev=function(e){var r={};function t(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,t),i.l=!0,i.exports}return t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:n})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(t.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var i in e)t.d(n,i,function(r){return e[r]}.bind(null,i));return n},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";function n(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r&&(n=n.filter((function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable}))),t.push.apply(t,n)}return t}function i(e){for(var r=1;r0&&void 0!==arguments[0]?arguments[0]:function(){},r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){},t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"";if(!this.cancelled){var i={};try{i=JSON.parse(n)}catch(e){i={errorCode:-1,errorText:n,returnValue:!1}}var o=i,u=o.errorCode,c=o.returnValue;u||!1===c?(i.returnValue=!1,r(i)):(i.returnValue=!0,e(i)),t(i),this.subscribe||this.cancel()}}},{key:"cancel",value:function(){this.cancelled=!0,null!==this.bridge&&(this.bridge.cancel(),this.bridge=null),this.ts&&c[this.ts]&&delete c[this.ts]}}])&&u(r.prototype,t),n&&u(r,n),e}(),s={BROWSER:"APP_BROWSER"},l=function(e){var r=e.id,t=void 0===r?"":r,n=e.params,i=void 0===n?{}:n,o=e.onSuccess,u=void 0===o?function(){}:o,c=e.onFailure,l=void 0===c?function(){}:c,d={id:t,params:i};s.BROWSER===t&&(d.params.target=i.target||"",d.params.fullMode=!0,d.id="com.webos.app.browser"),function(e){var r=e.parameters,t=e.onSuccess,n=e.onFailure;(new a).send({service:"luna://com.webos.applicationManager",method:"launch",parameters:r,onComplete:function(e){var r=e.returnValue,i=e.errorCode,o=e.errorText;return!0===r?t():n({errorCode:i,errorText:o})}})}({parameters:d,onSuccess:u,onFailure:l})},d=function(){var e={};if(window.PalmSystem&&""!==window.PalmSystem.launchParams)try{e=JSON.parse(window.PalmSystem.launchParams)}catch(e){console.error("JSON parsing error")}return e},f=function(){return window.PalmSystem&&window.PalmSystem.identifier?window.PalmSystem.identifier.split(" ")[0]:""};function v(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r&&(n=n.filter((function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable}))),t.push.apply(t,n)}return t}function p(e){for(var r=1;r0&&void 0!==arguments[0]?arguments[0]:function(){},r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};setTimeout((function(){return e(r)}),0)},j=function(e){return e.state===w&&""!==e.getClientId()},D=function(e,r){var t=r.errorCode,n=void 0===t?h.UNKNOWN_ERROR:t,i=r.errorText,o={errorCode:n,errorText:void 0===i?"Unknown error.":i};return e.setError(o),o},E={errorCode:h.CLIENT_NOT_LOADED,errorText:"DRM client is not loaded."},I=function(){function e(r){!function(e,r){if(!(e instanceof r))throw new TypeError("Cannot call a class as a function")}(this,e),this.clientId="",this.drmType=r,this.errorCode=h.NOT_ERROR,this.errorText="",this.state=O}var r,t,n;return r=e,(t=[{key:"getClientId",value:function(){return this.clientId}},{key:"getDrmType",value:function(){return this.drmType}},{key:"getErrorCode",value:function(){return this.errorCode}},{key:"getErrorText",value:function(){return this.errorText}},{key:"setError",value:function(e){var r=e.errorCode,t=e.errorText;this.errorCode=r,this.errorText=t}},{key:"isLoaded",value:function(e){var r=this,t=e.onSuccess,n=void 0===t?function(){}:t,i=e.onFailure,o=void 0===i?function(){}:i;S({method:"isLoaded",parameters:{appId:f()},onComplete:function(e){if(!0===e.returnValue){if(r.clientId=e.clientId||"",r.state=e.loadStatus?w:O,!0===e.loadStatus&&e.drmType!==r.drmType)return o(D(r,{errorCode:h.UNKNOWN_ERROR,errorText:"DRM types of set and loaded are not matched."}));var t=p({},e);return delete t.returnValue,n(t)}return o(D(r,e))}})}},{key:"load",value:function(e){var r=this,t=e.onSuccess,n=void 0===t?function(){}:t,i=e.onFailure,o=void 0===i?function(){}:i;if(this.state!==g&&this.state!==w){var u={appId:f(),drmType:this.drmType};this.state=g,S({method:"load",onComplete:function(e){return!0===e.returnValue?(r.clientId=e.clientId,r.state=w,n({clientId:r.clientId})):o(D(r,e))},parameters:u})}else T(n,{isLoaded:!0,clientId:this.clientId})}},{key:"unload",value:function(e){var r=this,t=e.onSuccess,n=void 0===t?function(){}:t,i=e.onFailure,o=void 0===i?function(){}:i;if(j(this)){var u={clientId:this.clientId};this.state=P,S({method:"unload",onComplete:function(e){return!0===e.returnValue?(r.clientId="",r.state=O,n()):o(D(r,e))},parameters:u})}else T(o,D(this,E))}},{key:"getRightsError",value:function(e){var r=this,t=e.onSuccess,n=void 0===t?function(){}:t,i=e.onFailure,o=void 0===i?function(){}:i;j(this)?S({method:"getRightsError",parameters:{clientId:this.clientId,subscribe:!0},onComplete:function(e){if(!0===e.returnValue){var t=p({},e);return delete t.returnValue,n(t)}return o(D(r,e))}}):T(o,D(this,E))}},{key:"sendDrmMessage",value:function(e){var r=this,t=e.msg,n=void 0===t?"":t,i=e.onSuccess,o=void 0===i?function(){}:i,u=e.onFailure,c=void 0===u?function(){}:u;if(j(this)){var a=function(e){var r="",t="";switch(e){case y.PLAYREADY:r="application/vnd.ms-playready.initiator+xml",t="urn:dvb:casystemid:19219";break;case y.WIDEVINE:r="application/widevine+xml",t="urn:dvb:casystemid:19156"}return{msgType:r,drmSystemId:t}}(this.drmType),s=p({clientId:this.clientId,msg:n},a);S({method:"sendDrmMessage",onComplete:function(e){if(!0===e.returnValue){var t=p({},e);return delete t.returnValue,o(t)}return c(D(r,e))},parameters:s})}else T(c,D(this,E))}}])&&m(r.prototype,t),n&&m(r,n),e}(),R={Error:h,Type:y},C=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return""===e?null:new I(e)};function N(e,r){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);r&&(n=n.filter((function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable}))),t.push.apply(t,n)}return t}function _(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[r]=t,e}var x=function(e){var r=e.service,t=e.subscribe,n=e.onSuccess,i=e.onFailure;(new a).send({service:r,method:"getStatus",parameters:{subscribe:t},onComplete:function(e){var r=function(e){for(var r=1;r-1&&(c="palm"),x({service:"luna://com.".concat(c,".connectionmanager"),subscribe:u,onSuccess:t,onFailure:i})}},k=function(e){var r=e.onSuccess,t=void 0===r?function(){}:r,n=e.onFailure,i=void 0===n?function(){}:n;-1!==navigator.userAgent.indexOf("Chrome")?(new a).send({service:"luna://com.webos.service.sm",method:"deviceid/getIDs",parameters:{idType:["LGUDID"]},onComplete:function(e){if(!0!==e.returnValue)i({errorCode:e.errorCode,errorText:e.errorText});else{var r=e.idList.filter((function(e){return"LGUDID"===e.idType}))[0].idValue;t({id:r})}}}):setTimeout((function(){return i({errorCode:"ERROR.000",errorText:"Not supported."})}),0)}}]);
================================================
FILE: frontend/webOSTVjs-1.2.4/webOSTV.js
================================================
window.webOS=function(e){var n={};function r(t){if(n[t])return n[t].exports;var o=n[t]={i:t,l:!1,exports:{}};return e[t].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=n,r.d=function(e,n,t){r.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,n){if(1&n&&(e=r(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(r.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)r.d(t,o,function(n){return e[n]}.bind(null,o));return t},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=1)}([function(e){e.exports=JSON.parse('{"name":"webostv-js","version":"1.2.4","description":"","main":"index.js","scripts":{"belazy":"npm run lint && npm run docs && npm run release","build":"node scripts/build.js","build:dev":"node scripts/build.js develop","clean":"git clean -xdf","docs":"jsdoc -c jsdoc.json","lint":"eslint . --cache","release":"node scripts/release.js","test":"node scripts/test.js app","test:mocha":"node scripts/test.js mocha"},"repository":{"type":"git","url":"http://mod.lge.com/hub/tvsdk/webostv-js.git"},"keywords":[],"author":"LGE TV Lab","license":"Apache-2.0","dependencies":{"address":"^1.0.3","archiver":"^4.0.1","chalk":"^2.4.1","command-exists":"^1.2.7","fs-extra":"^8.1.0","jsdoc":"^3.5.5","webpack":"^4.10.1","webpack-dev-server":"^3.1.4","webpack-merge":"^4.1.2"},"devDependencies":{"@babel/cli":"^7.10.1","@babel/core":"^7.10.2","@babel/polyfill":"^7.10.1","@babel/preset-env":"^7.10.2","babel-loader":"^8.1.0","eslint":"^4.19.1","eslint-config-airbnb-base":"^12.1.0","eslint-loader":"^2.0.0","eslint-plugin-import":"^2.12.0","html-webpack-plugin":"^4.3.0","mocha":"^5.2.0","mocha-loader":"^1.1.3"}}')},function(e,n,r){"use strict";r.r(n),r.d(n,"deviceInfo",(function(){return P})),r.d(n,"fetchAppId",(function(){return t})),r.d(n,"fetchAppInfo",(function(){return i})),r.d(n,"fetchAppRootPath",(function(){return s})),r.d(n,"keyboard",(function(){return x})),r.d(n,"libVersion",(function(){return D})),r.d(n,"platformBack",(function(){return a})),r.d(n,"platform",(function(){return V})),r.d(n,"service",(function(){return p})),r.d(n,"systemInfo",(function(){return k}));var t=function(){return window.PalmSystem&&window.PalmSystem.identifier?window.PalmSystem.identifier.split(" ")[0]:""},o={},i=function(e,n){if(0===Object.keys(o).length){var r=function(n,r){if(!n&&r)try{o=JSON.parse(r),e&&e(o)}catch(n){console.error("Unable to parse appinfo.json file for",t()),e&&e()}else e&&e()},i=new window.XMLHttpRequest;i.onreadystatechange=function(){4===i.readyState&&(i.status>=200&&i.status<300||0===i.status?r(null,i.responseText):r({status:404}))};try{i.open("GET",n||"appinfo.json",!0),i.send(null)}catch(e){r({status:404})}}else e&&e(o)},s=function(){var e=window.location.href;if("baseURI"in window.document)e=window.document.baseURI;else{var n=window.document.getElementsByTagName("base");n.length>0&&(e=n[0].href)}var r=e.match(new RegExp(".*://[^#]*/"));return r?r[0]:""},a=function(){if(window.PalmSystem&&window.PalmSystem.platformBack)return window.PalmSystem.platformBack()};function c(e,n){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);n&&(t=t.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),r.push.apply(r,t)}return r}function l(e){for(var n=1;n0&&void 0!==arguments[0]?arguments[0]:function(){},n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){},r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},t=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"";if(!this.cancelled){var o={};try{o=JSON.parse(t)}catch(e){o={errorCode:-1,errorText:t,returnValue:!1}}var i=o,s=i.errorCode,a=i.returnValue;s||!1===a?(o.returnValue=!1,n(o)):(o.returnValue=!0,e(o)),r(o),this.subscribe||this.cancel()}}},{key:"cancel",value:function(){this.cancelled=!0,null!==this.bridge&&(this.bridge.cancel(),this.bridge=null),this.ts&&f[this.ts]&&delete f[this.ts]}}])&&u(n.prototype,r),t&&u(n,t),e}(),p={request:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=l({service:e},n);return(new m).send(r)}};function v(e){return(v="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var w={};if("object"===("undefined"==typeof window?"undefined":v(window))&&window.PalmSystem){if(window.navigator.userAgent.indexOf("SmartWatch")>-1)w.watch=!0;else if(window.navigator.userAgent.indexOf("SmartTV")>-1||window.navigator.userAgent.indexOf("Large Screen")>-1)w.tv=!0;else{try{var b=JSON.parse(window.PalmSystem.deviceInfo||"{}");if(b.platformVersionMajor&&b.platformVersionMinor){var h=Number(b.platformVersionMajor),y=Number(b.platformVersionMinor);h<3||3===h&&y<=0?w.legacy=!0:w.open=!0}}catch(e){w.open=!0}window.Mojo=window.Mojo||{relaunch:function(){}},window.PalmSystem.stageReady&&window.PalmSystem.stageReady()}if(window.navigator.userAgent.indexOf("Chr0me")>-1||window.navigator.userAgent.indexOf("Chrome")>-1){var g=window.navigator.userAgent.indexOf("Chr0me")>-1?window.navigator.userAgent.indexOf("Chr0me"):window.navigator.userAgent.indexOf("Chrome"),S=window.navigator.userAgent.slice(g).indexOf(" "),O=window.navigator.userAgent.slice(g+7,g+S).split(".");w.chrome=Number(O[0])}else w.chrome=0}else w.unknown=!0;var V=w,j={},P=function(e){if(0===Object.keys(j).length){try{var n=JSON.parse(window.PalmSystem.deviceInfo);j.modelName=n.modelName,j.version=n.platformVersion,j.versionMajor=n.platformVersionMajor,j.versionMinor=n.platformVersionMinor,j.versionDot=n.platformVersionDot,j.sdkVersion=n.platformVersion,j.screenWidth=n.screenWidth,j.screenHeight=n.screenHeight}catch(e){j.modelName="webOS Device"}j.screenHeight=j.screenHeight||window.screen.height,j.screenWidth=j.screenWidth||window.screen.width,V.tv&&(j.uhd=!1,j.uhd8K=!1,j.hdr10=!1,j.dolbyVision=!1,j.dolbyAtmos=!1,(new m).send({service:"luna://com.webos.service.config",method:"getConfigs",parameters:{configNames:["tv.model.modelname","tv.nyx.platformVersion","tv.nyx.firmwareVersion","tv.hw.panelResolution","tv.hw.displayType","tv.hw.ddrSize","tv.model.supportHDR","tv.config.supportDolbyHDRContents","tv.config.supportDolbyTVATMOS","tv.model.supportTemp8K"]},onComplete:function(n){if(n.configs&&(j.modelName=n.configs["tv.model.modelname"]||j.modelName,j.sdkVersion=n.configs["tv.nyx.platformVersion"]||j.sdkVersion,j.uhd="UD"===n.configs["tv.hw.panelResolution"]||"8K"===n.configs["tv.hw.panelResolution"],j.uhd8K="8K"===n.configs["tv.hw.panelResolution"]||!0===n.configs["tv.model.supportTemp8K"],j.oled="OLED"===n.configs["tv.hw.displayType"],j.ddrSize=n.configs["tv.hw.ddrSize"],j.hdr10=!0===n.configs["tv.model.supportHDR"],j.dolbyVision=!0===n.configs["tv.config.supportDolbyHDRContents"],j.dolbyAtmos=!0===n.configs["tv.config.supportDolbyTVATMOS"],n.configs["tv.nyx.firmwareVersion"]&&"0.0.0"!==n.configs["tv.nyx.firmwareVersion"]||(n.configs["tv.nyx.firmwareVersion"]=n.configs["tv.nyx.platformVersion"]),n.configs["tv.nyx.firmwareVersion"])){j.version=n.configs["tv.nyx.firmwareVersion"];for(var r=j.version.split("."),t=["versionMajor","versionMinor","versionDot"],o=0;o