Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
099fe5a714 | ||
|
|
b63f2aee34 | ||
|
|
9d88a9ba38 | ||
|
|
bae0ef26fe | ||
|
|
f0438d7c88 | ||
|
|
f46649780c | ||
|
|
73edbaedb7 | ||
|
|
3b150fc540 | ||
|
|
7c96fbf427 | ||
|
|
fc31721ae0 | ||
|
|
fb841cce14 | ||
|
|
d5caffe9fd | ||
|
|
2442d4a681 | ||
|
|
ab0c1284fb | ||
|
|
55e528d95b | ||
|
|
ffcc9517f2 | ||
|
|
3ea4921861 | ||
|
|
1e91b7aa27 | ||
|
|
eb4b101f45 | ||
|
|
290e524901 | ||
|
|
4b010925ed | ||
|
|
910001f0e1 | ||
|
|
61628b1864 | ||
|
|
1a79ffd855 | ||
|
|
5423239e62 | ||
|
|
44a4fa9d2d | ||
|
|
26494e6844 | ||
|
|
4f8a9c307c | ||
|
|
a3860ebd69 | ||
|
|
b3a787d03d | ||
|
|
41e4c2d434 | ||
|
|
77cb962e6f | ||
|
|
c85055a222 | ||
|
|
81b4c30aa2 | ||
|
|
e6d8c537f2 | ||
|
|
0c51c1c1ff | ||
|
|
45ea309f3b |
@@ -0,0 +1,116 @@
|
|||||||
|
name: Report a bug in the application
|
||||||
|
description: I encountered a bug that was not reported before.
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for reporting unknown bugs only!
|
||||||
|
- [x] I promise to have read the [FAQ](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#faq) section in the hardware documentation first! 🤞
|
||||||
|
- [x] I promise to have read the [ISSUES](https://github.com/leukipp/touchkio?tab=readme-ov-file#issues) section and know how to debug the application! 🤞
|
||||||
|
- [x] I also swear to have searched for similar open and closed issues on [GITHUB](https://github.com/leukipp/touchkio/issues?q=is%3Aissue) first! 🤞
|
||||||
|
- type: textarea
|
||||||
|
id: problem_description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Problem description
|
||||||
|
placeholder: problem description
|
||||||
|
description: |
|
||||||
|
Describe the issue you are experiencing and how to reproduce it.
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Details
|
||||||
|
- type: input
|
||||||
|
id: app_version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What version of touchkio is used?
|
||||||
|
placeholder: v
|
||||||
|
description: >
|
||||||
|
Can be found on the MQTT device under `Diagnostics > Version`.
|
||||||
|
- type: input
|
||||||
|
id: operating_system
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What operating system and version is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi OS (Trixie), Ubuntu (24.04), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: display_server
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which display server is configured on the system?
|
||||||
|
description: >
|
||||||
|
Use `loginctl show-session $(loginctl show-user $(whoami) -p Display --value) -p Type --value` to find out.
|
||||||
|
options:
|
||||||
|
- Wayland
|
||||||
|
- X11
|
||||||
|
- Other
|
||||||
|
- type: input
|
||||||
|
id: system_hardware
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which system hardware is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi 5 (8GB RAM), Intel NUC 14 (16GB RAM), etc.
|
||||||
|
- type: input
|
||||||
|
id: screen_hardware
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which screen hardware is used?
|
||||||
|
description: >
|
||||||
|
Waveshare 8" CAPLCD (768x1024), Raspberry Pi 7" Touch Display 2 (720x1280), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: screen_connection
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which screen connection type is used?
|
||||||
|
description: >
|
||||||
|
Choose the connection type between your screen and the host machine.
|
||||||
|
options:
|
||||||
|
- DSI
|
||||||
|
- HDMI
|
||||||
|
- HDMI + USB
|
||||||
|
- Other
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Diagnostics
|
||||||
|
- type: textarea
|
||||||
|
id: arguments_json
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Used arguments
|
||||||
|
placeholder: arguments json
|
||||||
|
description: |
|
||||||
|
You can use `cat ~/.config/touchkio/Arguments.json` to get the file content.
|
||||||
|
render: json
|
||||||
|
- type: textarea
|
||||||
|
id: log_file
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Log file
|
||||||
|
placeholder: drag-and-drop the file or copy-and-paste the full content here
|
||||||
|
description: |
|
||||||
|
From the host machine use `scp USER@IP:~/.config/touchkio/logs/main.log main.log` to download the log file.
|
||||||
|
Alternatively, you can use `cat ~/.config/touchkio/logs/main.log` on a ssh connection to get the file content.
|
||||||
|
- type: textarea
|
||||||
|
id: additional_information
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
placeholder: additional information
|
||||||
|
description: |
|
||||||
|
If you have any additional information that might be useful, use this field.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: Request a new feature for the application
|
||||||
|
description: I want to request a new feature that is not yet available.
|
||||||
|
title: "[Feature]: "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for requesting new features only!
|
||||||
|
- [x] I promise to have read the [FAQ](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#faq) section in the hardware documentation first! 🤞
|
||||||
|
- [x] I also swear to have searched for similar open and closed feature requests on [GITHUB](https://github.com/leukipp/touchkio/issues?q=is%3Aissue) first! 🤞
|
||||||
|
- type: textarea
|
||||||
|
id: feature_description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Feature description
|
||||||
|
placeholder: feature description
|
||||||
|
description: |
|
||||||
|
Describe the feature you want to request and why it would be useful for others.
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Details
|
||||||
|
- type: input
|
||||||
|
id: app_version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What version of touchkio is used?
|
||||||
|
placeholder: v
|
||||||
|
description: >
|
||||||
|
Can be found on the MQTT device under `Diagnostics > Version`.
|
||||||
|
- type: input
|
||||||
|
id: operating_system
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What operating system and version is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi OS (Trixie), Ubuntu (24.04), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: display_server
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which display server is configured on the system?
|
||||||
|
description: >
|
||||||
|
Use `loginctl show-session $(loginctl show-user $(whoami) -p Display --value) -p Type --value` to find out.
|
||||||
|
options:
|
||||||
|
- Wayland
|
||||||
|
- X11
|
||||||
|
- Other
|
||||||
|
- type: input
|
||||||
|
id: system_hardware
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which system hardware is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi 5 (8GB RAM), Intel NUC 14 (16GB RAM), etc.
|
||||||
|
- type: input
|
||||||
|
id: screen_hardware
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which screen hardware is used?
|
||||||
|
description: >
|
||||||
|
Waveshare 8" CAPLCD (768x1024), Raspberry Pi 7" Touch Display 2 (720x1280), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: screen_connection
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which screen connection type is used?
|
||||||
|
description: >
|
||||||
|
Choose the connection type between your screen and the host machine.
|
||||||
|
options:
|
||||||
|
- DSI
|
||||||
|
- HDMI
|
||||||
|
- HDMI + USB
|
||||||
|
- Other
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: General inquiry or suggestion for the project
|
||||||
|
description: I want to ask a question or improve the documentation.
|
||||||
|
title: "[General]: "
|
||||||
|
labels: ["question"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
This issue form is for general inquiries only!
|
||||||
|
- [x] I promise that the `Report a bug in the application` issue form isn't suitable for my inquiry! 🤞
|
||||||
|
- [x] I promise that the `Request a new feature for the application` issue form isn't suitable for my inquiry! 🤞
|
||||||
|
- type: textarea
|
||||||
|
id: general_comment
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: General comment
|
||||||
|
placeholder: general comment
|
||||||
|
description: |
|
||||||
|
Ask a question, request some help, leave a general information or suggest an improvement for the documentation.
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Details (optional)
|
||||||
|
- type: input
|
||||||
|
id: app_version
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: What version of touchkio is used?
|
||||||
|
placeholder: v
|
||||||
|
description: >
|
||||||
|
Can be found on the MQTT device under `Diagnostics > Version`.
|
||||||
|
- type: input
|
||||||
|
id: operating_system
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: What operating system and version is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi OS (Trixie), Ubuntu (24.04), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: display_server
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Which display server is configured on the system?
|
||||||
|
description: >
|
||||||
|
Use `loginctl show-session $(loginctl show-user $(whoami) -p Display --value) -p Type --value` to find out.
|
||||||
|
options:
|
||||||
|
- Wayland
|
||||||
|
- X11
|
||||||
|
- Other
|
||||||
|
- type: input
|
||||||
|
id: system_hardware
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Which system hardware is used?
|
||||||
|
description: >
|
||||||
|
Raspberry Pi 5 (8GB RAM), Intel NUC 14 (16GB RAM), etc.
|
||||||
|
- type: input
|
||||||
|
id: screen_hardware
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Which screen hardware is used?
|
||||||
|
description: >
|
||||||
|
Waveshare 8" CAPLCD (768x1024), Raspberry Pi 7" Touch Display 2 (720x1280), etc.
|
||||||
|
- type: dropdown
|
||||||
|
id: screen_connection
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: Which screen connection type is used?
|
||||||
|
description: >
|
||||||
|
Choose the connection type between your screen and the host machine.
|
||||||
|
options:
|
||||||
|
- DSI
|
||||||
|
- HDMI
|
||||||
|
- HDMI + USB
|
||||||
|
- Other
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
+233
-51
@@ -17,75 +17,257 @@ If you are running Linux with a graphical user interface (Wayland or X11), you s
|
|||||||
| ⬛ | Not operational | The house is on fire. |
|
| ⬛ | Not operational | The house is on fire. |
|
||||||
|
|
||||||
### DSI
|
### DSI
|
||||||
| Device | System | Display | Status |
|
| Device | System | Display | Status |
|
||||||
| ---------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------ |
|
| ---------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------ |
|
||||||
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | ⬜ |
|
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display) | ⬜ |
|
||||||
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | ⬜ |
|
| Raspberry Pi 3 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2) | ⬜ |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | 🟩 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display) | 🟩 |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2) | 🟩 |
|
||||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) | 🟩 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 1 (800x480)](https://www.raspberrypi.com/products/raspberry-pi-touch-display) | 🟩 |
|
||||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2) | 🟩 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 10.1" DSI Touch (800x1280)](https://www.waveshare.com/10.1-dsi-touch-a.htm) | 🟩 |
|
||||||
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 12.3" DSI Touch (720x1920)](https://www.waveshare.com/12.3-dsi-touch-a.htm) | 🟩 |
|
||||||
|
|
||||||
### HDMI
|
### HDMI
|
||||||
| Device | System | Display | Status |
|
| Device | System | Display | Status |
|
||||||
| ------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------ |
|
| ------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN Touch Display 5" (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN Touch Display 5" (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 7EP-CAPLCD 7" (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 7EP-CAPLCD 7" (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare CAPLCD 8" (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare CAPLCD 8" (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [EVICIV 10.1" Touch (1920x1200)](https://de.aliexpress.com/item/1005001870058398.html?spm=a2g0o.order_list.order_list_main.10.21ef5c5fCTxEDm&gatewayAdapt=glo2deu) | 🟨 |
|
||||||
| Raspberry Pi CM 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Lenovo ThinkCentre 23.8" (1920x1080)](https://psref.lenovo.com/Detail/ThinkCentre_Tiny_In_One_24_Gen_5?M=12NBGAT1EU) | 🟨 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 |
|
||||||
| Raspberry Pi 400 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [UPerfect Vertical Touch 15.6" (1920x1080)](https://uperfect.com/products/uperfect-y-vertical-monitor-15-6) | 🟨 |
|
| Raspberry Pi CM 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Lenovo ThinkCentre 23.8" (1920x1080)](https://psref.lenovo.com/Detail/ThinkCentre_Tiny_In_One_24_Gen_5?M=12NBGAT1EU) | 🟨 |
|
||||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi Capacitive Touch 10.1" (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
|
| Raspberry Pi 400 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [UPerfect Vertical Touch 15.6" (1920x1080)](https://uperfect.com/products/uperfect-y-vertical-monitor-15-6) | 🟨 |
|
||||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [ELECROW Portable Monitor 10.1" (1280x800)](https://www.amazon.co.uk/dp/B0BHHQLKPY) | 🟨 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi Capacitive Touch 10.1" (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
|
||||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare Capacitive Touch 14" (2160x1440)](https://www.waveshare.com/14inch-2160x1440-lcd.htm) | 🟨 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [ELECROW Portable Monitor 10.1" (1280x800)](https://www.amazon.co.uk/dp/B0BHHQLKPY) | 🟨 |
|
||||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Akntzcs Portable Touch HD 16" (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare Capacitive Touch 14" (2160x1440)](https://www.waveshare.com/14inch-2160x1440-lcd.htm) | 🟨 |
|
||||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Prechen Portable Touch FHD 18.5" (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Akntzcs Portable Touch HD 16" (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
|
||||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Prechen Portable Touch FHD 18.5" (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 |
|
||||||
| Generic PC (x64) | Debian KDE (64-bit), Wayland, X11 | Generic Non-Touch | 🟧 |
|
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
|
||||||
| Generic PC (x64) | Ubuntu XFCE (64-bit), X11 | Generic Non-Touch | 🟧 |
|
| Generic PC (x64) | Debian KDE (64-bit), Wayland, X11 | Generic Non-Touch | 🟧 |
|
||||||
| Generic PC (x64) | Ubuntu GNOME (64-bit), X11 | Generic Non-Touch | 🟧 |
|
| Generic PC (x64) | Ubuntu XFCE (64-bit), X11 | Generic Non-Touch | 🟧 |
|
||||||
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 |
|
| Generic PC (x64) | Ubuntu GNOME (64-bit), X11 | Generic Non-Touch | 🟧 |
|
||||||
|
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 |
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
**Minimal features** are designed to run on any system without issues:
|
**Minimal features** are designed to run on any system without issues:
|
||||||
|
|
||||||
- A webview kiosk window launched in fullscreen mode and loading the specified `--web-url` website should not cause any problems.
|
- A webview kiosk window launched in fullscreen mode and loading the specified `--web-url` website should not cause any problems.
|
||||||
- When connecting to an internal HTTPS site with a self-signed certificate, ensure the specified FQDN matches. Using the `--ignore-certificate-errors` flag is [not recommended](https://github.com/leukipp/touchkio/issues/76) for browsing external sites.
|
|
||||||
|
|
||||||
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
|
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
|
||||||
|
|
||||||
- The following commands are currently implemented to modify the display status. Make sure that one of these works for your display when you run it directly on the terminal, otherwise the MQTT switch will not work either.
|
|
||||||
- `wlopm --[on,off] \*` (Raspberry Pi OS, Wayland)
|
|
||||||
- `kscreen-doctor --dpms [on,off]` (Debian KDE, Wayland)
|
|
||||||
- `xset dpms force [on,off]` (Generic, X11)
|
|
||||||
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
|
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
|
||||||
- On some Debian based systems (e.g. Ubuntu GNOME), the display status control is only available when using X11 (`xset`).
|
|
||||||
- There are certain HDMI screens (e.g. Viewsonic) where the display status command doesn't work as expected.
|
|
||||||
- The screen [immediately turns on again](https://github.com/leukipp/touchkio/issues/38), which may be caused by incompatible HDMI cables being used.
|
|
||||||
- On Raspberry Pi OS the display command may fail with `ERROR: Setting power mode for output '[DSI-*,HDMI-*]' failed`.
|
|
||||||
- This can happen if you have `wayvnc` running for remote access and is caused by a [known bug](https://github.com/leukipp/touchkio/issues/78).
|
|
||||||
- Certain commands from the MQTT integration may require elevated privileges to work correctly.
|
|
||||||
- Ensure that your local user has the [necessary permissions](https://github.com/leukipp/touchkio/issues/39) to run `sudo` without being prompted for a password.
|
|
||||||
|
|
||||||
Hardware support is verified during application startup and can be seen in the terminal or in the log file under the `Supported` section.
|
Hardware support is verified during application startup and can be checked in the terminal or in the log file under the `Supported` section.
|
||||||
The necessary requirements for MQTT sensors to work are listed here:
|
The necessary requirements for MQTT sensors to work are listed here:
|
||||||
| Name | Requirements | References |
|
| Name | Requirements | References |
|
||||||
| -------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
| -------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
| App (Update) | Requires `sudo` rights, `.deb` install and `touchkio.service` running . | [#70](https://github.com/leukipp/touchkio/issues/70), [#77](https://github.com/leukipp/touchkio/issues/77) |
|
| App (Update) | Requires `sudo` rights, `.deb` install and `touchkio.service` running . | [#70](https://github.com/leukipp/touchkio/issues/70), [#77](https://github.com/leukipp/touchkio/issues/77) |
|
||||||
| Display (Status) | Working `wlopm`, `kscreen-doctor` or `xset` command. | [#38](https://github.com/leukipp/touchkio/issues/38), [#57](https://github.com/leukipp/touchkio/issues/57) |
|
| Display (Status) | Working `wlopm`, `kscreen-doctor` or `xset` command. | [#38](https://github.com/leukipp/touchkio/issues/38), [#57](https://github.com/leukipp/touchkio/issues/57) |
|
||||||
| Display (Brightness) | Device under `/sys/class/backlight/*/brightness` exists. | [#30](https://github.com/leukipp/touchkio/issues/30), [#32](https://github.com/leukipp/touchkio/issues/32) |
|
| Display (Brightness) | Requires `sudo` rights, device under `/sys/class/backlight/*/brightness` exists or working `ddcutil` command. | [#30](https://github.com/leukipp/touchkio/issues/30), [#101](https://github.com/leukipp/touchkio/issues/101) |
|
||||||
| Keyboard | Raspberry Pi OS (Wayland) with `squeekboard` running. | [#7](https://github.com/leukipp/touchkio/issues/7), [#85](https://github.com/leukipp/touchkio/issues/85) |
|
| Keyboard | Raspberry Pi OS (Wayland) with `squeekboard` running. | [#7](https://github.com/leukipp/touchkio/issues/7), [#85](https://github.com/leukipp/touchkio/issues/85) |
|
||||||
| Battery | Device under `/sys/class/power_supply/*/capacity` exists. | [#33](https://github.com/leukipp/touchkio/issues/33) |
|
| Battery | Device under `/sys/class/power_supply/*/capacity` exists. | [#33](https://github.com/leukipp/touchkio/issues/33) |
|
||||||
| Volume | Device (non-dummy) `pactl get-default-sink` exists. | [#82](https://github.com/leukipp/touchkio/issues/82) |
|
| Volume | Device (non-dummy) `pactl get-default-sink` exists. | [#82](https://github.com/leukipp/touchkio/issues/82) |
|
||||||
| Reboot | Requires password-less `sudo` rights. | [#39](https://github.com/leukipp/touchkio/issues/39) |
|
| Reboot | Requires password-less `sudo` rights. | [#39](https://github.com/leukipp/touchkio/issues/39) |
|
||||||
| Shutdown | Requires password-less `sudo` rights. | [#39](https://github.com/leukipp/touchkio/issues/39) |
|
| Shutdown | Requires password-less `sudo` rights. | [#39](https://github.com/leukipp/touchkio/issues/39) |
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Operating System
|
||||||
|
|
||||||
|
<details><summary>I have installed Ubuntu GNOME.</summary>
|
||||||
|
|
||||||
|
- On some Debian based systems (e.g. Ubuntu GNOME), the display status control is only available when using X11 (`xset`).
|
||||||
|
- GNOME is the least supported window manager.
|
||||||
|
- It's recommended to switch to KDE wayland, if you want better support for display status control (`kscreen-doctor`).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>I have installed Raspberry Pi OS on a RPI3.</summary>
|
||||||
|
|
||||||
|
- Raspberry Pi 3 devices have produced a bunch of issues in the past.
|
||||||
|
- [RPI 3B thermostat card issue](https://github.com/leukipp/touchkio/issues/17).
|
||||||
|
- [RPI 3B wayland rendering issue](https://github.com/leukipp/touchkio/issues/104).
|
||||||
|
- [Webpage cards don't work (RPI 3B)](https://github.com/leukipp/touchkio/issues/24).
|
||||||
|
- It looks like the GPU is not properly supported by Electron.
|
||||||
|
- Running `touchkio --disable-gpu` may improve the situation, but upgrading the hardware is the best solution.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>I want to update APT packages automatically.</summary>
|
||||||
|
|
||||||
|
- The **Package Upgrades** MQTT sensor will show if `apt` package upgrades are available, but updates are [intentionally](https://github.com/leukipp/touchkio/issues/7) not triggered by TouchKio.
|
||||||
|
- It's recommended to keep your system up to date for security and compatibility, as TouchKio primarily supports the latest OS versions.
|
||||||
|
- Consider configuring `sudo apt install unattended-upgrades` for automatic updates.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Display
|
||||||
|
|
||||||
|
<details><summary>Display status can't be controlled through MQTT.</summary>
|
||||||
|
|
||||||
|
- The following commands are currently implemented to modify the display status. Make sure that one of these works for your display when you run it directly on the terminal, otherwise the MQTT switch will not work either.
|
||||||
|
- `wlopm --[on,off] \*` (Raspberry Pi OS, Wayland)
|
||||||
|
- `kscreen-doctor --dpms [on,off]` (Debian KDE, Wayland)
|
||||||
|
- `xset dpms force [on,off]` (Generic, X11)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Display brightness can't be controlled through MQTT.</summary>
|
||||||
|
|
||||||
|
- Have a look at the [features](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#features) section to check if all requirements are fulfilled.
|
||||||
|
- The **Display** MQTT control is a light entity with brightness support. So make sure to to [click on the entity](https://github.com/leukipp/touchkio/issues/75) to check for brightness support.
|
||||||
|
- HDMI screens typically do not offer brightness control out of the box, so additional setup steps are required.
|
||||||
|
- Brightness support for [ddcutil](https://github.com/leukipp/touchkio/issues/101#issuecomment-3521247263) is build-in and checked on application startup.
|
||||||
|
- Alternatively installing [ddcci-driver-linux](https://github.com/leukipp/touchkio/issues/132#issue-3659009749) or [ddcci-dkms](https://github.com/leukipp/touchkio/issues/101#issuecomment-3571523927) may also work.
|
||||||
|
- Make sure that one of these works for your display when you run it directly on the terminal, otherwise the MQTT switch will not work either.
|
||||||
|
- `sudo cat /sys/class/backlight/*/brightness` returns some numeric value.
|
||||||
|
- `sudo ddcutil setvcp 10 42` changes the brightness to 42% without asking for a password.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Turning HDMI screen off via MQTT causes it to turn back on.</summary>
|
||||||
|
|
||||||
|
- There are certain HDMI screens (e.g. Viewsonic) where the MQTT display status command doesn't work as expected.
|
||||||
|
- The screen [immediately turns on again](https://github.com/leukipp/touchkio/issues/38), which may be caused by incompatible HDMI cables being used.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Touch
|
||||||
|
|
||||||
|
<details><summary>Automated screen blanking on inactivity.</summary>
|
||||||
|
|
||||||
|
- You can use Raspberry Pi's build-in [screen blanking](https://www.raspberrypi.com/documentation/computers/configuration.html#screen-blanking-3) functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
|
||||||
|
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant [automation](https://www.home-assistant.io/docs/automation/basics) (e.g. presence detection or **Last Active** MQTT sensor) to manage the screen status.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Touch in multitouch mode not waking the screen.</summary>
|
||||||
|
|
||||||
|
- It's recommended to use **Mouse Emulation** for touch screens, otherwise touch based [screen wake-up](https://github.com/leukipp/touchkio/issues/127) from display off state will fail, especially if build-in [screen blanking](https://www.raspberrypi.com/documentation/computers/configuration.html#screen-blanking-3) is disabled.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Touch events may propagate through a display that is turned off.</summary>
|
||||||
|
|
||||||
|
- It's recommended to use **Mouse Emulation** for touch screens, otherwise Home Assistant actions could be triggered [unintentionally](https://github.com/leukipp/touchkio/issues/61) on touch.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Network
|
||||||
|
|
||||||
|
<details><summary>Connecting via https/mqtts with a self-signed certificate.</summary>
|
||||||
|
|
||||||
|
- When connecting to a service with a [custom certificate](https://github.com/leukipp/touchkio/issues/42#issuecomment-3041870215), ensure the specified FQDN matches.
|
||||||
|
- Using the `--ignore-certificate-errors` flag is [not recommended](https://github.com/leukipp/touchkio/issues/76) for browsing external sites.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Local DNS entries in /etc/hosts are ignored.</summary>
|
||||||
|
|
||||||
|
- Electron bypasses the system resolver stack, ignoring `/etc/hosts` and local DNS servers.
|
||||||
|
- To use the system resolver, modify `~/.config/systemd/user/touchkio.service` to disable these features:
|
||||||
|
```bash
|
||||||
|
ExecStart=/usr/bin/touchkio --disable-features=UseDNSHttps,AsyncDns
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>During VNC access the display turns gray.</summary>
|
||||||
|
|
||||||
|
- On Raspberry Pi OS the display command may fail with `ERROR: Setting power mode for output '[DSI-*,HDMI-*]' failed`.
|
||||||
|
- This can happen if you have `wayvnc` running for remote access and is caused by a [known bug](https://github.com/leukipp/touchkio/issues/78).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Media
|
||||||
|
|
||||||
|
<details><summary>I need Music and Voice Assistant.</summary>
|
||||||
|
|
||||||
|
- Not planned, look for [alternatives](https://github.com/leukipp/touchkio/issues/88#issuecomment-3366659265).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>I need a camera stream from the device.</summary>
|
||||||
|
|
||||||
|
- Not planned, look for [alternatives](https://github.com/leukipp/touchkio/issues/130#issuecomment-3591770594).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>I need additional MQTT entities to control hardware.</summary>
|
||||||
|
|
||||||
|
- Not planned, look for [alternatives](https://github.com/leukipp/touchkio/issues/138#issuecomment-3591784725).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
<details><summary>Some of the MQTT controls are missing.</summary>
|
||||||
|
|
||||||
|
- Certain [features](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#features) from the MQTT integration may require elevated privileges to work correctly.
|
||||||
|
- Test if your local user has the [necessary permissions](https://github.com/leukipp/touchkio/issues/39) to run `sudo -n true` without being [prompted for a password](https://github.com/leukipp/touchkio/issues/116#issuecomment-3471566411).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>The ddcutil command doesn't seem to be used.</summary>
|
||||||
|
|
||||||
|
- Currently `ddcutil` is only used for brightness control of HDMI screens.
|
||||||
|
- Make sure that your screen is supporting [continuous](https://github.com/leukipp/touchkio/issues/101#issuecomment-3521247263) adjustments of brightness values.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>The on-screen keyboard doesn't automatically pop-out.</summary>
|
||||||
|
|
||||||
|
- Most of the available on-screen keyboards will not render above any [fullscreen window](https://forums.raspberrypi.com/viewtopic.php?p=2327148).
|
||||||
|
- There is a [workaround](https://github.com/leukipp/touchkio/issues/85) for `squeekboard`, which will only work on Raspberry Pi OS.
|
||||||
|
- If the on-screen keyboard still doesn't [automatically pop-out](https://github.com/leukipp/touchkio/issues/4) when entering a text field inside the webview you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the visibility.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
<details><summary>Terminal is full with "ERROR:gbm_wrapper.cc" messages.</summary>
|
||||||
|
|
||||||
|
- On the terminal you may see some _ERROR:gbm_wrapper.cc_ messages.
|
||||||
|
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>CPU and RAM usage increases without reason.</summary>
|
||||||
|
|
||||||
|
- This was [especially observed](https://github.com/leukipp/touchkio/issues/123) when the screen is permanently on and renders a dashboard with interactive elements (e.g. custom lovelace cards).
|
||||||
|
- Switching to another **Page Url** or turning the **Display** off during idle times improves resource usage.
|
||||||
|
- Alternatively a timed **Refresh** of the webview via MQTT can also help.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Error message "GPU Process abnormal-exit".</summary>
|
||||||
|
|
||||||
|
- _GPU Process abnormal-exit (code 512)_ means that the Electron GPU process crashed, probably caused by some issues inside the webview.
|
||||||
|
- Disabling the GPU via `touchkio --disable-gpu` may help.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Error message "Render Process killed".</summary>
|
||||||
|
|
||||||
|
- _Render Process killed (code 9)_ will lead to a temporary [white screen](https://github.com/leukipp/touchkio/issues/115).
|
||||||
|
- The problem likely stems from Electron’s calculated RAM limit (~2GB) and the dashboard using too much memory.
|
||||||
|
- Although there are flags to [increase the limit](https://github.com/leukipp/touchkio/issues/36#issuecomment-3406441947), a standard webview shouldn’t require that much RAM.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
In case your hardware is not listed above don't worry, give it a try.
|
In case your hardware is not listed above don't worry, give it a try.
|
||||||
Running `touchkio --web-url=https://demo.home-assistant.io` will most likely just work.
|
Running `touchkio --web-url=https://demo.home-assistant.io` will most likely just work.
|
||||||
The only problems that may arise are when controlling the display via the Home Assistant integration.
|
The only problems that may arise are when controlling the display via the Home Assistant integration.
|
||||||
|
|
||||||
|
There is a possibility to test `pre-releases` if you would like to try upcoming versions before the official release is published.
|
||||||
|
These early builds are available to a smaller group of users who have chosen to take part in testing.
|
||||||
|
|
||||||
|
If you are interested in long-term testing, check out the related [help request](https://github.com/leukipp/touchkio/issues/96).
|
||||||
|
When a [pre-release](https://github.com/leukipp/touchkio/releases) is available and you want to test it on-demand, run:
|
||||||
|
```bash
|
||||||
|
bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install.sh) update early
|
||||||
|
```
|
||||||
|
|
||||||
- If you encounter any problems, please create a new [issue](https://github.com/leukipp/touchkio/issues).
|
- If you encounter any problems, please create a new [issue](https://github.com/leukipp/touchkio/issues).
|
||||||
- If you encounter any problems and are able to fix it yourself, feel free to create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
- If you encounter any problems and are able to fix it yourself, feel free to create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
||||||
- If everything works as expected and your hardware is not yet listed, you are welcome to [report](https://github.com/leukipp/touchkio/issues/12) it or create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
- If everything works as expected and your hardware is not yet listed, you are welcome to [report](https://github.com/leukipp/touchkio/issues/12) it or create a [pull request](https://github.com/leukipp/touchkio/pulls).
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# TouchKio
|
# TouchKio
|
||||||

|
[](https://github.com/leukipp/touchkio/actions)
|
||||||

|
[](https://github.com/leukipp/touchkio/releases)
|
||||||

|
[](https://github.com/leukipp/touchkio/releases)
|
||||||

|
[](https://github.com/leukipp/touchkio/releases)
|
||||||
[](https://github.com/sponsors/leukipp)
|
[](https://github.com/sponsors/leukipp)
|
||||||
|
|
||||||
**TouchKio** is a Node.js application that utilizes Electron to create a kiosk mode window specifically designed for a Home Assistant dashboard.
|
**TouchKio** is a Node.js application that utilizes Electron to create a kiosk mode window specifically designed for a Home Assistant dashboard.
|
||||||
@@ -27,10 +27,11 @@ Moreover, the device running the **kiosk application** also offers several **Hom
|
|||||||
- [x] Toggle the on-screen keyboard.
|
- [x] Toggle the on-screen keyboard.
|
||||||
- [x] Touch display power and brightness.
|
- [x] Touch display power and brightness.
|
||||||
- [x] Manage kiosk window status and zoom.
|
- [x] Manage kiosk window status and zoom.
|
||||||
|
- [x] Screenshot image of the kiosk webview.
|
||||||
- [x] Show network interfaces and addresses.
|
- [x] Show network interfaces and addresses.
|
||||||
- [x] List all available system package upgrades.
|
- [x] List all available system package upgrades.
|
||||||
- [x] Multi-webpage switching and url navigation.
|
- [x] Multi-webpage switching and url navigation.
|
||||||
- [x] Audio volume control for connected devices.
|
- [x] Volume control for connected audio outputs.
|
||||||
- [x] Execute system reboot and shutdown commands.
|
- [x] Execute system reboot and shutdown commands.
|
||||||
- [x] Monitor battery, temperature, processor and memory usage.
|
- [x] Monitor battery, temperature, processor and memory usage.
|
||||||
|
|
||||||
@@ -51,11 +52,12 @@ This setup allows seamless communication between your kiosk device and Home Assi
|
|||||||
For a comprehensive guide on setting up MQTT with Home Assistant, please refer to the official documentation available here: https://www.home-assistant.io/integrations/mqtt.
|
For a comprehensive guide on setting up MQTT with Home Assistant, please refer to the official documentation available here: https://www.home-assistant.io/integrations/mqtt.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
On the first run of the application, you may encounter a **setup procedure** and the Home Assistant **login screen**.
|
On the first run of the application, you will encounter a **setup procedure (CLI)** and the Home Assistant **login screen (UI)**.
|
||||||
It's recommended to create a dedicated Home Assistant user (local access only) for your kiosk device.
|
It's recommended to create a dedicated Home Assistant user (local access only) for your kiosk device.
|
||||||
|
|
||||||
You might also need a physical keyboard or remote VNC access to input these credentials once.
|
You might also need a physical keyboard or remote VNC access to input these credentials once.
|
||||||
If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) you may be able to activate the on-screen keyboard using the side [widget](https://github.com/leukipp/touchkio/issues/16).
|
If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) you may be able to activate the on-screen keyboard using the side [widget](https://github.com/leukipp/touchkio/issues/16).
|
||||||
|
After the first login the Home Assistant credentials are stored inside the `~/.config/touchkio/` folder.
|
||||||
|
|
||||||
#### Option 1 - The easy way
|
#### Option 1 - The easy way
|
||||||
Run this command to download and install the latest **.deb** (arm64 or x64) release.
|
Run this command to download and install the latest **.deb** (arm64 or x64) release.
|
||||||
@@ -236,20 +238,22 @@ The Raspberry Pi's **build-in on-screen keyboard** named `squeekboard` (it squea
|
|||||||
The kiosk application interacts with squeekboard via the `D-Bus` object path `/sm/puri/OSK0`, enabling the keyboard to be hidden or shown based on **MQTT** user input or system events.
|
The kiosk application interacts with squeekboard via the `D-Bus` object path `/sm/puri/OSK0`, enabling the keyboard to be hidden or shown based on **MQTT** user input or system events.
|
||||||
|
|
||||||
The Raspberry Pi's **build-in screen blanking** function uses the command `swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &` inside `~/.config/labwc/autostart` to blank the screen after **10 minutes**.
|
The Raspberry Pi's **build-in screen blanking** function uses the command `swayidle -w timeout 600 'wlopm --off \*' resume 'wlopm --on \*' &` inside `~/.config/labwc/autostart` to blank the screen after **10 minutes**.
|
||||||
The `wlopm --off \*` command changes the `bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
|
The `wlopm --off \*` command changes the `/sys/class/backlight/*/bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
|
||||||
However, `swayidle` still seems to consider the screen to be off and as a result it will not turn off again unless there is some interaction in the meantime.
|
However, `swayidle` still seems to consider the screen to be off and as a result it will not turn off again unless there is some interaction in the meantime.
|
||||||
|
|
||||||
When using the MQTT integration, the kiosk application must be able to **detect changes** made on the **device** itself.
|
When using the MQTT integration, the kiosk application must be able to **detect changes** made on the **device** itself.
|
||||||
I managed to achieve this for the `brightness` file by implementing a simple `fs.watch(..)` file listener.
|
I managed to achieve this for the `/sys/class/backlight/*/brightness` file by implementing a simple `fs.watch(..)` file listener.
|
||||||
However, I found that it **never triggered** for the `bl_power` file.
|
However, I found that it **never triggered** for the `/sys/class/backlight/*/bl_power` or the `/sys/class/drm/*/dpms` file.
|
||||||
Although the file content changes, none of the filesystem listeners where fired.
|
Although the file content changes, none of the filesystem listeners where fired.
|
||||||
This could be due to `swayidle`/`wlopm` performing write actions at a deeper level that are not detectable by file listeners.
|
This could be due to `swayidle`/`wlopm` performing write actions at a deeper level that are not detectable by file listeners.
|
||||||
As a result, I went for a **polling solution**, checking the state of the `brightness` and `dpms` file **every second** for any changes.
|
|
||||||
|
As a result, I went for a **polling solution**, checking the state of the `/sys/class/backlight/*/brightness` and `/sys/class/drm/*/dpms` file **every second** for any changes.
|
||||||
|
In case `ddcutil` is installed the file `~/.config/touchkio/Cache/Brightness.vcp` will be written/monitored to support brightness support for HDMI screens.
|
||||||
While I understand this is not ideal, it's necessary to ensure proper functionality.
|
While I understand this is not ideal, it's necessary to ensure proper functionality.
|
||||||
|
|
||||||
The display power status and brightness can be adjusted via the MQTT integration.
|
The display power status and brightness can be adjusted via the MQTT integration.
|
||||||
**Support** for changing the power status on **DSI and HDMI** displays is achieved by checking for connected screens in `/sys/class/drm/*/status`.
|
**Support** for changing the power status on **DSI and HDMI** displays is achieved by checking for connected screens in `/sys/class/drm/*/status`.
|
||||||
Support for changing the brightness of connected display is implemented by using `/sys/class/backlight/*/brightness`.
|
Support for changing the brightness of a connected display is implemented by using `sudo tee /sys/class/backlight/*/brightness` or `sudo ddcutil setvcp 10`.
|
||||||
In cases where no supported backlight device is found, the Home Assistant light entity will only show an on/off switch without brightness control.
|
In cases where no supported backlight device is found, the Home Assistant light entity will only show an on/off switch without brightness control.
|
||||||
|
|
||||||
Keep in mind that default arguments are stored as plain text in `~/.config/touchkio/Arguments.json`.
|
Keep in mind that default arguments are stored as plain text in `~/.config/touchkio/Arguments.json`.
|
||||||
@@ -269,31 +273,44 @@ This adjustment provides a user experience similar to that of a proper mobile de
|
|||||||
</div></details>
|
</div></details>
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
Please review the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation first if you encounter any problems.
|
> [!NOTE]
|
||||||
|
> ### Please read the hardware [FAQ](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#faq) section first if you encounter any issues.
|
||||||
|
|
||||||
**Known Issues** that are by-design or for which there isn't a solution so far:
|
For basic debugging **(TouchKio)**, stop the service and launch `touchkio` directly on the terminal to monitor the log output in real-time.
|
||||||
- You can use Raspberry Pi's build-in [screen blanking](https://www.raspberrypi.com/documentation/computers/configuration.html#screen-blanking-3) functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
|
|
||||||
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant [automation](https://www.home-assistant.io/docs/automation/basics) (e.g. presence detection) to manage the screen status.
|
|
||||||
- An active VNC session can also interfere with the display state, you can find details about this in the features [section](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#features).
|
|
||||||
- When using a Raspberry Pi, the on-screen keyboard doesn't [automatically pop-out](https://github.com/leukipp/touchkio/issues/4) when entering a text field inside the webview.
|
|
||||||
- As a current workaround you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the keyboard visibility.
|
|
||||||
- There is also an [experimental feature](https://github.com/leukipp/touchkio/issues/85) that uses a special Electron flag to address this problem.
|
|
||||||
- On the terminal you may see some _ERROR:gbm_wrapper.cc_ messages.
|
|
||||||
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works.
|
|
||||||
|
|
||||||
For debugging, stop the service and launch `touchkio` directly on the terminal to monitor the log output in real-time.
|
|
||||||
This output is also written into `~/.config/touchkio/logs/main.log` for review.
|
This output is also written into `~/.config/touchkio/logs/main.log` for review.
|
||||||
|
|
||||||
If you encounter any problems, please [create an issue](https://github.com/leukipp/touchkio/issues) and include the output of `touchkio --version`, additional information's about your system (such as operating system, hardware, etc.) and any relevant log files.
|
For extended logging **(Electron)** you can run `touchkio --enable-logging`, which will write an additional log file into `~/.config/touchkio/logs/electron.log`.
|
||||||
|
Refer to the [--log-level=[0-3]](https://www.electronjs.org/docs/latest/api/command-line-switches#--log-leveln) and [--v=[0-3]](https://www.electronjs.org/docs/latest/api/command-line-switches#--vlog_level) options to adjust the logging verbosity.
|
||||||
|
|
||||||
|
If you need to debug the webview **(Chrome DevTools)** use `touchkio --app-debug`.
|
||||||
|
|
||||||
|
In case [undocumented problems](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md#faq) arise, please [create an issue](https://github.com/leukipp/touchkio/issues) and include the output of `touchkio --version`, additional information about your system (such as operating system, hardware, etc.), and any relevant log files.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
[Inspired by](https://www.jeffgeerling.com/blog/2024/home-assistant-and-carplay-pi-touch-display-2) the one and only Raspberry Pi Master, Jeff Geerling ([@geerlingguy](https://github.com/geerlingguy)).
|
[Inspired by](https://www.jeffgeerling.com/blog/2024/home-assistant-and-carplay-pi-touch-display-2) the one and only Raspberry Pi Master, Jeff Geerling ([@geerlingguy](https://github.com/geerlingguy)).
|
||||||
|
|
||||||
Thanks to Sebastian ([@pdsccode](https://github.com/pdsccode)) for his contributions on issues and [community](https://community.home-assistant.io/t/kiosk-mode-for-raspberry-pi-with-touch-display/821196) discussions.
|
Thanks to Sebastian ([@pdsccode](https://github.com/pdsccode)) for his contributions on issues and [community](https://community.home-assistant.io/t/kiosk-mode-for-raspberry-pi-with-touch-display/821196) discussions.
|
||||||
|
|
||||||
If you are looking for hardware or a well-designed mounting solution for the Raspberry Pi Display, check out the [blog post](https://www.thestockpot.net/videos/home-assistant-wall-display) from Dillan Stock ([@TheStockPot-AU](https://www.youtube.com/channel/UCmlzNHg8QiWVutUyFOV2UdQ)).
|
### Tutorials
|
||||||
|
If you are looking for hardware or a well-designed mounting solution for the Raspberry Pi Display, check out the [blog post](https://www.thestockpot.net/videos/home-assistant-wall-display) from Dillan Stock ([@TheStockPot-AU](https://www.youtube.com/@TheStockPot-AU)):
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=uTxURzmrVtA)
|
<a href="https://www.youtube.com/watch?v=uTxURzmrVtA">
|
||||||
|
<img src="https://img.youtube.com/vi/uTxURzmrVtA/maxresdefault.jpg" alt="What My Smart Home Was Missing" title="@TheStockPot-AU" style="width:100%">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Also, have a look at this curated collection of helpful videos contributed by the community:
|
||||||
|
|
||||||
|
<p align="center" style="width:100%">
|
||||||
|
<a href="https://www.youtube.com/watch?v=_adl1fiXlgk">
|
||||||
|
<img src="https://img.youtube.com/vi/_adl1fiXlgk/mqdefault.jpg" alt="Building a Home Assistant Kiosk - Why Did I Wait So Long?" title="@Jims-Garage" style="width:32%">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/watch?v=uXcjx-zL_UU">
|
||||||
|
<img src="https://img.youtube.com/vi/uXcjx-zL_UU/mqdefault.jpg" alt="Touch Display for Home Assistant with TouchKIO" title="@haus_automation" style="width:32%">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/watch?v=t2YQm7GPmpY">
|
||||||
|
<img src="https://img.youtube.com/vi/t2YQm7GPmpY/mqdefault.jpg" alt="DIY Home Assistant Kiosk Build: Raspberry Pi & 3D Printing Livestream" title="@homeautomatorza" style="width:32%">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](https://github.com/leukipp/touchkio/blob/main/LICENSE)
|
[MIT](https://github.com/leukipp/touchkio/blob/main/LICENSE)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'" />
|
||||||
<style>
|
<style>
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--primary-color: rgba(225, 225, 225, 1);
|
--primary-color: rgba(225, 225, 225, 1);
|
||||||
|
|||||||
+29
-6
@@ -1,9 +1,22 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'" />
|
||||||
<style>
|
<style>
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--background-color: rgba(225, 225, 225, 0.15);
|
||||||
|
--previous-cursor: w-resize;
|
||||||
|
--next-cursor: e-resize;
|
||||||
|
}
|
||||||
|
[data-theme="light"] {
|
||||||
|
--background-color: rgba(33, 33, 33, 0.15);
|
||||||
|
--previous-cursor: w-resize;
|
||||||
|
--next-cursor: e-resize;
|
||||||
|
}
|
||||||
[data-theme="hidden"] {
|
[data-theme="hidden"] {
|
||||||
--background-color: rgba(0, 0, 0, 0);
|
--background-color: rgba(0, 0, 0, 0);
|
||||||
|
--previous-cursor: none;
|
||||||
|
--next-cursor: none;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -26,24 +39,31 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
background: var(--background-color);
|
|
||||||
}
|
}
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
li:not(.disabled) #previous {
|
||||||
|
cursor: var(--previous-cursor);
|
||||||
|
}
|
||||||
|
li:not(.disabled) #next {
|
||||||
|
cursor: var(--next-cursor);
|
||||||
|
}
|
||||||
button {
|
button {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 50vh;
|
height: 50vh;
|
||||||
border: none;
|
border: none;
|
||||||
|
cursor: not-allowed;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
#previous {
|
button:not(:active) {
|
||||||
cursor: w-resize;
|
transition: background-color 500ms step-end;
|
||||||
}
|
}
|
||||||
#next {
|
menu:has(button:active) button {
|
||||||
cursor: e-resize;
|
background-color: var(--background-color);
|
||||||
|
transition-duration: 0ms;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -61,13 +81,16 @@
|
|||||||
|
|
||||||
const registerSendEvents = () => {
|
const registerSendEvents = () => {
|
||||||
document.querySelectorAll("button").forEach((button) => {
|
document.querySelectorAll("button").forEach((button) => {
|
||||||
button.addEventListener("click", (e) => {
|
button.addEventListener("pointerdown", (e) => {
|
||||||
ipcRenderer.send("button-click", { id: e.target.id });
|
ipcRenderer.send("button-click", { id: e.target.id });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerReceiveEvents = () => {
|
const registerReceiveEvents = () => {
|
||||||
|
ipcRenderer.on("button-disabled", (e, button) => {
|
||||||
|
document.getElementById(button.id).parentNode.classList.toggle("disabled", button.disabled);
|
||||||
|
});
|
||||||
ipcRenderer.on("data-theme", (e, data) => {
|
ipcRenderer.on("data-theme", (e, data) => {
|
||||||
document.documentElement.setAttribute("data-theme", data.theme);
|
document.documentElement.setAttribute("data-theme", data.theme);
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'" />
|
||||||
<style>
|
<style>
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--primary-color: rgba(229, 229, 229, 1);
|
--primary-color: rgba(229, 229, 229, 1);
|
||||||
@@ -99,7 +100,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1 id="title"></h1>
|
<h1 id="title">TouchKio</h1>
|
||||||
</header>
|
</header>
|
||||||
<menu>
|
<menu>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'" />
|
||||||
<style>
|
<style>
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--primary-color: rgba(225, 225, 225, 1);
|
--primary-color: rgba(225, 225, 225, 1);
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ const integration = require("./js/integration");
|
|||||||
const hardware = require("./js/hardware");
|
const hardware = require("./js/hardware");
|
||||||
const webview = require("./js/webview");
|
const webview = require("./js/webview");
|
||||||
const log = require("electron-log");
|
const log = require("electron-log");
|
||||||
const { app } = require("electron");
|
const { app, powerMonitor } = require("electron");
|
||||||
const Events = require("events");
|
const Events = require("events");
|
||||||
|
|
||||||
global.APP = global.APP || {};
|
global.APP = global.APP || {};
|
||||||
global.ARGS = global.ARGS || {};
|
global.ARGS = global.ARGS || {};
|
||||||
global.EVENTS = global.EVENTS || new Events();
|
global.EVENTS = global.EVENTS || new Events();
|
||||||
|
|
||||||
|
// Check display environment variable
|
||||||
if (!process.env.DISPLAY) {
|
if (!process.env.DISPLAY) {
|
||||||
console.error(`\n$DISPLAY variable not set to run the GUI application, are you connected via SSH?\n`);
|
console.error(`\n$DISPLAY variable not set to run the GUI application, are you connected via SSH?\n`);
|
||||||
console.error(`If you have installed the service use:`);
|
console.error(`If you have installed the service use:`);
|
||||||
@@ -22,6 +23,18 @@ if (!process.env.DISPLAY) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move electron log file
|
||||||
|
const elog = path.join(app.getPath("logs"), "electron.log");
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(elog)) {
|
||||||
|
fs.renameSync(elog, elog.replace(".log", ".old.log"));
|
||||||
|
}
|
||||||
|
console.debug = () => {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to move electron log file:", error.message);
|
||||||
|
}
|
||||||
|
app.commandLine.appendSwitch("log-file", elog);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This promise resolves when the app has finished initializing,
|
* This promise resolves when the app has finished initializing,
|
||||||
* allowing to safely create browser windows and perform other
|
* allowing to safely create browser windows and perform other
|
||||||
@@ -38,11 +51,18 @@ app.whenReady().then(async () => {
|
|||||||
args.mqtt_password = "*".repeat((args.mqtt_password || "").length);
|
args.mqtt_password = "*".repeat((args.mqtt_password || "").length);
|
||||||
}
|
}
|
||||||
console.info(`Arguments: ${JSON.stringify(args, null, 2)}`);
|
console.info(`Arguments: ${JSON.stringify(args, null, 2)}`);
|
||||||
|
process.stdout.write("\n");
|
||||||
|
|
||||||
// Chained init functions
|
// Chained init functions
|
||||||
const chained = [webview.init, hardware.init, integration.init];
|
const chained = [
|
||||||
for (const init of chained) {
|
["webview.js", webview.init],
|
||||||
|
["hardware.js", hardware.init],
|
||||||
|
["integration.js", integration.init],
|
||||||
|
];
|
||||||
|
for (const [name, init] of chained) {
|
||||||
|
console.debug(`${name}: init()`);
|
||||||
if (!(await init())) {
|
if (!(await init())) {
|
||||||
|
console.debug(`${name}: init() --> aborted`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,6 +87,7 @@ const initApp = async () => {
|
|||||||
APP.path = app.getAppPath();
|
APP.path = app.getAppPath();
|
||||||
APP.icon = path.join(app.getAppPath(), "img", "icon.png");
|
APP.icon = path.join(app.getAppPath(), "img", "icon.png");
|
||||||
APP.log = path.join(app.getPath("logs"), "main.log");
|
APP.log = path.join(app.getPath("logs"), "main.log");
|
||||||
|
APP.cache = path.join(app.getPath("userData"), "Cache");
|
||||||
APP.config = app.getPath("userData");
|
APP.config = app.getPath("userData");
|
||||||
|
|
||||||
// Set additional update infos
|
// Set additional update infos
|
||||||
@@ -75,6 +96,7 @@ const initApp = async () => {
|
|||||||
url: `https://api.github.com/repos/${packageJson.author}/${packageJson.name}/releases`,
|
url: `https://api.github.com/repos/${packageJson.author}/${packageJson.name}/releases`,
|
||||||
latest: null,
|
latest: null,
|
||||||
};
|
};
|
||||||
|
APP.issues = `https://github.com/${packageJson.author}/${packageJson.name}/issues`;
|
||||||
APP.scripts = {
|
APP.scripts = {
|
||||||
install: `https://raw.githubusercontent.com/${packageJson.author}/${packageJson.name}/main/install.sh`,
|
install: `https://raw.githubusercontent.com/${packageJson.author}/${packageJson.name}/main/install.sh`,
|
||||||
};
|
};
|
||||||
@@ -91,6 +113,38 @@ const initApp = async () => {
|
|||||||
return app.exit(1);
|
return app.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register app quit events
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
APP.exiting = true;
|
||||||
|
});
|
||||||
|
app.on("will-quit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
process.exitCode = process.exitCode !== 0 ? 1 : 0;
|
||||||
|
const level = process.exitCode === 0 ? "warn" : "error";
|
||||||
|
console[level](`${APP.title} Terminated (${process.exitCode})`);
|
||||||
|
app.exit(process.exitCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register process exit events
|
||||||
|
["SIGINT", "SIGTERM", "SIGQUIT", "exit"].forEach((signal) => {
|
||||||
|
process.on(signal, () => {
|
||||||
|
if (APP.exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.exitCode = 0;
|
||||||
|
APP.exiting = true;
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
powerMonitor.on("shutdown", () => {
|
||||||
|
if (APP.exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.exitCode = 0;
|
||||||
|
APP.exiting = true;
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,6 +160,9 @@ const initArgs = async () => {
|
|||||||
let argsFilePath = path.join(APP.config, "Arguments.json");
|
let argsFilePath = path.join(APP.config, "Arguments.json");
|
||||||
let argsFileExists = fs.existsSync(argsFilePath);
|
let argsFileExists = fs.existsSync(argsFilePath);
|
||||||
|
|
||||||
|
let argsFileHashPath = path.join(APP.cache, "Arguments.hash");
|
||||||
|
let argsFileHashExists = fs.existsSync(argsFileHashPath);
|
||||||
|
|
||||||
// Show version and release info
|
// Show version and release info
|
||||||
if ("help" in args || "version" in args) {
|
if ("help" in args || "version" in args) {
|
||||||
let build = "";
|
let build = "";
|
||||||
@@ -138,6 +195,18 @@ const initArgs = async () => {
|
|||||||
if (!Array.isArray(args.web_url)) {
|
if (!Array.isArray(args.web_url)) {
|
||||||
args.web_url = args.web_url.split(",").map((url) => url.trim());
|
args.web_url = args.web_url.split(",").map((url) => url.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate arguments hash
|
||||||
|
const argsFileHash = crypto.createHash("sha256").update(JSON.stringify(args)).digest("hex");
|
||||||
|
const argsUpdated = argsFileHashExists && argsFileHash !== fs.readFileSync(argsFileHashPath, "utf8");
|
||||||
|
if (argsUpdated && !("app_reset" in args)) {
|
||||||
|
args.app_reset = "arguments";
|
||||||
|
}
|
||||||
|
if (fs.existsSync(APP.cache)) {
|
||||||
|
fs.writeFileSync(argsFileHashPath, argsFileHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set global arguments
|
||||||
ARGS = args;
|
ARGS = args;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -151,23 +220,44 @@ const initArgs = async () => {
|
|||||||
const initLog = async () => {
|
const initLog = async () => {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(APP.log)) {
|
if (fs.existsSync(APP.log)) {
|
||||||
fs.unlinkSync(APP.log);
|
fs.renameSync(APP.log, APP.log.replace(".log", ".old.log"));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete log file:", error.message);
|
console.error("Failed to move main log file:", error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite console log
|
// Set log level and path
|
||||||
APP.logs = [];
|
const level = "enable_logging" in ARGS ? "silly" : "app_debug" in ARGS ? "debug" : "verbose";
|
||||||
|
log.transports.file.level = level;
|
||||||
|
log.transports.console.level = level;
|
||||||
|
log.transports.file.resolvePathFn = () => {
|
||||||
|
return APP.log;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Catch unhandled errors
|
||||||
log.errorHandler.startCatching({
|
log.errorHandler.startCatching({
|
||||||
showDialog: false,
|
showDialog: false,
|
||||||
onError({ error }) {
|
onError({ error, versions }) {
|
||||||
console.error(`Uncaught Error: ${error.message} (${error.stack})`);
|
if (!error?.message?.includes("Object has been destroyed")) {
|
||||||
app.exit(1);
|
const build = { ...APP.build, ...versions };
|
||||||
|
const whoopsie = "💥 Whoopsie!";
|
||||||
|
const section2 = `# Description\n- Hardware information?\n- How to reproduce?\n- Additional logs?\n`;
|
||||||
|
const section3 = `# Error\n\`\`\`bash\n${new Date().toISOString()}: ${error.stack}\n\`\`\`\n`;
|
||||||
|
const section4 = `# Application\n\`\`\`json\n${JSON.stringify(build, null, 2)}\n\`\`\`\n`;
|
||||||
|
const title = encodeURIComponent(`${whoopsie} - ${error}`);
|
||||||
|
const body = encodeURIComponent(`${section2}\n${section3}\n${section4}`);
|
||||||
|
console.error(`${whoopsie} -`, error, build);
|
||||||
|
console.info(`🪲 Report issue --> ${APP.issues}/new?title=${title}&body=${body}`);
|
||||||
|
}
|
||||||
|
app.quit();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Emit console log
|
||||||
|
APP.logs = [];
|
||||||
log.hooks.push((message, transport, type) => {
|
log.hooks.push((message, transport, type) => {
|
||||||
const text = message.data.join(" ");
|
const data = message.data.map((d) => (typeof d === "object" ? String(d?.message) || JSON.stringify(d) : String(d)));
|
||||||
|
const text = data.filter((s) => s && s.trim()).join(" ");
|
||||||
if (!text.startsWith("(node:") && type === "console") {
|
if (!text.startsWith("(node:") && type === "console") {
|
||||||
APP.logs.unshift({
|
APP.logs.unshift({
|
||||||
time: message.date,
|
time: message.date,
|
||||||
@@ -181,10 +271,10 @@ const initLog = async () => {
|
|||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
});
|
});
|
||||||
log.transports.file.resolvePathFn = () => {
|
|
||||||
return APP.log;
|
// Overwrite console log
|
||||||
};
|
|
||||||
Object.assign(console, log.functions);
|
Object.assign(console, log.functions);
|
||||||
|
console.silly("Welcome To The Jungle!");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|||||||
+222
-67
@@ -15,14 +15,14 @@ global.HARDWARE = global.HARDWARE || {
|
|||||||
},
|
},
|
||||||
display: {
|
display: {
|
||||||
status: {
|
status: {
|
||||||
value: null,
|
|
||||||
path: null,
|
path: null,
|
||||||
command: null,
|
command: null,
|
||||||
|
value: {},
|
||||||
},
|
},
|
||||||
brightness: {
|
brightness: {
|
||||||
value: null,
|
|
||||||
path: null,
|
path: null,
|
||||||
max: null,
|
command: null,
|
||||||
|
value: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
keyboard: {
|
keyboard: {
|
||||||
@@ -52,7 +52,8 @@ const init = async () => {
|
|||||||
HARDWARE.display.status.path = getDisplayStatusPath();
|
HARDWARE.display.status.path = getDisplayStatusPath();
|
||||||
HARDWARE.display.status.command = getDisplayStatusCommand();
|
HARDWARE.display.status.command = getDisplayStatusCommand();
|
||||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
||||||
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
HARDWARE.display.brightness.command = getDisplayBrightnessCommand();
|
||||||
|
HARDWARE.display.brightness.value.max = getDisplayBrightnessMax();
|
||||||
HARDWARE.audio.device = getAudioDevice();
|
HARDWARE.audio.device = getAudioDevice();
|
||||||
HARDWARE.support = checkSupport();
|
HARDWARE.support = checkSupport();
|
||||||
HARDWARE.initialized = true;
|
HARDWARE.initialized = true;
|
||||||
@@ -86,22 +87,35 @@ const init = async () => {
|
|||||||
// Show hardware infos
|
// Show hardware infos
|
||||||
process.stdout.write("\n");
|
process.stdout.write("\n");
|
||||||
const unsupported = "unsupported";
|
const unsupported = "unsupported";
|
||||||
console.info(`Battery Level [${HARDWARE.battery.level.path || unsupported}]:`, `${getBatteryLevel() || unsupported}`);
|
const batteryLevel = `${getBatteryLevel()} (sysfs)`;
|
||||||
|
const batteryLevelInfo = HARDWARE.support.batteryLevel ? batteryLevel : unsupported;
|
||||||
console.info(
|
console.info(
|
||||||
`Display Status [${HARDWARE.display.status.path || unsupported}]:`,
|
`Battery Level [${HARDWARE.support.batteryLevel ? HARDWARE.battery.level.path : unsupported}]:`,
|
||||||
`${getDisplayStatus() || unsupported} (${HARDWARE.display.status.command || unsupported})`,
|
batteryLevelInfo,
|
||||||
);
|
);
|
||||||
|
const displayStatus = `${getDisplayStatus()} (${HARDWARE.display.status.command})`;
|
||||||
|
const displayStatusInfo = HARDWARE.support.displayStatus ? displayStatus : unsupported;
|
||||||
console.info(
|
console.info(
|
||||||
`Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`,
|
`Display Status [${HARDWARE.support.displayStatus ? HARDWARE.display.status.path : unsupported}]:`,
|
||||||
`${getDisplayBrightness() || unsupported}`,
|
displayStatusInfo,
|
||||||
);
|
);
|
||||||
|
const displayBrightness = `${getDisplayBrightness()} (${HARDWARE.display.brightness.command || "sysfs"})`;
|
||||||
|
const displayBrightnessInfo = HARDWARE.support.displayBrightness ? displayBrightness : unsupported;
|
||||||
|
console.info(
|
||||||
|
`Display Brightness [${HARDWARE.support.displayBrightness ? HARDWARE.display.brightness.path || "ddc://vcp/feature/0x10" : unsupported}]:`,
|
||||||
|
displayBrightnessInfo,
|
||||||
|
);
|
||||||
|
const audioVolume = `${getAudioVolume()} (pactl)`;
|
||||||
|
const audioVolumeInfo = HARDWARE.support.audioVolume ? audioVolume : unsupported;
|
||||||
console.info(
|
console.info(
|
||||||
`Audio Volume [${HARDWARE.support.audioVolume ? HARDWARE.audio.device : unsupported}]:`,
|
`Audio Volume [${HARDWARE.support.audioVolume ? HARDWARE.audio.device : unsupported}]:`,
|
||||||
`${getAudioVolume() || unsupported}`,
|
audioVolumeInfo,
|
||||||
);
|
);
|
||||||
|
const keyboardVisibility = `${getKeyboardVisibility()} (squeekboard)`;
|
||||||
|
const keyboardVisibilityInfo = HARDWARE.support.keyboardVisibility ? keyboardVisibility : unsupported;
|
||||||
console.info(
|
console.info(
|
||||||
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`,
|
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "dbus://sm/puri/OSK0" : unsupported}]:`,
|
||||||
`${getKeyboardVisibility() || unsupported}`,
|
keyboardVisibilityInfo,
|
||||||
);
|
);
|
||||||
process.stdout.write("\n");
|
process.stdout.write("\n");
|
||||||
|
|
||||||
@@ -135,9 +149,9 @@ const init = async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for display changes
|
// Monitor display changes (1s)
|
||||||
setDisplayStatus("ON", () => {
|
setDisplayStatus("ON", () => {
|
||||||
interval(update, 1000);
|
interval(update, 1 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -151,23 +165,65 @@ const update = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display status has changed
|
// Check if display status has changed
|
||||||
if (HARDWARE.support.displayStatus) {
|
if (HARDWARE.support.displayStatus) {
|
||||||
const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim();
|
let displayStatusChanged = false;
|
||||||
if (status !== HARDWARE.display.status.value) {
|
|
||||||
console.info("Update Display Status:", getDisplayStatus());
|
// Use sysfs dpms path if available
|
||||||
HARDWARE.display.status.value = status;
|
if (HARDWARE.display.status.path) {
|
||||||
|
const power = await readFile(path.join(HARDWARE.display.status.path, "dpms"), false);
|
||||||
|
const powerChanged = !!power && power !== HARDWARE.display.status.value.power;
|
||||||
|
|
||||||
|
// Update internal status values
|
||||||
|
HARDWARE.display.status.value.power = power;
|
||||||
|
displayStatusChanged |= powerChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use sysfs status path if available
|
||||||
|
if (HARDWARE.display.status.path) {
|
||||||
|
const connection = await readFile(path.join(HARDWARE.display.status.path, "status"), false);
|
||||||
|
const connectionChanged = !!connection && connection !== HARDWARE.display.status.value.connection;
|
||||||
|
|
||||||
|
// Update internal status values
|
||||||
|
HARDWARE.display.status.value.connection = connection;
|
||||||
|
displayStatusChanged |= connectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit display event if changed
|
||||||
|
if (displayStatusChanged) {
|
||||||
EVENTS.emit("updateDisplay");
|
EVENTS.emit("updateDisplay");
|
||||||
|
console.info("Update Display Status:", getDisplayStatus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display brightness has changed
|
// Check if display brightness has changed
|
||||||
if (HARDWARE.support.displayBrightness) {
|
if (HARDWARE.support.displayBrightness) {
|
||||||
const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim();
|
let displayBrightnessChanged = false;
|
||||||
if (brightness !== HARDWARE.display.brightness.value) {
|
|
||||||
console.info("Update Display Brightness:", getDisplayBrightness());
|
// Use cache brightness path if available
|
||||||
HARDWARE.display.brightness.value = brightness;
|
if (HARDWARE.display.brightness.command) {
|
||||||
|
const brightness = await readFile(path.join(APP.cache, "Brightness.vcp"), false);
|
||||||
|
const brightnessChanged = !!brightness && brightness !== HARDWARE.display.brightness.value.brightness;
|
||||||
|
|
||||||
|
// Update internal brightness values
|
||||||
|
HARDWARE.display.brightness.value.brightness = brightness;
|
||||||
|
displayBrightnessChanged |= brightnessChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use sysfs brightness path if available
|
||||||
|
if (HARDWARE.display.brightness.path) {
|
||||||
|
const brightness = await readFile(path.join(HARDWARE.display.brightness.path, "brightness"), false);
|
||||||
|
const brightnessChanged = !!brightness && brightness !== HARDWARE.display.brightness.value.brightness;
|
||||||
|
|
||||||
|
// Update internal brightness values
|
||||||
|
HARDWARE.display.brightness.value.brightness = brightness;
|
||||||
|
displayBrightnessChanged |= brightnessChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit display event if changed
|
||||||
|
if (displayBrightnessChanged) {
|
||||||
EVENTS.emit("updateDisplay");
|
EVENTS.emit("updateDisplay");
|
||||||
|
console.info("Update Display Brightness:", getDisplayBrightness());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -230,22 +286,26 @@ const sessionDesktop = () => {
|
|||||||
* @returns {Object} Returns support object with boolean values.
|
* @returns {Object} Returns support object with boolean values.
|
||||||
*/
|
*/
|
||||||
const checkSupport = () => {
|
const checkSupport = () => {
|
||||||
const battery = HARDWARE.battery.level;
|
|
||||||
const status = HARDWARE.display.status;
|
|
||||||
const brightness = HARDWARE.display.brightness;
|
|
||||||
const audio = HARDWARE.audio.device;
|
|
||||||
const keyboard = processRuns("squeekboard");
|
|
||||||
const service = serviceRuns(APP.name);
|
|
||||||
const sudo = sudoRights();
|
const sudo = sudoRights();
|
||||||
const deb = APP.build.maker === "deb";
|
const service = serviceRuns(APP.name);
|
||||||
|
const keyboard = processRuns("squeekboard");
|
||||||
|
const release = APP.build.maker === "deb";
|
||||||
|
|
||||||
|
const audioDevice = !!HARDWARE.audio.device;
|
||||||
|
const batteryPath = !!HARDWARE.battery.level.path;
|
||||||
|
const statusPath = !!HARDWARE.display.status.path;
|
||||||
|
const statusCommand = !!HARDWARE.display.status.command;
|
||||||
|
const brightnessPath = !!HARDWARE.display.brightness.path && !!HARDWARE.display.brightness.value.max;
|
||||||
|
const brightnessCommand = !!HARDWARE.display.brightness.command && !!HARDWARE.display.brightness.value.max;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
batteryLevel: !!battery.path,
|
batteryLevel: batteryPath,
|
||||||
displayStatus: !!status.path && !!status.command,
|
displayStatus: statusPath && statusCommand,
|
||||||
displayBrightness: sudo && !!brightness.path && !!brightness.max,
|
displayBrightness: sudo && statusPath && statusCommand && (brightnessPath || brightnessCommand),
|
||||||
keyboardVisibility: keyboard,
|
keyboardVisibility: keyboard,
|
||||||
audioVolume: !!audio,
|
audioVolume: audioDevice,
|
||||||
sudoRights: sudo,
|
sudoRights: sudo,
|
||||||
appUpdate: service && sudo && deb,
|
appUpdate: service && sudo && release,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -397,9 +457,12 @@ const getProcessorTemperature = () => {
|
|||||||
if (!fs.existsSync(typeFile) || !fs.existsSync(tempFile)) {
|
if (!fs.existsSync(typeFile) || !fs.existsSync(tempFile)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const type = fs.readFileSync(typeFile, "utf8").trim();
|
const type = readFile(typeFile);
|
||||||
if (["cpu-thermal", "x86_pkg_temp", "k10temp", "acpitz", "cpu"].includes(type)) {
|
if (["cpu-thermal", "x86_pkg_temp", "k10temp", "acpitz", "cpu"].includes(type)) {
|
||||||
return parseFloat(fs.readFileSync(tempFile, "utf8").trim()) / 1000;
|
const temp = readFile(tempFile);
|
||||||
|
if (temp) {
|
||||||
|
return parseFloat(temp) / 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -431,7 +494,7 @@ const getBatteryLevel = () => {
|
|||||||
if (!HARDWARE.support.batteryLevel) {
|
if (!HARDWARE.support.batteryLevel) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const capacity = fs.readFileSync(`${HARDWARE.battery.level.path}/capacity`, "utf8").trim();
|
const capacity = readFile(`${HARDWARE.battery.level.path}/capacity`);
|
||||||
if (capacity) {
|
if (capacity) {
|
||||||
return parseFloat(capacity);
|
return parseFloat(capacity);
|
||||||
}
|
}
|
||||||
@@ -450,7 +513,7 @@ const getDisplayStatusPath = () => {
|
|||||||
if (!fs.existsSync(statusFile)) {
|
if (!fs.existsSync(statusFile)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const content = fs.readFileSync(statusFile, "utf8").trim();
|
const content = readFile(statusFile);
|
||||||
if (content === "connected") {
|
if (content === "connected") {
|
||||||
return path.join(drm, card);
|
return path.join(drm, card);
|
||||||
}
|
}
|
||||||
@@ -478,7 +541,6 @@ const getDisplayStatusCommand = () => {
|
|||||||
return map.command;
|
return map.command;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.warn("No display command available");
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -567,23 +629,62 @@ const getDisplayBrightnessPath = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the maximum display brightness value using `/sys/class/backlight/.../max_brightness`.
|
* Gets the available display brightness command checking for `ddcutil`.
|
||||||
*
|
*
|
||||||
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
* @returns {string|null} The display brightness command or null if nothing was found.
|
||||||
*/
|
*/
|
||||||
const getDisplayBrightnessMax = () => {
|
const getDisplayBrightnessCommand = () => {
|
||||||
if (!HARDWARE.display.brightness.path) {
|
const type = HARDWARE.session.type;
|
||||||
return null;
|
const desktop = HARDWARE.session.desktop;
|
||||||
}
|
const mapping = {
|
||||||
const max = fs.readFileSync(`${HARDWARE.display.brightness.path}/max_brightness`, "utf8").trim();
|
wayland: [{ command: "ddcutil", desktops: ["*"] }],
|
||||||
if (max) {
|
x11: [{ command: "ddcutil", desktops: ["*"] }],
|
||||||
return parseInt(max, 10);
|
}[type];
|
||||||
|
for (const map of mapping || []) {
|
||||||
|
if (sudoRights() && commandExists(map.command) && map.desktops.some((d) => d === "*" || desktop.includes(d))) {
|
||||||
|
HARDWARE.display.brightness.path = null;
|
||||||
|
switch (map.command) {
|
||||||
|
case "ddcutil":
|
||||||
|
const output = execSyncCommand("sudo", ["ddcutil", "capabilities"]);
|
||||||
|
if (output && output.includes("Feature: 10")) {
|
||||||
|
fs.writeFileSync(path.join(APP.cache, "Brightness.vcp"), "");
|
||||||
|
return map.command;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return map.command;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current display brightness level using `/sys/class/backlight/.../brightness`.
|
* Gets the maximum display brightness value using `/sys/class/backlight/.../max_brightness` or `ddcutil getvcp 10`.
|
||||||
|
*
|
||||||
|
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
||||||
|
*/
|
||||||
|
const getDisplayBrightnessMax = () => {
|
||||||
|
switch (HARDWARE.display.brightness.command) {
|
||||||
|
case "ddcutil":
|
||||||
|
const output = execSyncCommand("sudo", ["ddcutil", "getvcp", "10", "--brief"]);
|
||||||
|
const match = output ? output.match(/VCP 10 C (\d+) (\d+)/) : null;
|
||||||
|
if (match) {
|
||||||
|
return parseInt(match[2], 10);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (HARDWARE.display.brightness.path) {
|
||||||
|
const max = readFile(path.join(HARDWARE.display.brightness.path, "max_brightness"));
|
||||||
|
if (max) {
|
||||||
|
return parseInt(max, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current display brightness level using `/sys/class/backlight/.../brightness` or `ddcutil getvcp 10`.
|
||||||
*
|
*
|
||||||
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
|
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
@@ -591,16 +692,27 @@ const getDisplayBrightness = () => {
|
|||||||
if (!HARDWARE.support.displayBrightness) {
|
if (!HARDWARE.support.displayBrightness) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const brightness = fs.readFileSync(`${HARDWARE.display.brightness.path}/brightness`, "utf8").trim();
|
switch (HARDWARE.display.brightness.command) {
|
||||||
if (brightness) {
|
case "ddcutil":
|
||||||
const max = HARDWARE.display.brightness.max || 1;
|
const output = execSyncCommand("sudo", ["ddcutil", "getvcp", "10", "--brief"]);
|
||||||
return Math.max(1, Math.min(Math.round((parseInt(brightness, 10) / max) * 100), 100));
|
const match = output ? output.match(/VCP 10 C (\d+) (\d+)/) : null;
|
||||||
|
if (match) {
|
||||||
|
return parseInt(match[1], 10);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (HARDWARE.display.brightness.path) {
|
||||||
|
const brightness = readFile(path.join(HARDWARE.display.brightness.path, "brightness"));
|
||||||
|
if (brightness) {
|
||||||
|
const max = HARDWARE.display.brightness.value.max || 1;
|
||||||
|
return Math.max(1, Math.min(Math.round((parseInt(brightness, 10) / max) * 100), 100));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the display brightness level using `/sys/class/backlight/.../brightness`.
|
* Sets the display brightness level using `/sys/class/backlight/.../brightness` or `ddcutil setvcp 10`.
|
||||||
*
|
*
|
||||||
* This function takes a brightness value between 1 to 100 percent,
|
* This function takes a brightness value between 1 to 100 percent,
|
||||||
* maps it to the proper range and writes it to the system.
|
* maps it to the proper range and writes it to the system.
|
||||||
@@ -618,11 +730,23 @@ const setDisplayBrightness = (brightness, callback = null) => {
|
|||||||
if (typeof callback === "function") callback(null, "Invalid brightness");
|
if (typeof callback === "function") callback(null, "Invalid brightness");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const max = HARDWARE.display.brightness.max || 1;
|
switch (HARDWARE.display.brightness.command) {
|
||||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
case "ddcutil":
|
||||||
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
execAsyncCommand("sudo", ["ddcutil", "setvcp", "10", `${brightness}`], (reply, error) => {
|
||||||
proc.stdin.write(value.toString());
|
if (!error) {
|
||||||
proc.stdin.end();
|
fs.writeFileSync(path.join(APP.cache, "Brightness.vcp"), `${brightness}`);
|
||||||
|
}
|
||||||
|
if (typeof callback === "function") callback(reply, error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (HARDWARE.display.brightness.path) {
|
||||||
|
const max = HARDWARE.display.brightness.value.max || 1;
|
||||||
|
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
||||||
|
const proc = execAsyncCommand("sudo", ["tee", path.join(HARDWARE.display.brightness.path, "brightness")], callback);
|
||||||
|
proc.stdin.write(value.toString());
|
||||||
|
proc.stdin.end();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -657,7 +781,7 @@ const getAudioVolume = () => {
|
|||||||
}
|
}
|
||||||
const match = volume.match(/\/(\s*(\d+)%)/);
|
const match = volume.match(/\/(\s*(\d+)%)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
return Math.max(0, Math.min(100, mute.includes("yes") ? 0 : parseInt(match[2])));
|
return Math.max(0, Math.min(100, mute.includes("yes") ? 0 : parseInt(match[2], 10)));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -831,10 +955,11 @@ const commandExists = (name) => {
|
|||||||
*/
|
*/
|
||||||
const execSyncCommand = (cmd, args) => {
|
const execSyncCommand = (cmd, args) => {
|
||||||
try {
|
try {
|
||||||
|
console.debug(`hardware.js: execSyncCommand(${[cmd, ...args].join(" ")})`);
|
||||||
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
||||||
return output.trim().replace(/\0/g, "");
|
return output.trim().replace(/\0/g, "");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Execute Sync: '${[cmd, ...args].join(" ")}' --> ${error.message}`);
|
console.error(`Execute Sync: '${[cmd, ...args].join(" ")}' --> ${error.message}`.trim());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -848,6 +973,7 @@ const execSyncCommand = (cmd, args) => {
|
|||||||
* @returns {Object} The spawned process object.
|
* @returns {Object} The spawned process object.
|
||||||
*/
|
*/
|
||||||
const execAsyncCommand = (cmd, args, callback = null) => {
|
const execAsyncCommand = (cmd, args, callback = null) => {
|
||||||
|
console.debug(`hardware.js: execAsyncCommand(${[cmd, ...args].join(" ")})`);
|
||||||
let errorOutput = "";
|
let errorOutput = "";
|
||||||
let successOutput = "";
|
let successOutput = "";
|
||||||
let proc = cpr.spawn(cmd, args);
|
let proc = cpr.spawn(cmd, args);
|
||||||
@@ -883,6 +1009,7 @@ const execAsyncCommand = (cmd, args, callback = null) => {
|
|||||||
* @returns {Object} The spawned process object.
|
* @returns {Object} The spawned process object.
|
||||||
*/
|
*/
|
||||||
const execScriptCommand = (cmd, args, callback = null) => {
|
const execScriptCommand = (cmd, args, callback = null) => {
|
||||||
|
console.debug(`hardware.js: execScriptCommand(${[cmd, ...args].join(" ")})`);
|
||||||
let progress = 1;
|
let progress = 1;
|
||||||
let proc = cpr.spawn(cmd, args);
|
let proc = cpr.spawn(cmd, args);
|
||||||
if (typeof callback === "function") callback(progress, null);
|
if (typeof callback === "function") callback(progress, null);
|
||||||
@@ -902,7 +1029,7 @@ const execScriptCommand = (cmd, args, callback = null) => {
|
|||||||
for (const line of lines.split("\n").filter(Boolean)) {
|
for (const line of lines.split("\n").filter(Boolean)) {
|
||||||
const matches = line.match(/(\d{1,3})%/g);
|
const matches = line.match(/(\d{1,3})%/g);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const match = Math.max(...matches.map((p) => parseInt(p)));
|
const match = Math.max(...matches.map((p) => parseInt(p, 10)));
|
||||||
const percent = Math.floor(match / 10) * 10;
|
const percent = Math.floor(match / 10) * 10;
|
||||||
if (percent > 10 && percent > progress) {
|
if (percent > 10 && percent > progress) {
|
||||||
progress = percent;
|
progress = percent;
|
||||||
@@ -931,7 +1058,8 @@ const execScriptCommand = (cmd, args, callback = null) => {
|
|||||||
* @param {Function} callback - A callback function that receives the output or error.
|
* @param {Function} callback - A callback function that receives the output or error.
|
||||||
* @returns {Object} The spawned process object.
|
* @returns {Object} The spawned process object.
|
||||||
*/
|
*/
|
||||||
const commandMonitor = (cmd, args, callback) => {
|
const commandMonitor = (cmd, args, callback = null) => {
|
||||||
|
console.debug(`hardware.js: commandMonitor(${[cmd, ...args].join(" ")})`);
|
||||||
const proc = cpr.spawn(cmd, args);
|
const proc = cpr.spawn(cmd, args);
|
||||||
proc.stdout.on("data", (data) => {
|
proc.stdout.on("data", (data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -955,9 +1083,10 @@ const commandMonitor = (cmd, args, callback) => {
|
|||||||
* @param {Function} callback - A callback function that receives the changed property.
|
* @param {Function} callback - A callback function that receives the changed property.
|
||||||
* @returns {Object} The spawned process object.
|
* @returns {Object} The spawned process object.
|
||||||
*/
|
*/
|
||||||
const dbusMonitor = (path, callback) => {
|
const dbusMonitor = (path, callback = null) => {
|
||||||
const cmd = "dbus-monitor";
|
const cmd = "dbus-monitor";
|
||||||
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
|
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
|
||||||
|
console.debug(`hardware.js: dbusMonitor(${[cmd, ...args].join(" ")})`);
|
||||||
const proc = cpr.spawn(cmd, args);
|
const proc = cpr.spawn(cmd, args);
|
||||||
proc.stdout.on("data", (data) => {
|
proc.stdout.on("data", (data) => {
|
||||||
try {
|
try {
|
||||||
@@ -1006,6 +1135,7 @@ const dbusCall = (path, method, values, callback = null) => {
|
|||||||
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
||||||
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
||||||
try {
|
try {
|
||||||
|
console.debug(`hardware.js: dbusCall(${[cmd, ...args].join(" ")})`);
|
||||||
const output = cpr.execSync([cmd, ...args].join(" ").trim(), { encoding: "utf8" });
|
const output = cpr.execSync([cmd, ...args].join(" ").trim(), { encoding: "utf8" });
|
||||||
if (typeof callback === "function") callback(output.trim().replace(/\0/g, ""), null);
|
if (typeof callback === "function") callback(output.trim().replace(/\0/g, ""), null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1014,6 +1144,31 @@ const dbusCall = (path, method, values, callback = null) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads file content synchronously or asynchronously from the filesystem.
|
||||||
|
*
|
||||||
|
* @param {string} path - Path of the file.
|
||||||
|
* @param {boolean} sync - If true, reads the file synchronously, otherwise asynchronously.
|
||||||
|
* @returns {string|null|Promise<string|null>} The file content or null if an error occurs.
|
||||||
|
*/
|
||||||
|
const readFile = (path, sync = true) => {
|
||||||
|
if (!sync) {
|
||||||
|
return fsp
|
||||||
|
.readFile(path, "utf8")
|
||||||
|
.then((content) => content.trim())
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`Read File ${path} Async:`, error.message);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(path, "utf8").trim();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Read File ${path} Sync:`, error.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function for asynchronous interval calls.
|
* Helper function for asynchronous interval calls.
|
||||||
*
|
*
|
||||||
@@ -1026,7 +1181,7 @@ const interval = (callback, ms) => {
|
|||||||
try {
|
try {
|
||||||
await callback();
|
await callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Interval Callback:", error.message);
|
console.error("Interval Async:", error.message);
|
||||||
}
|
}
|
||||||
run();
|
run();
|
||||||
}, ms);
|
}, ms);
|
||||||
|
|||||||
+78
-27
@@ -53,18 +53,17 @@ const init = async () => {
|
|||||||
const options = user === null || password === null ? {} : { username: user, password: password };
|
const options = user === null || password === null ? {} : { username: user, password: password };
|
||||||
options.will = { topic: `${INTEGRATION.root}/kiosk/state`, payload: "Terminated", qos: 1, retain: true };
|
options.will = { topic: `${INTEGRATION.root}/kiosk/state`, payload: "Terminated", qos: 1, retain: true };
|
||||||
options.rejectUnauthorized = !("ignore_certificate_errors" in ARGS);
|
options.rejectUnauthorized = !("ignore_certificate_errors" in ARGS);
|
||||||
|
options.reconnectPeriod = 10 * 1000;
|
||||||
|
|
||||||
// Client connect
|
// Client connecting
|
||||||
console.info("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
|
const connection = `${user}:${masked}@${url.toString()}`;
|
||||||
|
console.info("MQTT Connecting:", connection);
|
||||||
INTEGRATION.client = mqtt.connect(url.toString(), options);
|
INTEGRATION.client = mqtt.connect(url.toString(), options);
|
||||||
INTEGRATION.client.setMaxListeners(20);
|
INTEGRATION.client.setMaxListeners(20);
|
||||||
|
|
||||||
// Client connected
|
// Client connected
|
||||||
INTEGRATION.client
|
INTEGRATION.client
|
||||||
.once("connect", () => {
|
.once("connect", () => {
|
||||||
console.info(`MQTT Connected: ${url.toString()}`);
|
|
||||||
process.stdout.write("\n");
|
|
||||||
|
|
||||||
// Init client controls
|
// Init client controls
|
||||||
initApp();
|
initApp();
|
||||||
initShutdown();
|
initShutdown();
|
||||||
@@ -94,6 +93,7 @@ const init = async () => {
|
|||||||
initLastActive();
|
initLastActive();
|
||||||
|
|
||||||
// Init client diagnostic
|
// Init client diagnostic
|
||||||
|
initScreenshot();
|
||||||
initHeartbeat();
|
initHeartbeat();
|
||||||
initErrors();
|
initErrors();
|
||||||
initVersion();
|
initVersion();
|
||||||
@@ -102,18 +102,36 @@ const init = async () => {
|
|||||||
INTEGRATION.initialized = true;
|
INTEGRATION.initialized = true;
|
||||||
|
|
||||||
// Register global events
|
// Register global events
|
||||||
|
EVENTS.on("updateApp", updateApp);
|
||||||
|
EVENTS.on("updateStatus", updateKiosk);
|
||||||
|
EVENTS.on("updateVolume", updateVolume);
|
||||||
|
EVENTS.on("updateKeyboard", updateKeyboard);
|
||||||
|
EVENTS.on("updatePage", () => {
|
||||||
|
updatePageNumber();
|
||||||
|
updatePageZoom();
|
||||||
|
updatePageUrl();
|
||||||
|
updateTheme();
|
||||||
|
});
|
||||||
EVENTS.on("updateDisplay", () => {
|
EVENTS.on("updateDisplay", () => {
|
||||||
updateDisplay();
|
updateDisplay();
|
||||||
updateLastActive();
|
updateLastActive();
|
||||||
});
|
});
|
||||||
EVENTS.on("updateStatus", updateKiosk);
|
EVENTS.on("updateScreenshot", updateScreenshot);
|
||||||
EVENTS.on("updateTheme", updateTheme);
|
|
||||||
EVENTS.on("updateVolume", updateVolume);
|
|
||||||
EVENTS.on("updateKeyboard", updateKeyboard);
|
|
||||||
EVENTS.on("consoleLog", updateErrors);
|
EVENTS.on("consoleLog", updateErrors);
|
||||||
})
|
})
|
||||||
|
.on("connect", () => {
|
||||||
|
console.info(`MQTT Connected: ${connection}`);
|
||||||
|
process.stdout.write("\n");
|
||||||
|
updateKiosk();
|
||||||
|
})
|
||||||
|
.on("offline", () => {
|
||||||
|
console.warn(`MQTT Disconnected: ${connection}`);
|
||||||
|
})
|
||||||
|
.on("reconnect", () => {
|
||||||
|
console.info(`MQTT Reconnecting: ${connection}`);
|
||||||
|
})
|
||||||
.on("error", (error) => {
|
.on("error", (error) => {
|
||||||
console.error("MQTT", error.message);
|
console.error("MQTT Error:", error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update time sensors periodically (30s)
|
// Update time sensors periodically (30s)
|
||||||
@@ -139,7 +157,6 @@ const init = async () => {
|
|||||||
if (APP.exiting) {
|
if (APP.exiting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateApp();
|
|
||||||
updatePackageUpgrades();
|
updatePackageUpgrades();
|
||||||
}, 3600 * 1000);
|
}, 3600 * 1000);
|
||||||
|
|
||||||
@@ -153,11 +170,8 @@ const update = async () => {
|
|||||||
if (!INTEGRATION.initialized || APP.exiting) {
|
if (!INTEGRATION.initialized || APP.exiting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.debug("integration.js: update()");
|
||||||
|
|
||||||
// Update client sensors
|
|
||||||
updatePageNumber();
|
|
||||||
updatePageZoom();
|
|
||||||
updatePageUrl();
|
|
||||||
updateNetworkAddress();
|
updateNetworkAddress();
|
||||||
updateUpTime();
|
updateUpTime();
|
||||||
updateLastActive();
|
updateLastActive();
|
||||||
@@ -180,6 +194,7 @@ const removeConfig = (type, config) => {
|
|||||||
}
|
}
|
||||||
const path = config.unique_id.replace(`${INTEGRATION.node}_`, "");
|
const path = config.unique_id.replace(`${INTEGRATION.node}_`, "");
|
||||||
const root = `${INTEGRATION.discovery}/${type}/${INTEGRATION.node}/${path}/config`;
|
const root = `${INTEGRATION.discovery}/${type}/${INTEGRATION.node}/${path}/config`;
|
||||||
|
console.debug(`integration.js: removeConfig(${path})`);
|
||||||
return INTEGRATION.client.publish(root, JSON.stringify({}), { qos: 1, retain: true });
|
return INTEGRATION.client.publish(root, JSON.stringify({}), { qos: 1, retain: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -196,6 +211,7 @@ const publishConfig = (type, config) => {
|
|||||||
}
|
}
|
||||||
const path = config.unique_id.replace(`${INTEGRATION.node}_`, "");
|
const path = config.unique_id.replace(`${INTEGRATION.node}_`, "");
|
||||||
const root = `${INTEGRATION.discovery}/${type}/${INTEGRATION.node}/${path}/config`;
|
const root = `${INTEGRATION.discovery}/${type}/${INTEGRATION.node}/${path}/config`;
|
||||||
|
console.debug(`integration.js: publishConfig(${path})`);
|
||||||
return INTEGRATION.client.publish(root, JSON.stringify(config), { qos: 1, retain: true });
|
return INTEGRATION.client.publish(root, JSON.stringify(config), { qos: 1, retain: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -421,7 +437,7 @@ const initTheme = () => {
|
|||||||
publishConfig("select", config)
|
publishConfig("select", config)
|
||||||
.on("message", (topic, message) => {
|
.on("message", (topic, message) => {
|
||||||
if (topic === config.command_topic) {
|
if (topic === config.command_topic) {
|
||||||
const theme = message.toString();
|
const theme = message.toString().toLowerCase();
|
||||||
console.verbose("Set Application Theme:", theme);
|
console.verbose("Set Application Theme:", theme);
|
||||||
WEBVIEW.theme.set(theme);
|
WEBVIEW.theme.set(theme);
|
||||||
}
|
}
|
||||||
@@ -448,10 +464,12 @@ const initDisplay = () => {
|
|||||||
unique_id: `${INTEGRATION.node}_display`,
|
unique_id: `${INTEGRATION.node}_display`,
|
||||||
command_topic: `${root}/power/set`,
|
command_topic: `${root}/power/set`,
|
||||||
state_topic: `${root}/power/state`,
|
state_topic: `${root}/power/state`,
|
||||||
|
supported_color_modes: ["onoff"],
|
||||||
icon: "mdi:monitor-shimmer",
|
icon: "mdi:monitor-shimmer",
|
||||||
platform: "light",
|
platform: "light",
|
||||||
device: INTEGRATION.device,
|
device: INTEGRATION.device,
|
||||||
...(HARDWARE.support.displayBrightness && {
|
...(HARDWARE.support.displayBrightness && {
|
||||||
|
supported_color_modes: ["brightness"],
|
||||||
brightness_command_topic: `${root}/brightness/set`,
|
brightness_command_topic: `${root}/brightness/set`,
|
||||||
brightness_state_topic: `${root}/brightness/state`,
|
brightness_state_topic: `${root}/brightness/state`,
|
||||||
brightness_scale: 100,
|
brightness_scale: 100,
|
||||||
@@ -496,8 +514,8 @@ const initDisplay = () => {
|
|||||||
const updateDisplay = async () => {
|
const updateDisplay = async () => {
|
||||||
const status = hardware.getDisplayStatus();
|
const status = hardware.getDisplayStatus();
|
||||||
const brightness = hardware.getDisplayBrightness();
|
const brightness = hardware.getDisplayBrightness();
|
||||||
publishState("display/power", status);
|
|
||||||
publishState("display/brightness", brightness);
|
publishState("display/brightness", brightness);
|
||||||
|
publishState("display/power", status);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -607,9 +625,11 @@ const initPageNumber = () => {
|
|||||||
.on("message", (topic, message) => {
|
.on("message", (topic, message) => {
|
||||||
if (topic === config.command_topic) {
|
if (topic === config.command_topic) {
|
||||||
const number = parseInt(message, 10);
|
const number = parseInt(message, 10);
|
||||||
console.verbose("Set Page Number:", number);
|
if (WEBVIEW.viewActive && number) {
|
||||||
WEBVIEW.viewActive = number || 1;
|
console.verbose("Set Page Number:", number);
|
||||||
EVENTS.emit("updateView");
|
WEBVIEW.viewActive = number;
|
||||||
|
EVENTS.emit("updateView");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribe(config.command_topic);
|
.subscribe(config.command_topic);
|
||||||
@@ -647,9 +667,11 @@ const initPageZoom = () => {
|
|||||||
.on("message", (topic, message) => {
|
.on("message", (topic, message) => {
|
||||||
if (topic === config.command_topic) {
|
if (topic === config.command_topic) {
|
||||||
const zoom = parseInt(message, 10);
|
const zoom = parseInt(message, 10);
|
||||||
console.verbose("Set Page Zoom:", zoom);
|
if (WEBVIEW.viewActive && zoom) {
|
||||||
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.setZoomFactor(zoom / 100.0);
|
console.verbose("Set Page Zoom:", zoom);
|
||||||
EVENTS.emit("updateView");
|
WEBVIEW.zoom.set(zoom);
|
||||||
|
EVENTS.emit("updateView");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribe(config.command_topic);
|
.subscribe(config.command_topic);
|
||||||
@@ -660,7 +682,7 @@ const initPageZoom = () => {
|
|||||||
* Updates the page zoom via the mqtt connection.
|
* Updates the page zoom via the mqtt connection.
|
||||||
*/
|
*/
|
||||||
const updatePageZoom = async () => {
|
const updatePageZoom = async () => {
|
||||||
const pageZoom = Math.round(WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getZoomFactor() * 100.0);
|
const pageZoom = WEBVIEW.zoom.get();
|
||||||
publishState("page_zoom", pageZoom);
|
publishState("page_zoom", pageZoom);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -683,8 +705,10 @@ const initPageUrl = () => {
|
|||||||
.on("message", (topic, message) => {
|
.on("message", (topic, message) => {
|
||||||
if (topic === config.command_topic) {
|
if (topic === config.command_topic) {
|
||||||
const url = message.toString();
|
const url = message.toString();
|
||||||
console.verbose("Set Page Url:", url);
|
if (WEBVIEW.viewActive && url) {
|
||||||
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.loadURL(url);
|
console.verbose("Set Page Url:", url);
|
||||||
|
WEBVIEW.views[WEBVIEW.viewActive].webContents.loadURL(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribe(config.command_topic);
|
.subscribe(config.command_topic);
|
||||||
@@ -698,7 +722,7 @@ const updatePageUrl = async () => {
|
|||||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive || 1];
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive || 1];
|
||||||
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getURL();
|
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getURL();
|
||||||
const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl;
|
const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl;
|
||||||
publishState("page_url", pageUrl);
|
publishState("page_url", pageUrl.length < 255 ? pageUrl : null);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1040,6 +1064,33 @@ const updateLastActive = async () => {
|
|||||||
publishAttributes("last_active", tracker);
|
publishAttributes("last_active", tracker);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the page screenshot.
|
||||||
|
*/
|
||||||
|
const initScreenshot = () => {
|
||||||
|
const root = `${INTEGRATION.root}/screenshot`;
|
||||||
|
const config = {
|
||||||
|
name: "Screenshot",
|
||||||
|
unique_id: `${INTEGRATION.node}_screenshot`,
|
||||||
|
image_topic: `${root}/state`,
|
||||||
|
image_encoding: "b64",
|
||||||
|
content_type: "image/png",
|
||||||
|
entity_category: "diagnostic",
|
||||||
|
icon: "mdi:image-area",
|
||||||
|
device: INTEGRATION.device,
|
||||||
|
};
|
||||||
|
publishConfig("image", config);
|
||||||
|
updateScreenshot();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the page screenshot via the mqtt connection.
|
||||||
|
*/
|
||||||
|
const updateScreenshot = async () => {
|
||||||
|
const screenshot = WEBVIEW.tracker.screenshot;
|
||||||
|
publishState("screenshot", screenshot);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the heartbeat sensor.
|
* Initializes the heartbeat sensor.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+404
-110
@@ -29,6 +29,7 @@ global.WEBVIEW = global.WEBVIEW || {
|
|||||||
widget: {
|
widget: {
|
||||||
focused: null,
|
focused: null,
|
||||||
},
|
},
|
||||||
|
screenshot: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,69 +47,108 @@ const init = async () => {
|
|||||||
console.error("Please provide the '--web-url' parameter with http(s)");
|
console.error("Please provide the '--web-url' parameter with http(s)");
|
||||||
return app.quit();
|
return app.quit();
|
||||||
}
|
}
|
||||||
session.defaultSession.clearCache();
|
|
||||||
|
// Clear cache and storage
|
||||||
|
await session.defaultSession.clearCache();
|
||||||
|
if (ARGS.app_reset === "storage") {
|
||||||
|
await session.defaultSession.clearStorageData();
|
||||||
|
}
|
||||||
|
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
const debug = "app_debug" in ARGS;
|
const debug = "app_debug" in ARGS;
|
||||||
const widget = ARGS.web_widget ? ARGS.web_widget === "true" : true;
|
const widget = ARGS.web_widget ? ARGS.web_widget === "true" : true;
|
||||||
const zoom = !isNaN(parseFloat(ARGS.web_zoom)) ? parseFloat(ARGS.web_zoom) : 1.25;
|
|
||||||
const theme = ["light", "dark"].includes(ARGS.web_theme) ? ARGS.web_theme : "dark";
|
const theme = ["light", "dark"].includes(ARGS.web_theme) ? ARGS.web_theme : "dark";
|
||||||
const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url);
|
const zoom = (!isNaN(parseFloat(ARGS.web_zoom)) ? parseFloat(ARGS.web_zoom) : 1.25) * 100;
|
||||||
|
const urls = [loaderHtml(40, 1.0, theme), ...ARGS.web_url];
|
||||||
|
|
||||||
// Init global properties
|
// Init global controls
|
||||||
WEBVIEW.viewActive = 0;
|
WEBVIEW.statusEnabled = !debug;
|
||||||
WEBVIEW.viewUrls = urls;
|
|
||||||
WEBVIEW.viewZoom = zoom;
|
|
||||||
WEBVIEW.statusEnabled = true;
|
|
||||||
WEBVIEW.pagerEnabled = widget;
|
WEBVIEW.pagerEnabled = widget;
|
||||||
WEBVIEW.widgetEnabled = widget;
|
WEBVIEW.widgetEnabled = widget;
|
||||||
WEBVIEW.navigationEnabled = widget;
|
WEBVIEW.navigationEnabled = widget;
|
||||||
|
|
||||||
// Init global theme
|
// Init global views
|
||||||
|
WEBVIEW.views = [];
|
||||||
|
WEBVIEW.viewUrls = urls;
|
||||||
|
WEBVIEW.viewActive = 0;
|
||||||
|
|
||||||
|
// Init global display
|
||||||
|
const display = screen.getPrimaryDisplay();
|
||||||
|
WEBVIEW.display = display ? display.workAreaSize : {};
|
||||||
|
if (!WEBVIEW.display.width || !WEBVIEW.display.height) {
|
||||||
|
WEBVIEW.display = { width: 800, height: 600 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init global theme manager
|
||||||
WEBVIEW.theme = {
|
WEBVIEW.theme = {
|
||||||
set: (name) => {
|
set: function (value) {
|
||||||
nativeTheme.themeSource = name.toLowerCase();
|
const theme = value.toLowerCase();
|
||||||
|
nativeTheme.themeSource = theme;
|
||||||
|
if (typeof this.callback === "function") this.callback();
|
||||||
},
|
},
|
||||||
get: () => {
|
get: function () {
|
||||||
return nativeTheme.shouldUseDarkColors ? "dark" : "light";
|
return nativeTheme.shouldUseDarkColors ? "dark" : "light";
|
||||||
},
|
},
|
||||||
toggle: () => {
|
toggle: function () {
|
||||||
nativeTheme.themeSource = nativeTheme.shouldUseDarkColors ? "light" : "dark";
|
this.set(nativeTheme.shouldUseDarkColors ? "light" : "dark");
|
||||||
},
|
},
|
||||||
register: () => {
|
reset: function () {
|
||||||
nativeTheme.on("updated", () => {
|
this.set(this.default);
|
||||||
const theme = WEBVIEW.theme.get();
|
},
|
||||||
WEBVIEW.statusTheme = theme;
|
store: function () {
|
||||||
WEBVIEW.widgetTheme = theme;
|
cookieStore("web-theme", this.get());
|
||||||
WEBVIEW.navigationTheme = theme;
|
},
|
||||||
if (WEBVIEW.initialized) {
|
init: function (value, callback) {
|
||||||
resizeView();
|
this.callback = callback;
|
||||||
}
|
this.default = value;
|
||||||
EVENTS.emit("updateTheme");
|
this.reset();
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
WEBVIEW.theme.register();
|
|
||||||
WEBVIEW.theme.set(theme);
|
|
||||||
|
|
||||||
// Init global root window
|
// Init global zoom manager
|
||||||
WEBVIEW.display = screen.getPrimaryDisplay().workAreaSize;
|
WEBVIEW.zoom = {
|
||||||
|
set: function (value) {
|
||||||
|
const zoom = Math.max(0.25, Math.min(4.0, value / 100.0));
|
||||||
|
WEBVIEW.views[WEBVIEW.viewActive].webContents.setZoomFactor(zoom);
|
||||||
|
if (typeof this.callback === "function") this.callback();
|
||||||
|
},
|
||||||
|
get: function () {
|
||||||
|
const zoom = WEBVIEW.views[WEBVIEW.viewActive].webContents.getZoomFactor();
|
||||||
|
return Math.round(zoom * 100.0);
|
||||||
|
},
|
||||||
|
minus: function () {
|
||||||
|
this.set(Math.floor((this.get() - 1) / 10) * 10);
|
||||||
|
},
|
||||||
|
plus: function () {
|
||||||
|
this.set(Math.ceil((this.get() + 1) / 10) * 10);
|
||||||
|
},
|
||||||
|
reset: function () {
|
||||||
|
this.set(this.default);
|
||||||
|
},
|
||||||
|
store: function () {
|
||||||
|
cookieStore("web-zoom", this.get());
|
||||||
|
},
|
||||||
|
init: function (value, callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.default = value;
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init root window
|
||||||
WEBVIEW.window = new BaseWindow({
|
WEBVIEW.window = new BaseWindow({
|
||||||
title: APP.title,
|
title: APP.title,
|
||||||
icon: APP.icon,
|
icon: APP.icon,
|
||||||
fullscreen: !debug,
|
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
frame: debug || !WEBVIEW.statusEnabled,
|
frame: !WEBVIEW.statusEnabled,
|
||||||
width: Math.floor(WEBVIEW.display.width * 0.9),
|
width: Math.floor(WEBVIEW.display.width * 0.85),
|
||||||
height: Math.floor(WEBVIEW.display.height * 0.9),
|
height: Math.floor(WEBVIEW.display.height * 0.75),
|
||||||
minWidth: 136,
|
minWidth: 136,
|
||||||
minHeight: 136,
|
minHeight: 136,
|
||||||
});
|
});
|
||||||
WEBVIEW.window.setMenuBarVisibility(false);
|
|
||||||
|
|
||||||
// Init global webview
|
// Init global webview
|
||||||
WEBVIEW.views = [];
|
WEBVIEW.viewUrls.forEach((url, i) => {
|
||||||
urls.forEach((url, i) => {
|
|
||||||
const view = new WebContentsView({
|
const view = new WebContentsView({
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
@@ -172,6 +212,12 @@ const init = async () => {
|
|||||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.navigation);
|
WEBVIEW.window.contentView.addChildView(WEBVIEW.navigation);
|
||||||
WEBVIEW.navigation.webContents.loadFile(path.join(APP.path, "html", "navigation.html"));
|
WEBVIEW.navigation.webContents.loadFile(path.join(APP.path, "html", "navigation.html"));
|
||||||
|
|
||||||
|
// Init global layout
|
||||||
|
const { width, height, x, y } = WEBVIEW.window.getBounds();
|
||||||
|
console.info(`Open Window: ${width}x${height}+${x}+${y}`);
|
||||||
|
WEBVIEW.theme.init(theme, updateTheme);
|
||||||
|
WEBVIEW.zoom.init(zoom, updateZoom);
|
||||||
|
|
||||||
// Register global events
|
// Register global events
|
||||||
await windowEvents();
|
await windowEvents();
|
||||||
await widgetEvents();
|
await widgetEvents();
|
||||||
@@ -190,18 +236,61 @@ const update = async () => {
|
|||||||
if (!WEBVIEW.initialized || APP.exiting) {
|
if (!WEBVIEW.initialized || APP.exiting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.debug("webview.js: update()");
|
||||||
|
|
||||||
// Update window status
|
// Update window status
|
||||||
updateStatus();
|
updateStatus();
|
||||||
|
|
||||||
|
// Update pager status
|
||||||
|
updatePager();
|
||||||
|
|
||||||
// Update widget status
|
// Update widget status
|
||||||
updateWidget();
|
updateWidget();
|
||||||
|
|
||||||
// Update navigation status
|
// Update navigation status
|
||||||
updateNavigation();
|
updateNavigation();
|
||||||
|
};
|
||||||
|
|
||||||
// Update integration sensor
|
/**
|
||||||
integration.update();
|
* Updates the application theme.
|
||||||
|
*/
|
||||||
|
const updateTheme = () => {
|
||||||
|
if (APP.exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const theme = WEBVIEW.theme.get();
|
||||||
|
|
||||||
|
// Update controls theme
|
||||||
|
WEBVIEW.statusTheme = theme;
|
||||||
|
WEBVIEW.pagerTheme = theme;
|
||||||
|
WEBVIEW.widgetTheme = theme;
|
||||||
|
WEBVIEW.navigationTheme = theme;
|
||||||
|
WEBVIEW.window.setMenuBarVisibility(false);
|
||||||
|
|
||||||
|
// Update application theme
|
||||||
|
WEBVIEW.theme.store();
|
||||||
|
if (WEBVIEW.initialized) {
|
||||||
|
console.info("Update Application Theme:", theme);
|
||||||
|
resizeView();
|
||||||
|
}
|
||||||
|
EVENTS.emit("updatePage");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the page zoom.
|
||||||
|
*/
|
||||||
|
const updateZoom = () => {
|
||||||
|
if (APP.exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const zoom = WEBVIEW.zoom.get();
|
||||||
|
|
||||||
|
// Update page zoom
|
||||||
|
WEBVIEW.zoom.store();
|
||||||
|
if (WEBVIEW.initialized) {
|
||||||
|
console.info("Update Page Zoom:", zoom);
|
||||||
|
}
|
||||||
|
EVENTS.emit("updatePage");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -211,7 +300,10 @@ const updateView = () => {
|
|||||||
if (!WEBVIEW.viewActive) {
|
if (!WEBVIEW.viewActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL();
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
|
const url = view.webContents.getURL();
|
||||||
|
|
||||||
|
// Build window title
|
||||||
const host = url.startsWith("data:") ? "whoopsie" : new URL(url).host;
|
const host = url.startsWith("data:") ? "whoopsie" : new URL(url).host;
|
||||||
const title = `${APP.title} - ${host} (${WEBVIEW.viewActive})`;
|
const title = `${APP.title} - ${host} (${WEBVIEW.viewActive})`;
|
||||||
const previous = WEBVIEW.window.getTitle();
|
const previous = WEBVIEW.window.getTitle();
|
||||||
@@ -227,6 +319,24 @@ const updateView = () => {
|
|||||||
WEBVIEW.views.forEach((view, i) => {
|
WEBVIEW.views.forEach((view, i) => {
|
||||||
view.setVisible(i === WEBVIEW.viewActive);
|
view.setVisible(i === WEBVIEW.viewActive);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update stored theme and zoom
|
||||||
|
cookieStore("web-theme").then((value) => {
|
||||||
|
if (value && value !== WEBVIEW.theme.get()) {
|
||||||
|
WEBVIEW.theme.set(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cookieStore("web-zoom").then((value) => {
|
||||||
|
if (value && value !== WEBVIEW.zoom.get()) {
|
||||||
|
WEBVIEW.zoom.set(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update webview screenshot
|
||||||
|
captureView(1000).then(() => {
|
||||||
|
EVENTS.emit("updateScreenshot");
|
||||||
|
});
|
||||||
|
EVENTS.emit("updatePage");
|
||||||
update();
|
update();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,16 +364,25 @@ const updateStatus = () => {
|
|||||||
EVENTS.emit("updateStatus");
|
EVENTS.emit("updateStatus");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the pager control.
|
||||||
|
*/
|
||||||
|
const updatePager = () => {
|
||||||
|
// Disable pager buttons
|
||||||
|
WEBVIEW.pager.webContents.send("button-disabled", {
|
||||||
|
id: "previous",
|
||||||
|
disabled: WEBVIEW.viewActive <= 1,
|
||||||
|
});
|
||||||
|
WEBVIEW.pager.webContents.send("button-disabled", {
|
||||||
|
id: "next",
|
||||||
|
disabled: WEBVIEW.viewActive >= WEBVIEW.viewUrls.length - 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the widget control.
|
* Updates the widget control.
|
||||||
*/
|
*/
|
||||||
const updateWidget = () => {
|
const updateWidget = () => {
|
||||||
// Hide status button
|
|
||||||
WEBVIEW.widget.webContents.send("button-hidden", {
|
|
||||||
id: "fullscreen",
|
|
||||||
hidden: !WEBVIEW.statusEnabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hide keyboard button
|
// Hide keyboard button
|
||||||
WEBVIEW.widget.webContents.send("button-hidden", {
|
WEBVIEW.widget.webContents.send("button-hidden", {
|
||||||
id: "keyboard",
|
id: "keyboard",
|
||||||
@@ -281,6 +400,9 @@ const updateWidget = () => {
|
|||||||
* Updates the navigation control.
|
* Updates the navigation control.
|
||||||
*/
|
*/
|
||||||
const updateNavigation = () => {
|
const updateNavigation = () => {
|
||||||
|
if (!WEBVIEW.viewActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||||
const currentUrl = view.webContents.getURL();
|
const currentUrl = view.webContents.getURL();
|
||||||
@@ -319,11 +441,11 @@ const updateNavigation = () => {
|
|||||||
// Disable zoom buttons
|
// Disable zoom buttons
|
||||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||||
id: "minus",
|
id: "minus",
|
||||||
disabled: view.webContents.getZoomFactor().toFixed(2) <= 0.25,
|
disabled: WEBVIEW.zoom.get() <= 25,
|
||||||
});
|
});
|
||||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||||
id: "plus",
|
id: "plus",
|
||||||
disabled: view.webContents.getZoomFactor().toFixed(2) >= 4.0,
|
disabled: WEBVIEW.zoom.get() >= 400,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable history buttons
|
// Disable history buttons
|
||||||
@@ -402,26 +524,31 @@ const toggleStatus = (force = null) => {
|
|||||||
* Decreases page zoom on the active webview.
|
* Decreases page zoom on the active webview.
|
||||||
*/
|
*/
|
||||||
const zoomMinus = () => {
|
const zoomMinus = () => {
|
||||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
if (!WEBVIEW.viewActive) {
|
||||||
const zoom = view.webContents.getZoomFactor() - 0.1;
|
return;
|
||||||
view.webContents.setZoomFactor(Math.max(0.25, Math.round(zoom * 10) / 10));
|
}
|
||||||
update();
|
WEBVIEW.zoom.minus();
|
||||||
|
updateView();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increases page zoom on the active webview.
|
* Increases page zoom on the active webview.
|
||||||
*/
|
*/
|
||||||
const zoomPlus = () => {
|
const zoomPlus = () => {
|
||||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
if (!WEBVIEW.viewActive) {
|
||||||
const zoom = view.webContents.getZoomFactor() + 0.1;
|
return;
|
||||||
view.webContents.setZoomFactor(Math.min(4.0, Math.round(zoom * 10) / 10));
|
}
|
||||||
update();
|
WEBVIEW.zoom.plus();
|
||||||
|
updateView();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates backward in the history of the active webview.
|
* Navigates backward in the history of the active webview.
|
||||||
*/
|
*/
|
||||||
const historyBackward = () => {
|
const historyBackward = () => {
|
||||||
|
if (!WEBVIEW.viewActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
if (view.webContents.navigationHistory.canGoBack()) {
|
if (view.webContents.navigationHistory.canGoBack()) {
|
||||||
view.webContents.navigationHistory.goBack();
|
view.webContents.navigationHistory.goBack();
|
||||||
@@ -432,6 +559,9 @@ const historyBackward = () => {
|
|||||||
* Navigates forward in the history of the active webview.
|
* Navigates forward in the history of the active webview.
|
||||||
*/
|
*/
|
||||||
const historyForward = () => {
|
const historyForward = () => {
|
||||||
|
if (!WEBVIEW.viewActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
if (view.webContents.navigationHistory.canGoForward()) {
|
if (view.webContents.navigationHistory.canGoForward()) {
|
||||||
view.webContents.navigationHistory.goForward();
|
view.webContents.navigationHistory.goForward();
|
||||||
@@ -442,7 +572,12 @@ const historyForward = () => {
|
|||||||
* Activates the previous webview page.
|
* Activates the previous webview page.
|
||||||
*/
|
*/
|
||||||
const previousView = () => {
|
const previousView = () => {
|
||||||
if (WEBVIEW.viewActive > 1) WEBVIEW.viewActive--;
|
if (!WEBVIEW.viewActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (WEBVIEW.viewActive > 1) {
|
||||||
|
WEBVIEW.viewActive--;
|
||||||
|
}
|
||||||
updateView();
|
updateView();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -450,7 +585,12 @@ const previousView = () => {
|
|||||||
* Activates the next webview page.
|
* Activates the next webview page.
|
||||||
*/
|
*/
|
||||||
const nextView = () => {
|
const nextView = () => {
|
||||||
if (WEBVIEW.viewActive < WEBVIEW.views.length - 1) WEBVIEW.viewActive++;
|
if (!WEBVIEW.viewActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (WEBVIEW.viewActive < WEBVIEW.views.length - 1) {
|
||||||
|
WEBVIEW.viewActive++;
|
||||||
|
}
|
||||||
updateView();
|
updateView();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -458,12 +598,21 @@ const nextView = () => {
|
|||||||
* Reloads the default url and settings on the active webview.
|
* Reloads the default url and settings on the active webview.
|
||||||
*/
|
*/
|
||||||
const homeView = () => {
|
const homeView = () => {
|
||||||
|
if (!WEBVIEW.viewActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||||
const currentUrl = view.webContents.getURL();
|
const currentUrl = view.webContents.getURL();
|
||||||
|
|
||||||
// Reset log history
|
// Clear logs, cache and history
|
||||||
APP.logs = [];
|
APP.logs = [];
|
||||||
|
view.webContents.session.clearCache();
|
||||||
|
view.webContents.navigationHistory.clear();
|
||||||
|
|
||||||
|
// Reset page zoom and theme
|
||||||
|
WEBVIEW.zoom.reset();
|
||||||
|
WEBVIEW.theme.reset();
|
||||||
|
|
||||||
// Reload the default url or refresh the page
|
// Reload the default url or refresh the page
|
||||||
if (currentUrl != defaultUrl) {
|
if (currentUrl != defaultUrl) {
|
||||||
@@ -471,25 +620,22 @@ const homeView = () => {
|
|||||||
} else {
|
} else {
|
||||||
view.webContents.reloadIgnoringCache();
|
view.webContents.reloadIgnoringCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset page zoom and history
|
|
||||||
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
|
||||||
setTimeout(() => {
|
|
||||||
view.webContents.navigationHistory.clear();
|
|
||||||
update();
|
|
||||||
}, 2000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reloads the current url on the active webview.
|
* Reloads the current url on the active webview.
|
||||||
*/
|
*/
|
||||||
const reloadView = () => {
|
const reloadView = () => {
|
||||||
|
if (!WEBVIEW.viewActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||||
const currentUrl = view.webContents.getURL();
|
const currentUrl = view.webContents.getURL();
|
||||||
|
|
||||||
// Reset log history
|
// Clear logs and cache
|
||||||
APP.logs = [];
|
APP.logs = [];
|
||||||
|
view.webContents.session.clearCache();
|
||||||
|
|
||||||
// Reload the default url or refresh the page
|
// Reload the default url or refresh the page
|
||||||
if (currentUrl.startsWith("data:")) {
|
if (currentUrl.startsWith("data:")) {
|
||||||
@@ -497,7 +643,6 @@ const reloadView = () => {
|
|||||||
} else {
|
} else {
|
||||||
view.webContents.reloadIgnoringCache();
|
view.webContents.reloadIgnoringCache();
|
||||||
}
|
}
|
||||||
update();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -511,7 +656,8 @@ const resizeView = () => {
|
|||||||
const widget = { width: 60, height: 200 };
|
const widget = { width: 60, height: 200 };
|
||||||
|
|
||||||
// Update view size
|
// Update view size
|
||||||
WEBVIEW.views.forEach((view) => {
|
WEBVIEW.views.forEach((view, i) => {
|
||||||
|
console.debug(`webview.js: resizeView(${i})`);
|
||||||
view.setBounds({
|
view.setBounds({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: status.height,
|
y: status.height,
|
||||||
@@ -529,7 +675,7 @@ const resizeView = () => {
|
|||||||
height: pager.height - status.height - navigation.height,
|
height: pager.height - status.height - navigation.height,
|
||||||
});
|
});
|
||||||
WEBVIEW.pager.webContents.send("data-theme", {
|
WEBVIEW.pager.webContents.send("data-theme", {
|
||||||
theme: "hidden",
|
theme: WEBVIEW.viewUrls.length > 2 ? WEBVIEW.pagerTheme : "hidden",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,20 +717,28 @@ const resizeView = () => {
|
|||||||
theme: WEBVIEW.navigationTheme,
|
theme: WEBVIEW.navigationTheme,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update webview screenshot
|
||||||
|
captureView(1000).then(() => {
|
||||||
|
EVENTS.emit("updateScreenshot");
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register window events and handler.
|
* Register window events and handler.
|
||||||
*/
|
*/
|
||||||
const windowEvents = async () => {
|
const windowEvents = async () => {
|
||||||
// Handle window resize events
|
console.debug("webview.js: windowEvents()");
|
||||||
WEBVIEW.window.on("ready-to-show", resizeView);
|
|
||||||
WEBVIEW.window.on("resize", resizeView);
|
|
||||||
resizeView();
|
|
||||||
|
|
||||||
// Handle window status updates
|
// Handle window status updates
|
||||||
WEBVIEW.window.setStatus = async (status) => {
|
WEBVIEW.window.setStatus = async (status) => {
|
||||||
|
if (APP.exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const apply = (func, ...args) => {
|
const apply = (func, ...args) => {
|
||||||
|
if (APP.exiting) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
func.apply(WEBVIEW.window, args);
|
func.apply(WEBVIEW.window, args);
|
||||||
return new Promise((r) => setTimeout(r, 50));
|
return new Promise((r) => setTimeout(r, 50));
|
||||||
};
|
};
|
||||||
@@ -625,6 +779,7 @@ const windowEvents = async () => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "Terminated":
|
case "Terminated":
|
||||||
|
process.exitCode = 0;
|
||||||
app.quit();
|
app.quit();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -640,6 +795,10 @@ const windowEvents = async () => {
|
|||||||
WEBVIEW.window.on("enter-full-screen", WEBVIEW.window.onStatus);
|
WEBVIEW.window.on("enter-full-screen", WEBVIEW.window.onStatus);
|
||||||
WEBVIEW.window.on("leave-full-screen", WEBVIEW.window.onStatus);
|
WEBVIEW.window.on("leave-full-screen", WEBVIEW.window.onStatus);
|
||||||
|
|
||||||
|
// Handle window resize events
|
||||||
|
WEBVIEW.window.on("resize", resizeView);
|
||||||
|
resizeView();
|
||||||
|
|
||||||
// Handle global shortcut events
|
// Handle global shortcut events
|
||||||
globalShortcut.register("Control+Left", () => {
|
globalShortcut.register("Control+Left", () => {
|
||||||
previousView();
|
previousView();
|
||||||
@@ -660,8 +819,8 @@ const windowEvents = async () => {
|
|||||||
historyForward();
|
historyForward();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for window touch events (1s)
|
// Check for window touch events (every full 1s)
|
||||||
setInterval(() => {
|
interval(() => {
|
||||||
if (APP.exiting) {
|
if (APP.exiting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -676,7 +835,7 @@ const windowEvents = async () => {
|
|||||||
}
|
}
|
||||||
toggleNavigation("OFF");
|
toggleNavigation("OFF");
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1 * 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -686,6 +845,7 @@ const widgetEvents = async () => {
|
|||||||
if (!WEBVIEW.widgetEnabled) {
|
if (!WEBVIEW.widgetEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.debug("webview.js: widgetEvents()");
|
||||||
|
|
||||||
// Handle widget focus events
|
// Handle widget focus events
|
||||||
WEBVIEW.widget.webContents.on("focus", () => {
|
WEBVIEW.widget.webContents.on("focus", () => {
|
||||||
@@ -704,6 +864,7 @@ const widgetEvents = async () => {
|
|||||||
theme: WEBVIEW.widgetTheme,
|
theme: WEBVIEW.widgetTheme,
|
||||||
});
|
});
|
||||||
WEBVIEW.tracker.widget.focused = true;
|
WEBVIEW.tracker.widget.focused = true;
|
||||||
|
console.debug("webview.js: widgetEvents(widget-focus)");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle widget blur events
|
// Handle widget blur events
|
||||||
@@ -722,12 +883,17 @@ const widgetEvents = async () => {
|
|||||||
theme: "hidden",
|
theme: "hidden",
|
||||||
});
|
});
|
||||||
WEBVIEW.tracker.widget.focused = false;
|
WEBVIEW.tracker.widget.focused = false;
|
||||||
|
console.debug("webview.js: widgetEvents(widget-blur)");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle widget button click events
|
// Handle widget button click events
|
||||||
ipcMain.on("button-click", (e, button) => {
|
ipcMain.on("button-click", (e, button) => {
|
||||||
|
console.debug(`webview.js: widgetEvents(button-click-${button.id})`);
|
||||||
WEBVIEW.tracker.pointer.time = new Date();
|
WEBVIEW.tracker.pointer.time = new Date();
|
||||||
switch (button.id) {
|
switch (button.id) {
|
||||||
|
case "fullscreen":
|
||||||
|
WEBVIEW.window.setStatus(WEBVIEW.tracker.window.status === "Fullscreen" ? "Framed" : "Fullscreen");
|
||||||
|
break;
|
||||||
case "theme":
|
case "theme":
|
||||||
WEBVIEW.theme.toggle();
|
WEBVIEW.theme.toggle();
|
||||||
break;
|
break;
|
||||||
@@ -748,14 +914,13 @@ const statusEvents = async () => {
|
|||||||
if (!WEBVIEW.statusEnabled) {
|
if (!WEBVIEW.statusEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.debug("webview.js: statusEvents()");
|
||||||
|
|
||||||
// Handle status button click events
|
// Handle status button click events
|
||||||
ipcMain.on("button-click", (e, button) => {
|
ipcMain.on("button-click", (e, button) => {
|
||||||
|
console.debug(`webview.js: statusEvents(button-click-${button.id})`);
|
||||||
WEBVIEW.tracker.pointer.time = new Date();
|
WEBVIEW.tracker.pointer.time = new Date();
|
||||||
switch (button.id) {
|
switch (button.id) {
|
||||||
case "fullscreen":
|
|
||||||
WEBVIEW.window.setStatus(WEBVIEW.tracker.window.status === "Fullscreen" ? "Framed" : "Fullscreen");
|
|
||||||
break;
|
|
||||||
case "minimize":
|
case "minimize":
|
||||||
WEBVIEW.window.setStatus("Minimized");
|
WEBVIEW.window.setStatus("Minimized");
|
||||||
break;
|
break;
|
||||||
@@ -790,10 +955,12 @@ const navigationEvents = async () => {
|
|||||||
if (!WEBVIEW.navigationEnabled) {
|
if (!WEBVIEW.navigationEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.debug("webview.js: navigationEvents()");
|
||||||
|
|
||||||
// Handle input blur events
|
// Handle input blur events
|
||||||
let selected = false;
|
let selected = false;
|
||||||
ipcMain.on("input-blur", (e, input) => {
|
ipcMain.on("input-blur", (e, input) => {
|
||||||
|
console.debug(`webview.js: navigationEvents(input-blur-${input.id})`);
|
||||||
switch (input.id) {
|
switch (input.id) {
|
||||||
case "url":
|
case "url":
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -809,6 +976,7 @@ const navigationEvents = async () => {
|
|||||||
|
|
||||||
// Handle input focus events
|
// Handle input focus events
|
||||||
ipcMain.on("input-focus", (e, input) => {
|
ipcMain.on("input-focus", (e, input) => {
|
||||||
|
console.debug(`webview.js: navigationEvents(input-focus-${input.id})`);
|
||||||
WEBVIEW.tracker.pointer.time = new Date();
|
WEBVIEW.tracker.pointer.time = new Date();
|
||||||
switch (input.id) {
|
switch (input.id) {
|
||||||
case "url":
|
case "url":
|
||||||
@@ -826,6 +994,8 @@ const navigationEvents = async () => {
|
|||||||
|
|
||||||
// Handle input enter events
|
// Handle input enter events
|
||||||
ipcMain.on("input-enter", (e, input) => {
|
ipcMain.on("input-enter", (e, input) => {
|
||||||
|
console.debug(`webview.js: navigationEvents(input-enter-${input.id})`);
|
||||||
|
WEBVIEW.tracker.pointer.time = new Date();
|
||||||
switch (input.id) {
|
switch (input.id) {
|
||||||
case "url":
|
case "url":
|
||||||
let url = input.text.trim();
|
let url = input.text.trim();
|
||||||
@@ -842,6 +1012,7 @@ const navigationEvents = async () => {
|
|||||||
|
|
||||||
// Handle navigation button click events
|
// Handle navigation button click events
|
||||||
ipcMain.on("button-click", (e, button) => {
|
ipcMain.on("button-click", (e, button) => {
|
||||||
|
console.debug(`webview.js: navigationEvents(button-click-${button.id})`);
|
||||||
WEBVIEW.tracker.pointer.time = new Date();
|
WEBVIEW.tracker.pointer.time = new Date();
|
||||||
switch (button.id) {
|
switch (button.id) {
|
||||||
case "home":
|
case "home":
|
||||||
@@ -877,6 +1048,29 @@ const navigationEvents = async () => {
|
|||||||
*/
|
*/
|
||||||
const viewEvents = async () => {
|
const viewEvents = async () => {
|
||||||
const ready = [];
|
const ready = [];
|
||||||
|
|
||||||
|
const loaded = (i) => {
|
||||||
|
if (WEBVIEW.viewActive) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set window status to fullscreen
|
||||||
|
if (i === 0 && !("app_debug" in ARGS)) {
|
||||||
|
WEBVIEW.window.setStatus("Fullscreen");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide loader and show first view
|
||||||
|
const done = ready.length >= WEBVIEW.views.length;
|
||||||
|
if (done) {
|
||||||
|
WEBVIEW.viewActive = 1;
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
console.debug(`webview.js: viewEvents(${i},loaded-${ready.length}/${WEBVIEW.views.length})`);
|
||||||
|
|
||||||
|
return done;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle events per webview
|
||||||
WEBVIEW.views.forEach((view, i) => {
|
WEBVIEW.views.forEach((view, i) => {
|
||||||
// Enable webview touch emulation
|
// Enable webview touch emulation
|
||||||
view.webContents.debugger.attach("1.1");
|
view.webContents.debugger.attach("1.1");
|
||||||
@@ -892,32 +1086,33 @@ const viewEvents = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update webview layout
|
// Update webview layout
|
||||||
|
view.webContents.once("dom-ready", () => {
|
||||||
|
console.debug(`webview.js: viewEvents(${i},dom-ready)`);
|
||||||
|
if ("app_reset" in ARGS) {
|
||||||
|
cookieStore("web-theme", WEBVIEW.theme.default, view);
|
||||||
|
cookieStore("web-zoom", WEBVIEW.zoom.default, view);
|
||||||
|
}
|
||||||
|
ready.push(i);
|
||||||
|
});
|
||||||
view.webContents.on("dom-ready", () => {
|
view.webContents.on("dom-ready", () => {
|
||||||
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
||||||
if (ready.length < WEBVIEW.views.length) {
|
|
||||||
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
|
||||||
ready.push(i);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Webview fully loaded
|
// Webview fully loaded
|
||||||
view.webContents.on("did-finish-load", () => {
|
view.webContents.on("did-finish-load", () => {
|
||||||
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
|
console.debug(`webview.js: viewEvents(${i},did-finish-load)`);
|
||||||
nextView();
|
|
||||||
}
|
|
||||||
if ("app_debug" in ARGS) {
|
if ("app_debug" in ARGS) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
view.webContents.openDevTools();
|
view.webContents.openDevTools();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
loaded(i);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Webview not loaded
|
// Webview not loaded
|
||||||
view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => {
|
view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => {
|
||||||
if (mainframe) {
|
if (mainframe) {
|
||||||
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
|
console.debug(`webview.js: viewEvents(${i},did-fail-load)`);
|
||||||
nextView();
|
|
||||||
}
|
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case -3:
|
case -3:
|
||||||
console.warn(`Load Warning: ${url}, ERR_ABORTED (${code})`);
|
console.warn(`Load Warning: ${url}, ERR_ABORTED (${code})`);
|
||||||
@@ -926,35 +1121,44 @@ const viewEvents = async () => {
|
|||||||
console.error(`Load Error: ${url}, ${text} (${code})`);
|
console.error(`Load Error: ${url}, ${text} (${code})`);
|
||||||
view.webContents.loadURL(errorHtml(code, text, url, WEBVIEW.theme.get()));
|
view.webContents.loadURL(errorHtml(code, text, url, WEBVIEW.theme.get()));
|
||||||
}
|
}
|
||||||
|
loaded(i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Webview url changed
|
// Webview url changed
|
||||||
|
view.webContents.on("did-navigate", () => {
|
||||||
|
console.debug(`webview.js: viewEvents(${i},did-navigate)`);
|
||||||
|
updateView();
|
||||||
|
});
|
||||||
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
|
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
|
||||||
if (mainframe) {
|
if (mainframe) {
|
||||||
|
console.debug(`webview.js: viewEvents(${i},did-navigate-in-page)`);
|
||||||
updateView();
|
updateView();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
view.webContents.on("did-navigate", () => {
|
|
||||||
updateView();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle webview mouse events
|
// Handle webview mouse events
|
||||||
view.webContents.on("before-mouse-event", (e, mouse) => {
|
view.webContents.on("before-mouse-event", (e, mouse) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const then = WEBVIEW.tracker.pointer.time;
|
const then = WEBVIEW.tracker.pointer.time;
|
||||||
const delta = (now - then) / 1000;
|
const delta = (now - then) / 1000;
|
||||||
|
|
||||||
|
// Check mouse event type
|
||||||
switch (mouse.type) {
|
switch (mouse.type) {
|
||||||
case "mouseMove":
|
case "mouseMove":
|
||||||
const posNew = { x: mouse.globalX, y: mouse.globalY };
|
const posNew = { x: Math.round(mouse.globalX), y: Math.round(mouse.globalY) };
|
||||||
if (posNew.x < 0 || posNew.y < 0) {
|
if (posNew.x < 0 || posNew.y < 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Update last active on pointer position change
|
console.debug(`webview.js: viewEvents(${i},${mouse.type}-${posNew.x}-${posNew.y})`);
|
||||||
|
|
||||||
|
// Update tracker pointer time and position
|
||||||
const posOld = WEBVIEW.tracker.pointer.position;
|
const posOld = WEBVIEW.tracker.pointer.position;
|
||||||
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
||||||
WEBVIEW.tracker.pointer.time = now;
|
WEBVIEW.tracker.pointer.time = now;
|
||||||
WEBVIEW.tracker.pointer.position = posNew;
|
WEBVIEW.tracker.pointer.position = posNew;
|
||||||
|
|
||||||
|
// Update last active on pointer position change
|
||||||
if (delta > 30) {
|
if (delta > 30) {
|
||||||
console.info("Update Last Active");
|
console.info("Update Last Active");
|
||||||
integration.update();
|
integration.update();
|
||||||
@@ -962,16 +1166,20 @@ const viewEvents = async () => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "mouseDown":
|
case "mouseDown":
|
||||||
|
console.debug(`webview.js: viewEvents(${i},${mouse.type}-${mouse.button})`);
|
||||||
switch (mouse.button) {
|
switch (mouse.button) {
|
||||||
case "left":
|
case "left":
|
||||||
|
const off = WEBVIEW.tracker.display.off > WEBVIEW.tracker.display.on;
|
||||||
|
console.debug(`webview.js: viewEvents(${i},display-${off ? "off" : "on"})`);
|
||||||
|
|
||||||
// Ignore touch event if display was off
|
// Ignore touch event if display was off
|
||||||
if (WEBVIEW.tracker.display.off > WEBVIEW.tracker.display.on) {
|
if (off) {
|
||||||
console.verbose("Display Touch Event: Ignored");
|
console.verbose("Display Touch Event: Ignored");
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
|
||||||
// Turn display on if it was off
|
// Turn display on if it was off
|
||||||
if (hardware.getDisplayStatus() === "OFF") {
|
|
||||||
hardware.setDisplayStatus("ON");
|
hardware.setDisplayStatus("ON");
|
||||||
|
WEBVIEW.tracker.display.on = new Date();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "back":
|
case "back":
|
||||||
@@ -991,6 +1199,8 @@ const viewEvents = async () => {
|
|||||||
* Register app events and handler.
|
* Register app events and handler.
|
||||||
*/
|
*/
|
||||||
const appEvents = async () => {
|
const appEvents = async () => {
|
||||||
|
console.debug("webview.js: appEvents()");
|
||||||
|
|
||||||
// Handle global events
|
// Handle global events
|
||||||
EVENTS.on("reloadView", reloadView);
|
EVENTS.on("reloadView", reloadView);
|
||||||
EVENTS.on("updateView", updateView);
|
EVENTS.on("updateView", updateView);
|
||||||
@@ -1026,24 +1236,40 @@ const appEvents = async () => {
|
|||||||
WEBVIEW.window.focus();
|
WEBVIEW.window.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle exit events
|
// Handle crash events
|
||||||
app.on("before-quit", () => {
|
app.on("render-process-gone", (e, wc, details) => {
|
||||||
APP.exiting = true;
|
const url = wc.getURL() || "Unknown";
|
||||||
console.warn(`${APP.title} Terminated`);
|
if (!url.startsWith("data:")) {
|
||||||
|
console.error(`Render Process ${details.reason} (code ${details.exitCode}): ${url}`);
|
||||||
|
}
|
||||||
|
reloadView();
|
||||||
|
});
|
||||||
|
app.on("child-process-gone", (e, details) => {
|
||||||
|
const name = details.name || details.serviceName || "Unknown";
|
||||||
|
console.error(`${details.type} Process ${details.reason} (code ${details.exitCode}): ${name}`);
|
||||||
});
|
});
|
||||||
process.on("SIGINT", app.quit);
|
|
||||||
|
|
||||||
// Webview initialized
|
// Update latest screenshot (every full 1min)
|
||||||
WEBVIEW.initialized = true;
|
interval(() => {
|
||||||
|
if (APP.exiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
captureView(5000).then(() => {
|
||||||
|
EVENTS.emit("updateScreenshot");
|
||||||
|
});
|
||||||
|
}, 60 * 1000);
|
||||||
|
|
||||||
// Check for latest release infos (2h)
|
// Check latest release (every full 2h)
|
||||||
setInterval(() => {
|
interval(() => {
|
||||||
if (APP.exiting) {
|
if (APP.exiting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
latestRelease();
|
latestRelease();
|
||||||
}, 7200 * 1000);
|
}, 7200 * 1000);
|
||||||
await latestRelease();
|
latestRelease();
|
||||||
|
|
||||||
|
// Webview initialized
|
||||||
|
WEBVIEW.initialized = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1051,7 +1277,7 @@ const appEvents = async () => {
|
|||||||
*/
|
*/
|
||||||
const latestRelease = async () => {
|
const latestRelease = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(APP.releases.url, { timeout: 10000 });
|
const response = await axios.get(APP.releases.url, { timeout: 20000 });
|
||||||
const release = response?.data?.find((item) => {
|
const release = response?.data?.find((item) => {
|
||||||
return !item.draft && (!item.prerelease || "app_early" in ARGS);
|
return !item.draft && (!item.prerelease || "app_early" in ARGS);
|
||||||
});
|
});
|
||||||
@@ -1062,6 +1288,7 @@ const latestRelease = async () => {
|
|||||||
summary: release.body || " ",
|
summary: release.body || " ",
|
||||||
url: release.html_url || " ",
|
url: release.html_url || " ",
|
||||||
};
|
};
|
||||||
|
EVENTS.emit("updateApp");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Github Error:", error.message);
|
console.warn("Github Error:", error.message);
|
||||||
@@ -1084,7 +1311,7 @@ const onlineStatus = (url, interval = 1000, timeout = 60000) => {
|
|||||||
try {
|
try {
|
||||||
if (!url.startsWith("data:")) {
|
if (!url.startsWith("data:")) {
|
||||||
const agent = new https.Agent({ rejectUnauthorized: !("ignore_certificate_errors" in ARGS) });
|
const agent = new https.Agent({ rejectUnauthorized: !("ignore_certificate_errors" in ARGS) });
|
||||||
await axios.get(url, { httpsAgent: agent, timeout: 10000 });
|
await axios.get(url, { httpsAgent: agent, timeout: 20000 });
|
||||||
}
|
}
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1105,6 +1332,59 @@ const onlineStatus = (url, interval = 1000, timeout = 60000) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores, retrieves or deletes a cookie from the active webview.
|
||||||
|
*
|
||||||
|
* @param {string} key - The key of the webview cookie.
|
||||||
|
* @param {string|number} value - The value of the webview cookie.
|
||||||
|
* @param {WebContentsView} view - The webview that stores the cookie.
|
||||||
|
* @returns {Promise<string|number>} The value of the webview cookie.
|
||||||
|
*/
|
||||||
|
const cookieStore = async (key, value, view = WEBVIEW.views[WEBVIEW.viewActive]) => {
|
||||||
|
const url = view ? view.webContents.getURL() : null;
|
||||||
|
if (!url || url.startsWith("data:")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use origin session cookies
|
||||||
|
const name = `${APP.name}-${key}`;
|
||||||
|
const origin = new URL(url).origin;
|
||||||
|
const cookies = view.webContents.session.cookies;
|
||||||
|
const expires = Math.floor(Date.now() / 1000) + 3600 * 24 * 365;
|
||||||
|
|
||||||
|
// Delete cookie if value is null
|
||||||
|
if (value === null) {
|
||||||
|
await cookies.remove(origin, name);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write cookie if value is provided
|
||||||
|
if (value !== undefined) {
|
||||||
|
await cookies.set({ url: origin, name: name, value: `${value}`, expirationDate: expires });
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read cookie if no value is provided
|
||||||
|
const cookie = (await cookies.get({ url: origin, name: name }))[0] || {};
|
||||||
|
return !isNaN(Number(cookie.value)) ? Number(cookie.value) : cookie.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures a webview screenshot as a base64 image.
|
||||||
|
*
|
||||||
|
* @param {number} wait - The time to wait before capturing in milliseconds.
|
||||||
|
* @param {WebContentsView} view - The webview that captures the page.
|
||||||
|
* @returns {Promise<string|null>} The base64 image of the captured page or null if failed.
|
||||||
|
*/
|
||||||
|
const captureView = async (wait, view = WEBVIEW.views[WEBVIEW.viewActive]) => {
|
||||||
|
await new Promise((r) => setTimeout(r, wait));
|
||||||
|
const image = await view.webContents.capturePage();
|
||||||
|
const dataUrl = image.toDataURL();
|
||||||
|
const dataString = dataUrl.replace(/^data:image\/\w+;base64,/, "").trim();
|
||||||
|
WEBVIEW.tracker.screenshot = dataString || WEBVIEW.tracker.screenshot;
|
||||||
|
return WEBVIEW.tracker.screenshot;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a html template for a spinning loader.
|
* Generates a html template for a spinning loader.
|
||||||
*
|
*
|
||||||
@@ -1203,6 +1483,20 @@ const errorHtml = (code, text, url, theme) => {
|
|||||||
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interval function that runs at interval boundaries.
|
||||||
|
*
|
||||||
|
* @param {Function} callback - The function to execute.
|
||||||
|
* @param {number} ms - The interval time in milliseconds.
|
||||||
|
*/
|
||||||
|
const interval = (callback, ms) => {
|
||||||
|
const delay = ms - (Date.now() % ms);
|
||||||
|
setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
setInterval(callback, ms);
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
update,
|
update,
|
||||||
|
|||||||
+7
-7
@@ -4,7 +4,7 @@
|
|||||||
"description": "Home Assistant Touch Kiosk",
|
"description": "Home Assistant Touch Kiosk",
|
||||||
"homepage": "https://github.com/leukipp/touchkio",
|
"homepage": "https://github.com/leukipp/touchkio",
|
||||||
"author": "leukipp",
|
"author": "leukipp",
|
||||||
"version": "1.4.0",
|
"version": "1.4.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -15,16 +15,16 @@
|
|||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.10.0",
|
"@electron-forge/cli": "^7.10.2",
|
||||||
"@electron-forge/maker-deb": "^7.10.0",
|
"@electron-forge/maker-deb": "^7.10.2",
|
||||||
"@electron-forge/maker-zip": "^7.10.0",
|
"@electron-forge/maker-zip": "^7.10.2",
|
||||||
"@electron-forge/publisher-github": "^7.10.0",
|
"@electron-forge/publisher-github": "^7.10.2",
|
||||||
"electron": "^38.2.2",
|
"electron": "^39.2.4",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"prettier": "^3.6.2"
|
"prettier": "^3.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.13.2",
|
||||||
"electron-log": "^5.4.3",
|
"electron-log": "^5.4.3",
|
||||||
"mqtt": "^5.14.1"
|
"mqtt": "^5.14.1"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user