21 Commits
Author SHA1 Message Date
leukipp e195a56739 build: update version 2025-10-10 21:29:38 +02:00
leukipp d86a5385a8 docs: update readme 2025-10-10 19:11:29 +02:00
leukipp 3ce1265c8a docs: update hardware 2025-10-10 19:10:43 +02:00
leukipp c2bf551834 ref: update images 2025-10-10 19:08:29 +02:00
leukipp 785a97721a build: update dependencies 2025-10-09 14:19:57 +02:00
leukipp 05c9d98b61 feat: add mqtt version diagnostic #91 2025-10-09 13:43:20 +02:00
leukipp 9c26d9d8bb ref: add mqtt attributes 2025-10-09 13:09:30 +02:00
leukipp ad7d3fd3ff feat: early app updates #96 2025-10-08 20:47:59 +02:00
leukipp f666ed57ab ref: widget focus handling 2025-10-08 20:13:48 +02:00
leukipp c0b1a6e9ce feat: add mqtt error diagnostic #91 2025-10-07 21:48:49 +02:00
leukipp 19e51e349c fix: use mqtt last will #84 2025-10-07 09:59:48 +02:00
leukipp 5d012bd637 feat: extended theme switching #87 2025-10-07 09:17:57 +02:00
leukipp 01c7411ddc feat: use box for mqtt zoom #90 2025-10-06 19:17:35 +02:00
leukipp 3e4d96536f feat: dynamic status bar #94 2025-10-06 18:55:51 +02:00
leukipp 4bcb2c4e89 feat: auto on-screen keyboard #85 2025-09-30 21:26:39 +02:00
leukipp b2b87888e4 ref: keyboard window status handling 2025-09-30 17:31:00 +02:00
leukipp 164ef2838c docs: add sponsor 2025-09-29 10:20:40 +02:00
leukipp ae2633800d ref: allow electron flags 2025-09-29 10:10:12 +02:00
leukipp 552f0e19a4 feat: mqtt audio volume control #82 2025-09-26 13:47:15 +02:00
leukipp 890c84d69d Merge branch 'develop' of https://github.com/leukipp/touchkio into develop 2025-09-21 22:47:10 +02:00
leukipp ac69dee387 ref: improve error handling 2025-09-21 22:47:02 +02:00
16 changed files with 1352 additions and 612 deletions
+1
View File
@@ -0,0 +1 @@
github: leukipp
+44 -8
View File
@@ -29,26 +29,62 @@ If you are running Linux with a graphical user interface (Wayland or X11), you s
### HDMI ### HDMI
| Device | System | Display | Status | | Device | System | Display | Status |
| ------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------ | | ------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------ |
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN 5" Touch Display (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 8" CAPLCD (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 | | 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 7" 7EP-CAPLCD (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 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 | | 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 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 10.1" Capacitive Touch (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 | [Prechen Portable Touch 18.5" FHD (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 | | 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 | [Akntzcs Portable Touch 16" HD (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
| 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 | [ELECROW Portable Monitor 10.1" (1280x800)](https://www.amazon.co.uk/dp/B0BHHQLKPY) | 🟨 |
| 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 | [Akntzcs Portable Touch HD 16" (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
| 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 | Generic Non-Touch | 🟨 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
| Generic PC (x64) | Debian KDE (64-bit), Wayland, X11 | Generic Non-Touch | 🟧 | | Generic PC (x64) | Debian KDE (64-bit), Wayland, X11 | Generic Non-Touch | 🟧 |
| Generic PC (x64) | Ubuntu XFCE (64-bit), X11 | Generic Non-Touch | 🟧 | | Generic PC (x64) | Ubuntu XFCE (64-bit), X11 | Generic Non-Touch | 🟧 |
| Generic PC (x64) | Ubuntu GNOME (64-bit), X11 | 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 | 🟥 | | Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 |
## Features
**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.
- 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:
- 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.
- 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.
The necessary requirements for MQTT sensors to work are listed here:
| 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) |
| 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) |
| 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) |
| 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) |
| Shutdown | Requires password-less `sudo` rights. | [#39](https://github.com/leukipp/touchkio/issues/39) |
## 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 or keyboard via the Home Assistant integration. The only problems that may arise are when controlling the display via the Home Assistant integration.
- 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).
+25 -30
View File
@@ -1,4 +1,10 @@
# TouchKio # TouchKio
![build](https://img.shields.io/github/actions/workflow/status/leukipp/touchkio/release.yaml?style=flat-square)
![date](https://img.shields.io/github/release-date/leukipp/touchkio?style=flat-square)
![platform](https://img.shields.io/badge/platform-%20arm64%20|%20x64%20-teal?style=flat-square)
![downloads](https://img.shields.io/github/downloads/leukipp/touchkio/total?style=flat-square)
[![sponsor](https://img.shields.io/github/sponsors/leukipp?color=red&logo=github&style=flat-square)](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.
This tool is packaged as a **.deb** file, making it easy to launch the kiosk application on any Debian based Linux [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) (e.g. **Raspberry Pi**) equipped with a **DSI or HDMI** Touch Display. This tool is packaged as a **.deb** file, making it easy to launch the kiosk application on any Debian based Linux [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) (e.g. **Raspberry Pi**) equipped with a **DSI or HDMI** Touch Display.
Additional releases for other Linux systems are available as **.zip** file. Additional releases for other Linux systems are available as **.zip** file.
@@ -12,9 +18,10 @@ Moreover, the device running the **kiosk application** also offers several **Hom
- [x] Fast and easy setup. - [x] Fast and easy setup.
- [x] Remember login credentials. - [x] Remember login credentials.
- [x] Touch optimized web browsing. - [x] Touch optimized web browsing.
- [x] Dynamic window status bar layout.
- [x] Side panel widget for kiosk control. - [x] Side panel widget for kiosk control.
- [x] Navigation bar to switch between pages. - [x] Navigation bar to switch between pages.
- [x] Configurable browser zoom and theme support. - [x] Adjustable browser zoom and theme support.
- [x] Extended touch screen wake-up functionality. - [x] Extended touch screen wake-up functionality.
- [x] Remote controllable via MQTT. - [x] Remote controllable via MQTT.
- [x] Toggle the on-screen keyboard. - [x] Toggle the on-screen keyboard.
@@ -23,8 +30,9 @@ Moreover, the device running the **kiosk application** also offers several **Hom
- [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] Monitor temperature, processor and memory usage. - [x] Audio volume control for connected devices.
- [x] Execute system reboot and shutdown commands. - [x] Execute system reboot and shutdown commands.
- [x] Monitor battery, temperature, processor and memory usage.
The kiosk application can be executed with command line arguments to load a **Home Assistant dashboard in fullscreen** mode. The kiosk application can be executed with command line arguments to load a **Home Assistant dashboard in fullscreen** mode.
Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Linux device and the connected Touch Display. Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Linux device and the connected Touch Display.
@@ -70,9 +78,8 @@ systemctl --user start|stop|status|restart touchkio.service
When connected via SSH, it's necessary to export the display variables first, as outlined in the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section. When connected via SSH, it's necessary to export the display variables first, as outlined in the [development](https://github.com/leukipp/touchkio?tab=readme-ov-file#development) section.
The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script mentioned above performs the following tasks (and you just have to do it manually): The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script mentioned above performs the following tasks (and you just have to do it manually):
- [Download](https://github.com/leukipp/touchkio/releases/latest) the latest version file that is suitable for your architecture (arm64 or x64). - [Download](https://github.com/leukipp/touchkio/releases/latest) the latest version file that is suitable for your architecture (arm64 or x64).
- Debian (**deb**): Open a terminal and execute the following command to install the application, e.g: - Debian (**deb**): Open a terminal and execute the following command to install the application, e.g: `sudo apt install ./touchkio_1.x.x_arm64.deb && touchkio --setup`
`sudo apt install ./touchkio_1.x.x_arm64.deb && touchkio --setup` - Others (**zip**): Extract the archive and run the binary, e.g: `unzip touchkio-linux-x64-1.x.x.zip && cd touchkio-linux-x64 && ./touchkio --setup`
- Others (**zip**): Extract the archive and run the binary, e.g: `unzip touchkio-linux-x64-1.x.x.zip && cd touchkio-linux-x64 && ./touchkio --setup`
- If you just want to load a Home Assistant dashboard without further control you are good to go, e.g: `touchkio --web-url=https://demo.home-assistant.io` - If you just want to load a Home Assistant dashboard without further control you are good to go, e.g: `touchkio --web-url=https://demo.home-assistant.io`
- The `--web-url` doesn't necessarily need to be a Home Assistant url, any kind of website can be shown in kiosk mode. - The `--web-url` doesn't necessarily need to be a Home Assistant url, any kind of website can be shown in kiosk mode.
- Only when using the MQTT integration via `--mqtt-*`, a running Home Assistant instance is required. - Only when using the MQTT integration via `--mqtt-*`, a running Home Assistant instance is required.
@@ -103,7 +110,7 @@ Running `touchkio --setup` will prompt you to enter arguments that will be used
These default arguments are stored in `~/.config/touchkio/Arguments.json`, where they can also be modified. These default arguments are stored in `~/.config/touchkio/Arguments.json`, where they can also be modified.
### WEB ### WEB
The available arguments to control the kiosk application via terminal are as follows: The available arguments to control the kiosk application via terminal are as follows:
| Name | Default | Description | | Name | Default | Description |
| ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | | ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `--web-url` (Required) | - | Url of the Home Assistant instance<a id="ref1"></a><sup><a href="#foot1">[1]</a></sup> (HTTP(S)://IP:PORT) | | `--web-url` (Required) | - | Url of the Home Assistant instance<a id="ref1"></a><sup><a href="#foot1">[1]</a></sup> (HTTP(S)://IP:PORT) |
@@ -122,7 +129,7 @@ touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0
> You can configure multiple pages by separating them with a comma: > You can configure multiple pages by separating them with a comma:
> `touchkio --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app`. > `touchkio --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app`.
In the `~/.config/touchkio/Arguments.json` file: In the `~/.config/touchkio/Arguments.json` file:
```json ```json
{ {
"web_url": [ "web_url": [
@@ -190,7 +197,7 @@ To make this permanent, consider adding the export variables into the `~/.bashrc
<details><summary>You probably won't need this.</summary><div></br> <details><summary>You probably won't need this.</summary><div></br>
Incorporating custom extensions and external hardware (like motion sensors, ultrasonic sensors, cameras, relays and switches) via **Raspberry Pi's GPIO/USB** involves several steps. Incorporating custom extensions and external hardware (like motion sensors, ultrasonic sensors, cameras, relays and switches) via **Raspberry Pi's GPIO/USB** involves several steps.
While using external sensors that directly integrate with Home Assistant and by utilizing automation's to interact with **TouchKio via MQTT** is generally easier and **recommended**, here's a rough guide on how to proceed with custom hardware integration: While using external sensors that directly integrate with Home Assistant and by utilizing automation's to interact with **TouchKio via MQTT** is generally easier and **recommended**, here's a rough guide on how to proceed with custom hardware integration:
1. **Install Node.js library**: Use Yarn to add a library that can interact with your hardware (GPIO, USB, etc.): 1. **Install Node.js library**: Use Yarn to add a library that can interact with your hardware (GPIO, USB, etc.):
@@ -217,7 +224,7 @@ While using external sensors that directly integrate with Home Assistant and by
```javascript ```javascript
const init = async (args) => { ... } const init = async (args) => { ... }
``` ```
From there, you will need to further refine your code by tinkering with sensor updates. This can be achieved through either periodic update calls or event based triggers. From there, you will need to further refine your code by tinkering with sensor updates. This can be achieved through either periodic update calls or event based triggers.
</div></details> </div></details>
@@ -225,8 +232,8 @@ While using external sensors that directly integrate with Home Assistant and by
<details><summary>Don't waste your time reading this.</summary><div></br> <details><summary>Don't waste your time reading this.</summary><div></br>
The Raspberry Pi's **build-in on-screen keyboard** named `squeekboard` (it squeaks because some *Rust* got inside), is specifically designed for Wayland environments and features a **D-Bus interface** that allows applications to show or hide the keyboard as needed. The Raspberry Pi's **build-in on-screen keyboard** named `squeekboard` (it squeaks because some _Rust_ got inside), is specifically designed for Wayland environments and features a **D-Bus interface** that allows applications to show or hide the keyboard as needed.
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 `bl_power` value to **4**, when setting the value to **0** the screen will turn on again.
@@ -237,7 +244,7 @@ I managed to achieve this for the `brightness` file by implementing a simple `fs
However, I found that it **never triggered** for the `bl_power` file. However, I found that it **never triggered** for the `bl_power` 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 file **every second** for any changes. As a result, I went for a **polling solution**, checking the state of the `brightness` and `dpms` file **every second** for any changes.
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.
@@ -264,34 +271,22 @@ This adjustment provides a user experience similar to that of a proper mobile de
## Issues ## Issues
Please review the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation first if you encounter any problems. Please review the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation first if you encounter any problems.
**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.
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
- 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 may only be available when using X11.
- 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)
**Known Issues** that are by-design or for which there isn't a solution so far: **Known Issues** that are by-design or for which there isn't a solution so far:
- 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. - 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 (e.g. presence detection) to manage the screen status. - 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. - 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. - As a current workaround you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the keyboard visibility.
- Certain commands from the MQTT integration may require elevated privileges to work correctly. - There is also an [experimental feature](https://github.com/leukipp/touchkio/issues/85) that uses a special Electron flag to address this problem.
- 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. - On the terminal you may see some _ERROR:gbm_wrapper.cc_ messages.
- 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.
- 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. 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 specs, etc.) and any relevant log files. 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.
## 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.
+17 -8
View File
@@ -32,8 +32,8 @@
align-items: center; align-items: center;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
padding: 0 30px; padding: 0 10px;
column-gap: 20px; column-gap: 10px;
list-style-type: none; list-style-type: none;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
background: var(--background-color); background: var(--background-color);
@@ -74,6 +74,7 @@
padding: 6px 10px; padding: 6px 10px;
border-radius: 4px; border-radius: 4px;
font-size: 18px; font-size: 18px;
font-family: sans-serif;
outline: none; outline: none;
border: none; border: none;
cursor: text; cursor: text;
@@ -132,7 +133,7 @@
</li> </li>
<li class="separator"></li> <li class="separator"></li>
<li class="dynamic"> <li class="dynamic">
<input id="url" type="text" spellcheck="false" /> <input id="url" type="text" spellcheck="false" readonly="true" />
</li> </li>
<li class="separator"></li> <li class="separator"></li>
<li> <li>
@@ -187,6 +188,9 @@
}); });
}); });
document.querySelectorAll("input").forEach((input) => { document.querySelectorAll("input").forEach((input) => {
input.addEventListener("blur", (e) => {
ipcRenderer.send("input-blur", { id: e.target.id });
});
input.addEventListener("focus", (e) => { input.addEventListener("focus", (e) => {
ipcRenderer.send("input-focus", { id: e.target.id }); ipcRenderer.send("input-focus", { id: e.target.id });
}); });
@@ -205,16 +209,21 @@
ipcRenderer.on("button-hidden", (e, button) => { ipcRenderer.on("button-hidden", (e, button) => {
document.getElementById(button.id).parentNode.classList.toggle("hidden", button.hidden); document.getElementById(button.id).parentNode.classList.toggle("hidden", button.hidden);
}); });
ipcRenderer.on("input-blur", (e, input) => { ipcRenderer.on("input-readonly", (e, input) => {
document.getElementById(input.id).blur(); document.getElementById(input.id).readOnly = input.readonly;
});
ipcRenderer.on("input-select", (e, input) => {
document.getElementById(input.id).select();
}); });
ipcRenderer.on("input-text", (e, input) => { ipcRenderer.on("input-text", (e, input) => {
document.getElementById(input.id).value = input.text; document.getElementById(input.id).value = input.text;
document.getElementById(input.id).placeholder = input.placeholder; document.getElementById(input.id).placeholder = input.placeholder;
}); });
ipcRenderer.on("input-select", (e, input) => {
window.getSelection()?.removeAllRanges();
if (input.select) {
document.getElementById(input.id).select();
} else {
document.getElementById(input.id).blur();
}
});
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
View File
@@ -3,8 +3,6 @@
<head> <head>
<style> <style>
[data-theme="hidden"] { [data-theme="hidden"] {
--primary-color: rgba(0, 0, 0, 0);
--border-color: rgba(0, 0, 0, 0);
--background-color: rgba(0, 0, 0, 0); --background-color: rgba(0, 0, 0, 0);
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
+161
View File
@@ -0,0 +1,161 @@
<!doctype html>
<html>
<head>
<style>
[data-theme="dark"] {
--primary-color: rgba(229, 229, 229, 1);
--background-color: rgba(66, 67, 68, 1);
--minimize-color: rgba(52, 199, 89, 1);
--fullscreen-color: rgba(255, 149, 0, 1);
--terminate-color: rgba(255, 59, 48, 1);
}
[data-theme="light"] {
--primary-color: rgba(26, 26, 26, 1);
--background-color: rgba(237, 236, 235, 1);
--minimize-color: rgba(52, 199, 89, 1);
--fullscreen-color: rgba(255, 149, 0, 1);
--terminate-color: rgba(255, 59, 48, 1);
}
::-webkit-scrollbar {
display: none;
}
* {
margin: 0;
padding: 0;
cursor: pointer;
}
body {
display: flex;
align-items: center;
width: 100vw;
height: 100vh;
background: var(--background-color);
}
header {
display: flex;
align-items: center;
flex: 1 1 0%;
height: 100vh;
-webkit-app-region: drag;
}
h1 {
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
font-size: 18px;
font-weight: 500;
font-family: sans-serif;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
user-select: none;
color: var(--primary-color);
}
menu {
display: flex;
flex-direction: row;
align-items: center;
flex: 0 0 auto;
height: 100vh;
min-width: 116px;
padding: 0px 10px;
column-gap: 10px;
list-style-type: none;
background: var(--background-color);
position: relative;
z-index: 1;
}
li {
display: flex;
align-items: center;
flex: 0 0 auto;
}
li.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
button {
width: 32px;
height: 32px;
border: none;
background-color: transparent;
}
svg {
pointer-events: none;
}
#minimize > svg {
fill: var(--minimize-color);
}
#fullscreen > svg {
fill: var(--fullscreen-color);
}
#terminate > svg {
fill: var(--terminate-color);
}
</style>
</head>
<body>
<header>
<h1 id="title"></h1>
</header>
<menu>
<li>
<button id="minimize">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>minus-circle</title>
<path d="M17,13H7V11H17M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</svg>
</button>
</li>
<li>
<button id="fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>chevron-up-circle</title>
<path
d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M7.4,15.4L12,10.8L16.6,15.4L18,14L12,8L6,14L7.4,15.4Z"
/>
</svg>
</button>
</li>
<li>
<button id="terminate">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>close-circle</title>
<path
d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
/>
</svg>
</button>
</li>
</menu>
<script>
const { ipcRenderer } = require("electron");
const registerSendEvents = () => {
document.querySelectorAll("button").forEach((button) => {
button.addEventListener("click", (e) => {
ipcRenderer.send("button-click", { id: e.target.id });
});
});
};
const registerReceiveEvents = () => {
ipcRenderer.on("button-disabled", (e, button) => {
document.getElementById(button.id).parentNode.classList.toggle("disabled", button.disabled);
});
ipcRenderer.on("text-content", (e, text) => {
document.getElementById(text.id).textContent = text.content;
});
ipcRenderer.on("data-theme", (e, data) => {
document.documentElement.setAttribute("data-theme", data.theme);
});
};
registerSendEvents();
registerReceiveEvents();
</script>
</body>
</html>
+13 -13
View File
@@ -68,16 +68,6 @@
</head> </head>
<body> <body>
<menu> <menu>
<li>
<button id="keyboard">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>keyboard-close-outline</title>
<path
d="M12 23L16 19H8M4 3C2.9 3 2 3.9 2 5V15C2 16.1 2.9 17 4 17H20C21.1 17 22 16.1 22 15V5C22 3.9 21.1 3 20 3H4M4 5H20V15H4V5M5 6V8H7V6H5M8 6V8H10V6H8M11 6V8H13V6H11M14 6V8H16V6H14M17 6V8H19V6H17M5 9V11H7V9H5M8 9V11H10V9H8M11 9V11H13V9H11M14 9V11H16V9H14M17 9V11H19V9H17M8 12V14H16V12H8Z"
/>
</svg>
</button>
</li>
<li> <li>
<button id="fullscreen"> <button id="fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
@@ -89,11 +79,21 @@
</button> </button>
</li> </li>
<li> <li>
<button id="minimize"> <button id="theme">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>image-size-select-small</title> <title>compare</title>
<path <path
d="M23,15H21V17H23V15M23,11H21V13H23V11M23,19H21V21C22,21 23,20 23,19M15,3H13V5H15V3M23,7H21V9H23V7M21,3V5H23C23,4 22,3 21,3M3,21H11V15H1V19A2,2 0 0,0 3,21M3,7H1V9H3V7M15,19H13V21H15V19M19,3H17V5H19V3M19,19H17V21H19V19M3,3C2,3 1,4 1,5H3V3M3,11H1V13H3V11M11,3H9V5H11V3M7,3H5V5H7V3Z" d="M19,3H14V5H19V18L14,12V21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10,18H5L10,12M10,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H10V23H12V1H10V3Z"
/>
</svg>
</button>
</li>
<li>
<button id="keyboard">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>keyboard-close-outline</title>
<path
d="M12 23L16 19H8M4 3C2.9 3 2 3.9 2 5V15C2 16.1 2.9 17 4 17H20C21.1 17 22 16.1 22 15V5C22 3.9 21.1 3 20 3H4M4 5H20V15H4V5M5 6V8H7V6H5M8 6V8H10V6H8M11 6V8H13V6H11M14 6V8H16V6H14M17 6V8H19V6H17M5 9V11H7V9H5M8 9V11H10V9H8M11 9V11H13V9H11M14 9V11H16V9H14M17 9V11H19V9H17M8 12V14H16V12H8Z"
/> />
</svg> </svg>
</button> </button>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 152 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 90 KiB

+36 -11
View File
@@ -19,6 +19,7 @@ if (!process.env.DISPLAY) {
console.error(` systemctl --user start touchkio.service`); console.error(` systemctl --user start touchkio.service`);
console.error(`Alternatively export the variables first:`); console.error(`Alternatively export the variables first:`);
console.error(` export DISPLAY=":0" && export WAYLAND_DISPLAY="wayland-0" && touchkio\n`); console.error(` export DISPLAY=":0" && export WAYLAND_DISPLAY="wayland-0" && touchkio\n`);
process.exit(1);
} }
/** /**
@@ -36,7 +37,7 @@ app.whenReady().then(async () => {
if ("mqtt_password" in args) { if ("mqtt_password" in args) {
args.mqtt_password = "*".repeat((args.mqtt_password || "").length); args.mqtt_password = "*".repeat((args.mqtt_password || "").length);
} }
console.log(`Arguments: ${JSON.stringify(args, null, 2)}`); console.info(`Arguments: ${JSON.stringify(args, null, 2)}`);
// Chained init functions // Chained init functions
const chained = [webview.init, hardware.init, integration.init]; const chained = [webview.init, hardware.init, integration.init];
@@ -64,8 +65,9 @@ const initApp = async () => {
APP.title = packageJson.title; APP.title = packageJson.title;
APP.version = app.getVersion(); APP.version = app.getVersion();
APP.path = app.getAppPath(); APP.path = app.getAppPath();
APP.config = app.getPath("userData"); 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.config = app.getPath("userData");
// Set additional update infos // Set additional update infos
APP.homepage = `https://github.com/${packageJson.author}/${packageJson.name}`; APP.homepage = `https://github.com/${packageJson.author}/${packageJson.name}`;
@@ -86,7 +88,7 @@ const initApp = async () => {
// Request single app instance lock // Request single app instance lock
if (!app.requestSingleInstanceLock()) { if (!app.requestSingleInstanceLock()) {
console.error(`${APP.title} is already running`); console.error(`${APP.title} is already running`);
return app.quit(); return app.exit(1);
} }
return true; return true;
@@ -110,25 +112,25 @@ const initArgs = async () => {
if (APP.build.id) { if (APP.build.id) {
build = ` (${APP.build.id}), built on ${APP.build.date} (${APP.build.platform}-${APP.build.arch}-${APP.build.maker})`; build = ` (${APP.build.id}), built on ${APP.build.date} (${APP.build.platform}-${APP.build.arch}-${APP.build.maker})`;
} }
console.log(`${APP.name}-v${APP.version}${build}\n${APP.homepage}`); console.info(`${APP.name}-v${APP.version}${build}\n${APP.homepage}`);
return app.quit(); return app.exit(0);
} }
// Setup arguments from file path // Setup arguments from file path
if ("setup" in args || (!argsProvided && !argsFileExists)) { if ((!argsProvided && !argsFileExists) || "setup" in args) {
await sleep(3000); await sleep(3000);
do { do {
args = await promptArgs(process); args = await promptArgs(process);
} while (!Object.keys(args).length); } while (!Object.keys(args).length);
writeArgs(argsFilePath, args); writeArgs(argsFilePath, args);
} else if (!argsProvided && argsFileExists) { } else if (argsFileExists) {
args = readArgs(argsFilePath); args = { ...readArgs(argsFilePath), ...args };
} }
// Check arguments object // Check arguments object
if (!Object.keys(args).length) { if (!Object.keys(args).length) {
console.error(`No arguments provided`); console.error(`No arguments provided`);
return app.quit(); return app.exit(1);
} }
// Split url arguments // Split url arguments
@@ -155,7 +157,30 @@ const initLog = async () => {
console.error("Failed to delete log file:", error.message); console.error("Failed to delete log file:", error.message);
} }
// Overwrite console log settings // Overwrite console log
APP.logs = [];
log.errorHandler.startCatching({
showDialog: false,
onError({ error }) {
console.error(`Uncaught Error: ${error.message} (${error.stack})`);
app.exit(1);
},
});
log.hooks.push((message, transport, type) => {
const text = message.data.join(" ");
if (!text.startsWith("(node:") && type === "console") {
APP.logs.unshift({
time: message.date,
level: message.level,
text: text,
});
if (APP.logs.length > 10) {
APP.logs.splice(10);
}
EVENTS.emit("consoleLog", APP.logs[0]);
}
return message;
});
log.transports.file.resolvePathFn = () => { log.transports.file.resolvePathFn = () => {
return APP.log; return APP.log;
}; };
@@ -280,7 +305,7 @@ const promptArgs = async (proc) => {
} catch (error) { } catch (error) {
console.error(`\n${error.message}`); console.error(`\n${error.message}`);
args = {}; args = {};
app.quit(); app.exit(1);
} finally { } finally {
read.close(); read.close();
} }
+19 -18
View File
@@ -1,11 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Read arguments # Read arguments
ARG_EARLY=false
ARG_UPDATE=false ARG_UPDATE=false
for arg in "$@"; do for arg in "$@"; do
if [ "$arg" = "update" ]; then case "$arg" in
ARG_UPDATE=true early) ARG_EARLY=true ;;
fi update) ARG_UPDATE=true ;;
esac
done done
# Determine system architecture # Determine system architecture
@@ -13,15 +15,9 @@ echo -e "Determining system architecture..."
BITS=$(getconf LONG_BIT) BITS=$(getconf LONG_BIT)
case "$(uname -m)" in case "$(uname -m)" in
aarch64) x86_64) ARCH="x64" ;;
ARCH="arm64" aarch64) ARCH="arm64" ;;
;; *) { echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."; exit 1; } ;;
x86_64)
ARCH="x64"
;;
*)
{ echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."; exit 1; }
;;
esac esac
[ "$BITS" -eq 64 ] || { echo "Architecture $ARCH running $BITS-bit operating system is not supported."; exit 1; } [ "$BITS" -eq 64 ] || { echo "Architecture $ARCH running $BITS-bit operating system is not supported."; exit 1; }
@@ -31,12 +27,18 @@ echo "Architecture $ARCH running $BITS-bit operating system is supported."
echo -e "\nDownloading the latest release..." echo -e "\nDownloading the latest release..."
TMP_DIR=$(mktemp -d) TMP_DIR=$(mktemp -d)
DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \
grep -o "\"browser_download_url\": \"[^\"]*_${ARCH}\.deb\"" | \
sed 's/"browser_download_url": "//;s/"//g')
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
chmod 755 "$TMP_DIR" chmod 755 "$TMP_DIR"
JSON=$(wget -qO- "https://api.github.com/repos/leukipp/touchkio/releases" | tr -d '\r\n')
if $ARG_EARLY; then
DEB_REG='"prerelease":\s*(true|false).*?"browser_download_url":\s*"\K[^\"]*_'$ARCH'\.deb'
else
DEB_REG='"prerelease":\s*false.*?"browser_download_url":\s*"\K[^\"]*_'$ARCH'\.deb'
fi
DEB_URL=$(echo "$JSON" | grep -oP "$DEB_REG" | head -n 1)
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
[ -z "$DEB_URL" ] && { echo "Download url for .deb file not found."; exit 1; } [ -z "$DEB_URL" ] && { echo "Download url for .deb file not found."; exit 1; }
wget --show-progress -q -O "$DEB_PATH" "$DEB_URL" || { echo "Failed to download the .deb file."; exit 1; } wget --show-progress -q -O "$DEB_PATH" "$DEB_URL" || { echo "Failed to download the .deb file."; exit 1; }
@@ -108,8 +110,7 @@ else
fi fi
# Start the setup mode # Start the setup mode
echo "" read -p $'\nStart touchkio setup? (Y/n) ' setup
read -p "Start touchkio setup? (Y/n) " setup
if [[ ${setup:-y} == [Yy]* ]]; then if [[ ${setup:-y} == [Yy]* ]]; then
echo "/usr/bin/touchkio --setup" echo "/usr/bin/touchkio --setup"
+212 -99
View File
@@ -28,6 +28,9 @@ global.HARDWARE = global.HARDWARE || {
keyboard: { keyboard: {
visible: null, visible: null,
}, },
audio: {
device: null,
},
}; };
/** /**
@@ -50,66 +53,87 @@ const init = async () => {
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.max = getDisplayBrightnessMax();
HARDWARE.audio.device = getAudioDevice();
HARDWARE.support = checkSupport(); HARDWARE.support = checkSupport();
HARDWARE.initialized = true; HARDWARE.initialized = true;
// Show supported features // Show supported features
process.stdout.write("\n"); process.stdout.write("\n");
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`); console.info(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
// Show session infos // Show session infos
process.stdout.write("\n"); process.stdout.write("\n");
console.log("User:", HARDWARE.session.user); console.info("User:", HARDWARE.session.user);
console.log("Session:", HARDWARE.session.type); console.info("Session:", HARDWARE.session.type);
console.log("Desktop:", HARDWARE.session.desktop); console.info("Desktop:", HARDWARE.session.desktop);
// Show device infos // Show device infos
process.stdout.write("\n"); process.stdout.write("\n");
console.log("Model:", getModel()); console.info("Model:", getModel());
console.log("Vendor:", getVendor()); console.info("Vendor:", getVendor());
console.log("Serial Number:", getSerialNumber()); console.info("Serial Number:", getSerialNumber());
console.log("Network Addresses:", getNetworkAddresses()); console.info("Network Addresses:", getNetworkAddresses());
console.log("Host Name:", getHostName()); console.info("Host Name:", getHostName());
// Show system infos // Show system infos
process.stdout.write("\n"); process.stdout.write("\n");
console.log("Up Time:", getUpTime()); console.info("Up Time:", getUpTime());
console.log("Memory Size:", getMemorySize()); console.info("Memory Size:", getMemorySize());
console.log("Memory Usage:", getMemoryUsage()); console.info("Memory Usage:", getMemoryUsage());
console.log("Processor Usage:", getProcessorUsage()); console.info("Processor Usage:", getProcessorUsage());
console.log("Processor Temperature:", getProcessorTemperature()); console.info("Processor Temperature:", getProcessorTemperature());
// Show hardware infos // Show hardware infos
process.stdout.write("\n"); process.stdout.write("\n");
const unsupported = "unsupported"; const unsupported = "unsupported";
console.log(`Battery Level [${HARDWARE.battery.level.path || unsupported}]:`, `${getBatteryLevel() || unsupported}`); console.info(`Battery Level [${HARDWARE.battery.level.path || unsupported}]:`, `${getBatteryLevel() || unsupported}`);
console.log( console.info(
`Display Status [${HARDWARE.display.status.path || unsupported}]:`, `Display Status [${HARDWARE.display.status.path || unsupported}]:`,
`${getDisplayStatus() || unsupported} (${HARDWARE.display.status.command || unsupported})`, `${getDisplayStatus() || unsupported} (${HARDWARE.display.status.command || unsupported})`,
); );
console.log( console.info(
`Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`, `Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`,
`${getDisplayBrightness() || unsupported}`, `${getDisplayBrightness() || unsupported}`,
); );
console.log( console.info(
`Audio Volume [${HARDWARE.support.audioVolume ? HARDWARE.audio.device : unsupported}]:`,
`${getAudioVolume() || unsupported}`,
);
console.info(
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`, `Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`,
`${getKeyboardVisibility() || unsupported}`, `${getKeyboardVisibility() || unsupported}`,
); );
// Check for keyboard visibility
process.stdout.write("\n"); process.stdout.write("\n");
setKeyboardVisibility("OFF", (reply, error) => {
if (!reply || error) { // Monitor audio volume
return; if (HARDWARE.support.audioVolume) {
} commandMonitor("pactl", ["subscribe"], (reply, error) => {
dbusMonitor("/sm/puri/OSK0", (property, error) => { if (!reply || error) {
if (!property || error) {
return; return;
} }
HARDWARE.keyboard.visibility = property.Visible === "true"; if (reply.includes("'change' on sink")) {
EVENTS.emit("updateKeyboard"); console.info("Update Audio Volume:", getAudioVolume());
EVENTS.emit("updateVolume");
}
}); });
}); }
// Monitor keyboard visibility
if (HARDWARE.support.keyboardVisibility) {
setKeyboardVisibility("OFF", (reply, error) => {
if (!reply || error) {
return;
}
dbusMonitor("/sm/puri/OSK0", (property, error) => {
if (!property || error) {
return;
}
HARDWARE.keyboard.visibility = property.Visible === "true";
console.info("Update Keyboard Visibility:", getKeyboardVisibility());
EVENTS.emit("updateKeyboard");
});
});
}
// Check for display changes // Check for display changes
setDisplayStatus("ON", () => { setDisplayStatus("ON", () => {
@@ -123,7 +147,7 @@ const init = async () => {
* Updates the shared hardware properties. * Updates the shared hardware properties.
*/ */
const update = async () => { const update = async () => {
if (!HARDWARE.initialized) { if (!HARDWARE.initialized || APP.exiting) {
return; return;
} }
@@ -131,7 +155,7 @@ const update = async () => {
if (HARDWARE.support.displayStatus) { if (HARDWARE.support.displayStatus) {
const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim(); const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim();
if (status !== HARDWARE.display.status.value) { if (status !== HARDWARE.display.status.value) {
console.log("Update Display Status:", getDisplayStatus()); console.info("Update Display Status:", getDisplayStatus());
HARDWARE.display.status.value = status; HARDWARE.display.status.value = status;
EVENTS.emit("updateDisplay"); EVENTS.emit("updateDisplay");
} }
@@ -141,7 +165,7 @@ const update = async () => {
if (HARDWARE.support.displayBrightness) { if (HARDWARE.support.displayBrightness) {
const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim(); const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim();
if (brightness !== HARDWARE.display.brightness.value) { if (brightness !== HARDWARE.display.brightness.value) {
console.log("Update Display Brightness:", getDisplayBrightness()); console.info("Update Display Brightness:", getDisplayBrightness());
HARDWARE.display.brightness.value = brightness; HARDWARE.display.brightness.value = brightness;
EVENTS.emit("updateDisplay"); EVENTS.emit("updateDisplay");
} }
@@ -209,6 +233,7 @@ const checkSupport = () => {
const battery = HARDWARE.battery.level; const battery = HARDWARE.battery.level;
const status = HARDWARE.display.status; const status = HARDWARE.display.status;
const brightness = HARDWARE.display.brightness; const brightness = HARDWARE.display.brightness;
const audio = HARDWARE.audio.device;
const keyboard = processRuns("squeekboard"); const keyboard = processRuns("squeekboard");
const service = serviceRuns(APP.name); const service = serviceRuns(APP.name);
const sudo = sudoRights(); const sudo = sudoRights();
@@ -218,8 +243,9 @@ const checkSupport = () => {
displayStatus: !!status.path && !!status.command, displayStatus: !!status.path && !!status.command,
displayBrightness: sudo && !!brightness.path && !!brightness.max, displayBrightness: sudo && !!brightness.path && !!brightness.max,
keyboardVisibility: keyboard, keyboardVisibility: keyboard,
audioVolume: !!audio,
sudoRights: sudo, sudoRights: sudo,
appUpdate: sudo && service && deb, appUpdate: service && sudo && deb,
}; };
}; };
@@ -483,7 +509,7 @@ const getDisplayStatus = () => {
case "xset": case "xset":
const xset = execSyncCommand("xset", ["-q"]); const xset = execSyncCommand("xset", ["-q"]);
if (xset !== null) { if (xset !== null) {
const output = /Monitor is On/.test(xset); const output = xset.includes("Monitor is On");
return output ? "ON" : "OFF"; return output ? "ON" : "OFF";
} }
break; break;
@@ -599,6 +625,65 @@ const setDisplayBrightness = (brightness, callback = null) => {
proc.stdin.end(); proc.stdin.end();
}; };
/**
* Gets the default audio device using `pactl`.
*
* @returns {string|null} The default audio device or null if an error occurs.
*/
const getAudioDevice = () => {
if (!commandExists("pactl")) {
return null;
}
const output = execSyncCommand("pactl", ["get-default-sink"]);
if (!output) {
return null;
}
return !output.includes("auto_null") ? output.trim() : null;
};
/**
* Gets the default audio device volume using `pactl`.
*
* @returns {number|null} The default audio device volume as a percentage or null if an error occurs.
*/
const getAudioVolume = () => {
if (!HARDWARE.support.audioVolume) {
return null;
}
const mute = execSyncCommand("pactl", ["get-sink-mute", "@DEFAULT_SINK@"]);
const volume = execSyncCommand("pactl", ["get-sink-volume", "@DEFAULT_SINK@"]);
if (!mute || !volume) {
return null;
}
const match = volume.match(/\/(\s*(\d+)%)/);
if (match) {
return Math.max(0, Math.min(100, mute.includes("yes") ? 0 : parseInt(match[2])));
}
return null;
};
/**
* Sets the default audio device volume using `pactl`.
*
* This function takes a volume value between 0 to 100 percent and sends it to the device.
*
* @param {number} volume - The desired volume level (0-100).
* @param {Function} callback - A callback function that receives the output or error.
*/
const setAudioVolume = (volume, callback = null) => {
if (!HARDWARE.support.audioVolume) {
if (typeof callback === "function") callback(null, "Not supported");
return;
}
if (typeof volume !== "number" || volume < 0 || volume > 100) {
console.error("Volume must be a number between 0 and 100");
if (typeof callback === "function") callback(null, "Invalid volume");
return;
}
execAsyncCommand("pactl", ["set-sink-mute", "@DEFAULT_SINK@", volume === 0 ? "1" : "0"]);
execAsyncCommand("pactl", ["set-sink-volume", "@DEFAULT_SINK@", `${volume}%`], callback);
};
/** /**
* Gets the keyboard visibility using global properties. * Gets the keyboard visibility using global properties.
* *
@@ -689,8 +774,8 @@ const rebootSystem = (callback = null) => {
*/ */
const sudoRights = () => { const sudoRights = () => {
try { try {
const output = cpr.execSync(`sudo -n true`, { encoding: "utf8" }); cpr.execSync(`sudo -n true`, { encoding: "utf8", stdio: "ignore" });
return output !== null && !output.includes("password is required"); return true;
} catch {} } catch {}
return false; return false;
}; };
@@ -703,7 +788,8 @@ const sudoRights = () => {
*/ */
const serviceRuns = (name) => { const serviceRuns = (name) => {
try { try {
return !!cpr.execSync(`systemctl --user is-active ${name}`, { encoding: "utf8" }); cpr.execSync(`systemctl --user is-active ${name}`, { encoding: "utf8", stdio: "ignore" });
return true;
} catch {} } catch {}
return false; return false;
}; };
@@ -716,7 +802,8 @@ const serviceRuns = (name) => {
*/ */
const processRuns = (name) => { const processRuns = (name) => {
try { try {
return !!cpr.execSync(`pidof ${name}`, { encoding: "utf8" }); cpr.execSync(`pidof ${name}`, { encoding: "utf8", stdio: "ignore" });
return true;
} catch {} } catch {}
return false; return false;
}; };
@@ -729,7 +816,8 @@ const processRuns = (name) => {
*/ */
const commandExists = (name) => { const commandExists = (name) => {
try { try {
return !!cpr.execSync(`which ${name}`, { encoding: "utf8" }); cpr.execSync(`which ${name}`, { encoding: "utf8", stdio: "ignore" });
return true;
} catch {} } catch {}
return false; return false;
}; };
@@ -741,12 +829,12 @@ const commandExists = (name) => {
* @param {Array<string>} args - The arguments for the command. * @param {Array<string>} args - The arguments for the command.
* @returns {string|null} The output of the command or null if an error occurs. * @returns {string|null} The output of the command or null if an error occurs.
*/ */
const execSyncCommand = (cmd, args = []) => { const execSyncCommand = (cmd, args) => {
try { try {
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:", error.message); console.error(`Execute Sync: '${[cmd, ...args].join(" ")}' --> ${error.message}`);
} }
return null; return null;
}; };
@@ -759,7 +847,7 @@ const execSyncCommand = (cmd, args = []) => {
* @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 execAsyncCommand = (cmd, args = [], callback = null) => { const execAsyncCommand = (cmd, args, callback = null) => {
let errorOutput = ""; let errorOutput = "";
let successOutput = ""; let successOutput = "";
let proc = cpr.spawn(cmd, args); let proc = cpr.spawn(cmd, args);
@@ -774,17 +862,13 @@ const execAsyncCommand = (cmd, args = [], callback = null) => {
} }
}); });
proc.on("close", (code) => { proc.on("close", (code) => {
try { const error = errorOutput.trim().replace(/\0/g, "");
if (typeof callback === "function") { const reply = successOutput.trim().replace(/\0/g, "");
if (code === 0) { if (code !== 0 || error) {
callback(successOutput.trim().replace(/\0/g, ""), null); console.error(`Execute Async: '${[cmd, ...args].join(" ")}' --> ${error} (${code})`);
} else { if (typeof callback === "function") callback(null, error);
callback(null, errorOutput.trim().replace(/\0/g, "")); } else {
} if (typeof callback === "function") callback(reply, null);
}
} catch (error) {
console.error("Execute Async:", error.message);
if (typeof callback === "function") callback(null, error.message);
} }
}); });
return proc; return proc;
@@ -798,7 +882,7 @@ const execAsyncCommand = (cmd, args = [], callback = null) => {
* @param {Function} callback - A callback function that receives the progress or error. * @param {Function} callback - A callback function that receives the progress or error.
* @returns {Object} The spawned process object. * @returns {Object} The spawned process object.
*/ */
const execScriptCommand = (cmd, args = [], callback = null) => { const execScriptCommand = (cmd, args, callback = null) => {
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);
@@ -807,7 +891,7 @@ const execScriptCommand = (cmd, args = [], callback = null) => {
const output = data.toString().trim(); const output = data.toString().trim();
const lines = output.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, ""); const lines = output.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, "");
for (const line of lines.split("\n").filter(Boolean)) { for (const line of lines.split("\n").filter(Boolean)) {
console.log(line); console.info(line);
} }
} }
}); });
@@ -830,15 +914,84 @@ const execScriptCommand = (cmd, args = [], callback = null) => {
}); });
proc.on("close", (code) => { proc.on("close", (code) => {
if (code !== 0) { if (code !== 0) {
console.log(`Script exited with error code ${code}.`); console.error(`Script exited with error code (${code}).`);
if (typeof callback === "function") callback(null, code); if (typeof callback === "function") callback(null, code);
} else { } else {
console.log("Script exited successfully."); console.info("Script exited successfully.");
if (typeof callback === "function") callback(100, null); if (typeof callback === "function") callback(100, null);
} }
}); });
}; };
/**
* Monitors a command asynchronously.
*
* @param {string} cmd - The command to monitor.
* @param {Array<string>} args - The arguments for the command.
* @param {Function} callback - A callback function that receives the output or error.
* @returns {Object} The spawned process object.
*/
const commandMonitor = (cmd, args, callback) => {
const proc = cpr.spawn(cmd, args);
proc.stdout.on("data", (data) => {
if (data) {
const output = data.toString().trim().replace(/\0/g, "");
if (typeof callback === "function") callback(output, null);
}
});
proc.stderr.on("data", (data) => {
if (data) {
const output = data.toString().trim().replace(/\0/g, "");
if (typeof callback === "function") callback(null, output);
}
});
return proc;
};
/**
* Monitors D-Bus property changes asynchronously using `dbus-monitor`.
*
* @param {string} path - The D-Bus object path.
* @param {Function} callback - A callback function that receives the changed property.
* @returns {Object} The spawned process object.
*/
const dbusMonitor = (path, callback) => {
const cmd = "dbus-monitor";
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
const proc = cpr.spawn(cmd, args);
proc.stdout.on("data", (data) => {
try {
const signal = data.toString();
if (signal.includes(`string "Visible"`)) {
const dicts = [...signal.matchAll(/dict entry\(\s*([^)]*?)\)/g)].map((dict) => dict[1].trim());
if (dicts.length) {
dicts.forEach((dict) => {
const key = dict.match(/string "(.*?)"/);
const value = dict.match(/(?<=variant\s+)(.*)/);
if (key && value) {
const property = { [key[1].trim()]: value[1].trim().split(" ").pop() };
if (typeof callback === "function") callback(property, null);
}
});
} else {
const property = { Visible: `${HARDWARE.keyboard.visibility}` };
if (typeof callback === "function") callback(property, null);
}
}
} catch (error) {
console.error("Monitor D-Bus:", error.message);
if (typeof callback === "function") callback(null, error.message);
}
});
proc.stderr.on("data", (data) => {
if (data) {
console.error("Monitor D-Bus:", data.toString());
if (typeof callback === "function") callback(null, data.toString());
}
});
return proc;
};
/** /**
* Executes a D-Bus method call synchronously using `dbus-send`. * Executes a D-Bus method call synchronously using `dbus-send`.
* *
@@ -861,48 +1014,6 @@ const dbusCall = (path, method, values, callback = null) => {
} }
}; };
/**
* Monitors D-Bus property changes asynchronously using `dbus-monitor`.
*
* @param {string} path - The D-Bus object path.
* @param {Function} callback - A callback function that receives the changed property.
* @returns {Object} The spawned process object.
*/
const dbusMonitor = (path, callback) => {
const cmd = "dbus-monitor";
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
const proc = cpr.spawn(cmd, args);
proc.stdout.on("data", (data) => {
try {
const signal = data.toString();
if (signal.includes("member=PropertiesChanged") && signal.includes(`path=${path}`)) {
const dicts = [...signal.matchAll(/dict entry\(\s*([^)]*?)\)/g)].map((dict) => dict[1].trim());
if (dicts.length) {
dicts.forEach((dict) => {
const key = dict.match(/string "(.*?)"/);
const value = dict.match(/(?<=variant\s+)(.*)/);
if (key && value && typeof callback === "function") {
callback({ [key[1].trim()]: value[1].trim().split(" ").pop() }, null);
}
});
} else {
callback({ Visible: `${HARDWARE.keyboard.visibility}` }, null);
}
}
} catch (error) {
console.error("Monitor D-Bus:", error.message);
if (typeof callback === "function") callback(null, error.message);
}
});
proc.stderr.on("data", (data) => {
if (data) {
console.error("Monitor D-Bus:", data.toString());
if (typeof callback === "function") callback(null, data.toString());
}
});
return proc;
};
/** /**
* Helper function for asynchronous interval calls. * Helper function for asynchronous interval calls.
* *
@@ -942,6 +1053,8 @@ module.exports = {
setDisplayStatus, setDisplayStatus,
getDisplayBrightness, getDisplayBrightness,
setDisplayBrightness, setDisplayBrightness,
getAudioVolume,
setAudioVolume,
getKeyboardVisibility, getKeyboardVisibility,
setKeyboardVisibility, setKeyboardVisibility,
checkPackageUpgrades, checkPackageUpgrades,
+245 -95
View File
@@ -48,17 +48,21 @@ const init = async () => {
configuration_url: APP.homepage, configuration_url: APP.homepage,
}; };
// Connection settings // Init options
const masked = password === null ? null : "*".repeat(password.length); const masked = password === null ? null : "*".repeat(password.length);
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.rejectUnauthorized = !("ignore_certificate_errors" in ARGS); options.rejectUnauthorized = !("ignore_certificate_errors" in ARGS);
console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
// Client connect
console.info("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
INTEGRATION.client = mqtt.connect(url.toString(), options); INTEGRATION.client = mqtt.connect(url.toString(), options);
INTEGRATION.client.setMaxListeners(20);
// Client connected // Client connected
INTEGRATION.client INTEGRATION.client
.once("connect", () => { .once("connect", () => {
console.log(`MQTT Connected: ${url.toString()}`); console.info(`MQTT Connected: ${url.toString()}`);
process.stdout.write("\n"); process.stdout.write("\n");
// Init client controls // Init client controls
@@ -67,7 +71,9 @@ const init = async () => {
initReboot(); initReboot();
initRefresh(); initRefresh();
initKiosk(); initKiosk();
initTheme();
initDisplay(); initDisplay();
initVolume();
initKeyboard(); initKeyboard();
initPageNumber(); initPageNumber();
initPageZoom(); initPageZoom();
@@ -85,33 +91,54 @@ const init = async () => {
initProcessorTemperature(); initProcessorTemperature();
initBatteryLevel(); initBatteryLevel();
initPackageUpgrades(); initPackageUpgrades();
initHeartbeat();
initLastActive(); initLastActive();
// Init client diagnostic
initHeartbeat();
initErrors();
initVersion();
// Integration initialized // Integration initialized
INTEGRATION.initialized = true; INTEGRATION.initialized = true;
// Register global events
EVENTS.on("updateDisplay", () => {
updateDisplay();
updateLastActive();
});
EVENTS.on("updateStatus", updateKiosk);
EVENTS.on("updateTheme", updateTheme);
EVENTS.on("updateVolume", updateVolume);
EVENTS.on("updateKeyboard", updateKeyboard);
EVENTS.on("consoleLog", updateErrors);
}) })
.on("error", (error) => { .on("error", (error) => {
console.error("MQTT", error.message); console.error("MQTT", error.message);
}); });
// Update sensor states from events
EVENTS.on("updateDisplay", updateDisplay);
EVENTS.on("updateKeyboard", updateKeyboard);
// Update time sensors periodically (30s) // Update time sensors periodically (30s)
setInterval(() => { setInterval(() => {
updateHeartbeat(); if (APP.exiting) {
return;
}
updateLastActive(); updateLastActive();
updateHeartbeat();
updateErrors();
}, 30 * 1000); }, 30 * 1000);
// Update system sensors periodically (1min) // Update system sensors periodically (1min)
setInterval(() => { setInterval(() => {
if (APP.exiting) {
return;
}
update(); update();
}, 60 * 1000); }, 60 * 1000);
// Update upgrade sensors periodically (1h) // Update upgrade sensors periodically (1h)
setInterval(() => { setInterval(() => {
if (APP.exiting) {
return;
}
updateApp(); updateApp();
updatePackageUpgrades(); updatePackageUpgrades();
}, 3600 * 1000); }, 3600 * 1000);
@@ -123,12 +150,11 @@ const init = async () => {
* Updates the shared integration properties. * Updates the shared integration properties.
*/ */
const update = async () => { const update = async () => {
if (!INTEGRATION.initialized) { if (!INTEGRATION.initialized || APP.exiting) {
return; return;
} }
// Update client sensors // Update client sensors
updateKiosk();
updatePageNumber(); updatePageNumber();
updatePageZoom(); updatePageZoom();
updatePageUrl(); updatePageUrl();
@@ -213,7 +239,7 @@ const initApp = () => {
unique_id: `${INTEGRATION.node}_app`, unique_id: `${INTEGRATION.node}_app`,
command_topic: `${root}/install`, command_topic: `${root}/install`,
state_topic: `${root}/version/state`, state_topic: `${root}/version/state`,
payload_install: "update", payload_install: "app_early" in ARGS ? "update early" : "update",
device: INTEGRATION.device, device: INTEGRATION.device,
}; };
if (!HARDWARE.support.appUpdate) { if (!HARDWARE.support.appUpdate) {
@@ -223,12 +249,12 @@ const initApp = () => {
publishConfig("update", config) publishConfig("update", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
console.log("Update App..."); console.info("Update App...");
hardware.setDisplayStatus("ON", () => { hardware.setDisplayStatus("ON", () => {
const args = ["-c", `bash <(wget -qO- ${APP.scripts.install}) update`]; const args = ["-c", `bash <(wget -qO- ${APP.scripts.install}) ${message.toString()}`];
hardware.execScriptCommand("bash", args, (progress, error) => { hardware.execScriptCommand("bash", args, (progress, error) => {
if (progress) { if (progress) {
console.log(`Progress: ${progress}%`); console.info(`Progress: ${progress}%`);
} }
updateApp(progress); updateApp(progress);
}); });
@@ -279,7 +305,7 @@ const initShutdown = () => {
publishConfig("button", config) publishConfig("button", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
console.log("Shutdown system..."); console.verbose("Shutdown system...");
hardware.setDisplayStatus("ON", () => { hardware.setDisplayStatus("ON", () => {
hardware.shutdownSystem(); hardware.shutdownSystem();
}); });
@@ -307,7 +333,7 @@ const initReboot = () => {
publishConfig("button", config) publishConfig("button", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
console.log("Rebooting system..."); console.verbose("Rebooting system...");
hardware.setDisplayStatus("ON", () => { hardware.setDisplayStatus("ON", () => {
hardware.rebootSystem(); hardware.rebootSystem();
}); });
@@ -331,7 +357,7 @@ const initRefresh = () => {
publishConfig("button", config) publishConfig("button", config)
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
console.log("Refreshing webview..."); console.verbose("Refreshing webview...");
hardware.setDisplayStatus("ON", () => { hardware.setDisplayStatus("ON", () => {
EVENTS.emit("reloadView"); EVENTS.emit("reloadView");
}); });
@@ -359,32 +385,9 @@ const initKiosk = () => {
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
const status = message.toString(); const status = message.toString();
console.log("Set Kiosk Status:", status); console.verbose("Set Kiosk Status:", status);
hardware.setDisplayStatus("ON", () => { hardware.setDisplayStatus("ON", () => {
switch (status) { WEBVIEW.window.setStatus(status);
case "Framed":
WEBVIEW.window.restore();
WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(false);
break;
case "Fullscreen":
WEBVIEW.window.restore();
WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(true);
break;
case "Maximized":
WEBVIEW.window.restore();
WEBVIEW.window.setFullScreen(false);
WEBVIEW.window.maximize();
break;
case "Minimized":
WEBVIEW.window.restore();
WEBVIEW.window.setFullScreen(false);
WEBVIEW.window.minimize();
break;
case "Terminated":
app.quit();
}
}); });
} }
}) })
@@ -396,10 +399,45 @@ const initKiosk = () => {
* Updates the kiosk status via the mqtt connection. * Updates the kiosk status via the mqtt connection.
*/ */
const updateKiosk = async () => { const updateKiosk = async () => {
const kiosk = WEBVIEW.tracker.status; const kiosk = WEBVIEW.tracker.window.status;
publishState("kiosk", kiosk); publishState("kiosk", kiosk);
}; };
/**
* Initializes the application theme and handles the execute logic.
*/
const initTheme = () => {
const root = `${INTEGRATION.root}/theme`;
const config = {
name: "Theme",
unique_id: `${INTEGRATION.node}_theme`,
command_topic: `${root}/set`,
state_topic: `${root}/state`,
value_template: "{{ value }}",
options: ["Light", "Dark"],
icon: "mdi:compare",
device: INTEGRATION.device,
};
publishConfig("select", config)
.on("message", (topic, message) => {
if (topic === config.command_topic) {
const theme = message.toString();
console.verbose("Set Application Theme:", theme);
WEBVIEW.theme.set(theme);
}
})
.subscribe(config.command_topic);
updateTheme();
};
/**
* Updates the application theme via the mqtt connection.
*/
const updateTheme = async () => {
const theme = WEBVIEW.theme.get();
publishState("theme", theme.charAt(0).toUpperCase() + theme.slice(1));
};
/** /**
* Initializes the display status, brightness and handles the execute logic. * Initializes the display status, brightness and handles the execute logic.
*/ */
@@ -427,22 +465,22 @@ const initDisplay = () => {
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
const status = message.toString(); const status = message.toString();
console.log("Set Display Status:", status); console.verbose("Set Display Status:", status);
hardware.setDisplayStatus(status, (reply, error) => { hardware.setDisplayStatus(status, (reply, error) => {
if (!error) { if (!error) {
hardware.update(); hardware.update();
} else { } else {
console.warn("Failed:", error); console.warn("Command Failed:", error);
} }
}); });
} else if (topic === config.brightness_command_topic) { } else if (topic === config.brightness_command_topic) {
const brightness = parseInt(message, 10); const brightness = parseInt(message, 10);
console.log("Set Display Brightness:", brightness); console.verbose("Set Display Brightness:", brightness);
hardware.setDisplayBrightness(brightness, (reply, error) => { hardware.setDisplayBrightness(brightness, (reply, error) => {
if (!error) { if (!error) {
hardware.update(); hardware.update();
} else { } else {
console.warn("Failed:", error); console.warn("Command Failed:", error);
} }
}); });
} }
@@ -462,6 +500,48 @@ const updateDisplay = async () => {
publishState("display/brightness", brightness); publishState("display/brightness", brightness);
}; };
/**
* Initializes the audio volume and handles the execute logic.
*/
const initVolume = () => {
const root = `${INTEGRATION.root}/volume`;
const config = {
name: "Volume",
unique_id: `${INTEGRATION.node}_volume`,
command_topic: `${root}/set`,
state_topic: `${root}/state`,
value_template: "{{ value | int }}",
mode: "slider",
min: 0,
max: 100,
unit_of_measurement: "%",
icon: "mdi:volume-high",
device: INTEGRATION.device,
};
if (!HARDWARE.support.audioVolume) {
removeConfig("number", config);
return;
}
publishConfig("number", config)
.on("message", (topic, message) => {
if (topic === config.command_topic) {
const volume = parseInt(message, 10);
console.verbose("Set Audio Volume:", volume);
hardware.setAudioVolume(volume);
}
})
.subscribe(config.command_topic);
updateVolume();
};
/**
* Updates the audio volume via the mqtt connection.
*/
const updateVolume = async () => {
const volume = hardware.getAudioVolume();
publishState("volume", volume);
};
/** /**
* Initializes the keyboard visibility and handles the execute logic. * Initializes the keyboard visibility and handles the execute logic.
*/ */
@@ -483,20 +563,8 @@ const initKeyboard = () => {
.on("message", (topic, message) => { .on("message", (topic, message) => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
const status = message.toString(); const status = message.toString();
console.log("Set Keyboard Visibility:", status); console.verbose("Set Keyboard Visibility:", status);
hardware.setDisplayStatus("ON", () => { hardware.setDisplayStatus("ON", () => {
switch (status) {
case "OFF":
WEBVIEW.window.restore();
WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(true);
break;
case "ON":
WEBVIEW.window.restore();
WEBVIEW.window.setFullScreen(false);
WEBVIEW.window.maximize();
break;
}
hardware.setKeyboardVisibility(status); hardware.setKeyboardVisibility(status);
}); });
} }
@@ -539,7 +607,7 @@ 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.log("Set Page Number:", number); console.verbose("Set Page Number:", number);
WEBVIEW.viewActive = number || 1; WEBVIEW.viewActive = number || 1;
EVENTS.emit("updateView"); EVENTS.emit("updateView");
} }
@@ -567,9 +635,10 @@ const initPageZoom = () => {
command_topic: `${root}/set`, command_topic: `${root}/set`,
state_topic: `${root}/state`, state_topic: `${root}/state`,
value_template: "{{ value | int }}", value_template: "{{ value | int }}",
mode: "slider", mode: "box",
min: 25, min: 25,
max: 400, max: 400,
step: 5,
unit_of_measurement: "%", unit_of_measurement: "%",
icon: "mdi:magnify-plus", icon: "mdi:magnify-plus",
device: INTEGRATION.device, device: INTEGRATION.device,
@@ -578,7 +647,7 @@ 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.log("Set Page Zoom:", zoom); console.verbose("Set Page Zoom:", zoom);
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.setZoomFactor(zoom / 100.0); WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.setZoomFactor(zoom / 100.0);
EVENTS.emit("updateView"); EVENTS.emit("updateView");
} }
@@ -614,7 +683,7 @@ 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.log("Set Page Url:", url); console.verbose("Set Page Url:", url);
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.loadURL(url); WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.loadURL(url);
} }
}) })
@@ -748,6 +817,7 @@ const initUpTime = () => {
name: "Up Time", name: "Up Time",
unique_id: `${INTEGRATION.node}_up_time`, unique_id: `${INTEGRATION.node}_up_time`,
state_topic: `${root}/state`, state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ (value | float) | round(0) }}", value_template: "{{ (value | float) | round(0) }}",
unit_of_measurement: "min", unit_of_measurement: "min",
icon: "mdi:timeline-clock", icon: "mdi:timeline-clock",
@@ -762,7 +832,11 @@ const initUpTime = () => {
*/ */
const updateUpTime = async () => { const updateUpTime = async () => {
const upTime = hardware.getUpTime(); const upTime = hardware.getUpTime();
const bootTime = {
boot: new Date(new Date().getTime() - upTime * 60 * 1000),
};
publishState("up_time", upTime); publishState("up_time", upTime);
publishAttributes("up_time", bootTime);
}; };
/** /**
@@ -923,40 +997,15 @@ const initPackageUpgrades = () => {
const updatePackageUpgrades = async () => { const updatePackageUpgrades = async () => {
const packages = hardware.checkPackageUpgrades(); const packages = hardware.checkPackageUpgrades();
const upgrades = { const upgrades = {
total: packages.length, packages: packages.map((pkg) => {
packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()), const [name, version] = pkg.replace(/\s*\[.*?\]\s*/g, "").split(/\s+/, 2);
return { [name]: version };
}),
}; };
publishState("package_upgrades", upgrades.total); publishState("package_upgrades", packages.length);
publishAttributes("package_upgrades", upgrades); publishAttributes("package_upgrades", upgrades);
}; };
/**
* Initializes the heartbeat sensor.
*/
const initHeartbeat = () => {
const root = `${INTEGRATION.root}/heartbeat`;
const config = {
name: "Heartbeat",
unique_id: `${INTEGRATION.node}_heartbeat`,
state_topic: `${root}/state`,
value_template: "{{ value }}",
icon: "mdi:heart-flash",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateHeartbeat();
};
/**
* Updates the heartbeat sensor via the mqtt connection.
*/
const updateHeartbeat = async () => {
const now = new Date();
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
publishState("heartbeat", heartbeat);
};
/** /**
* Initializes the last active sensor. * Initializes the last active sensor.
*/ */
@@ -966,6 +1015,7 @@ const initLastActive = () => {
name: "Last Active", name: "Last Active",
unique_id: `${INTEGRATION.node}_last_active`, unique_id: `${INTEGRATION.node}_last_active`,
state_topic: `${root}/state`, state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ (value | float) | round(0) }}", value_template: "{{ (value | float) | round(0) }}",
unit_of_measurement: "min", unit_of_measurement: "min",
icon: "mdi:gesture-tap-hold", icon: "mdi:gesture-tap-hold",
@@ -982,7 +1032,107 @@ const updateLastActive = async () => {
const now = new Date(); const now = new Date();
const then = WEBVIEW.tracker.pointer.time; const then = WEBVIEW.tracker.pointer.time;
const lastActive = (now - then) / (1000 * 60); const lastActive = (now - then) / (1000 * 60);
const tracker = {
...WEBVIEW.tracker.pointer.position,
...WEBVIEW.tracker.display,
};
publishState("last_active", lastActive); publishState("last_active", lastActive);
publishAttributes("last_active", tracker);
};
/**
* Initializes the heartbeat sensor.
*/
const initHeartbeat = () => {
const root = `${INTEGRATION.root}/heartbeat`;
const config = {
name: "Heartbeat",
unique_id: `${INTEGRATION.node}_heartbeat`,
state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value }}",
entity_category: "diagnostic",
icon: "mdi:heart-flash",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateHeartbeat();
};
/**
* Updates the heartbeat sensor via the mqtt connection.
*/
const updateHeartbeat = async () => {
const now = new Date();
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
const attributes = { date: now };
publishState("heartbeat", heartbeat);
publishAttributes("heartbeat", attributes);
};
/**
* Initializes the error log sensor.
*/
const initErrors = () => {
const root = `${INTEGRATION.root}/errors`;
const config = {
name: "Errors",
unique_id: `${INTEGRATION.node}_errors`,
state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value | int }}",
entity_category: "diagnostic",
icon: "mdi:alert-circle",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateErrors();
};
/**
* Updates the error log sensor via the mqtt connection.
*/
const updateErrors = async () => {
const logs = APP.logs.slice().reverse();
const errors = logs.filter((log) => log.level === "error");
const history = logs.reduce((acc, log) => {
const time = log.time.toISOString().slice(0, 16);
if (!acc[time]) {
acc[time] = [];
}
acc[time].push({ [log.level.toUpperCase()]: log.text });
return acc;
}, {});
publishState("errors", errors.length);
publishAttributes("errors", history);
};
/**
* Initializes the version sensor.
*/
const initVersion = () => {
const root = `${INTEGRATION.root}/version`;
const config = {
name: "Version",
unique_id: `${INTEGRATION.node}_version`,
state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value }}",
entity_category: "diagnostic",
icon: "mdi:application-braces",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateVersion();
};
/**
* Updates the version sensor via the mqtt connection.
*/
const updateVersion = async () => {
publishState("version", APP.version);
publishAttributes("version", APP.build);
}; };
module.exports = { module.exports = {
+406 -156
View File
@@ -3,17 +3,32 @@ const axios = require("axios");
const https = require("https"); const https = require("https");
const hardware = require("./hardware"); const hardware = require("./hardware");
const integration = require("./integration"); const integration = require("./integration");
const { app, nativeTheme, globalShortcut, ipcMain, session, BaseWindow, WebContentsView } = require("electron"); const {
app,
nativeTheme,
globalShortcut,
ipcMain,
dialog,
screen,
session,
BaseWindow,
WebContentsView,
} = require("electron");
global.WEBVIEW = global.WEBVIEW || { global.WEBVIEW = global.WEBVIEW || {
initialized: false, initialized: false,
tracker: { tracker: {
display: {},
pointer: { pointer: {
position: {}, position: {},
time: new Date(), time: new Date(),
}, },
display: {}, window: {
status: null, status: null,
},
widget: {
focused: null,
},
}, },
}; };
@@ -34,7 +49,7 @@ const init = async () => {
session.defaultSession.clearCache(); session.defaultSession.clearCache();
// Parse arguments // Parse arguments
const fullscreen = ARGS.app_debug !== "true"; 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 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";
@@ -44,24 +59,52 @@ const init = async () => {
WEBVIEW.viewActive = 0; WEBVIEW.viewActive = 0;
WEBVIEW.viewUrls = urls; WEBVIEW.viewUrls = urls;
WEBVIEW.viewZoom = zoom; WEBVIEW.viewZoom = zoom;
WEBVIEW.viewTheme = theme; WEBVIEW.statusEnabled = true;
WEBVIEW.pagerEnabled = widget; WEBVIEW.pagerEnabled = widget;
WEBVIEW.widgetTheme = theme;
WEBVIEW.widgetEnabled = widget; WEBVIEW.widgetEnabled = widget;
WEBVIEW.navigationTheme = theme;
WEBVIEW.navigationEnabled = widget; WEBVIEW.navigationEnabled = widget;
nativeTheme.themeSource = WEBVIEW.viewTheme;
// Init global theme
WEBVIEW.theme = {
set: (name) => {
nativeTheme.themeSource = name.toLowerCase();
},
get: () => {
return nativeTheme.shouldUseDarkColors ? "dark" : "light";
},
toggle: () => {
nativeTheme.themeSource = nativeTheme.shouldUseDarkColors ? "light" : "dark";
},
register: () => {
nativeTheme.on("updated", () => {
const theme = WEBVIEW.theme.get();
WEBVIEW.statusTheme = theme;
WEBVIEW.widgetTheme = theme;
WEBVIEW.navigationTheme = theme;
if (WEBVIEW.initialized) {
resizeView();
}
EVENTS.emit("updateTheme");
});
},
};
WEBVIEW.theme.register();
WEBVIEW.theme.set(theme);
// Init global root window // Init global root window
WEBVIEW.display = screen.getPrimaryDisplay().workAreaSize;
WEBVIEW.window = new BaseWindow({ WEBVIEW.window = new BaseWindow({
title: APP.title, title: APP.title,
icon: path.join(APP.path, "img", "icon.png"), icon: APP.icon,
fullscreen: !debug,
autoHideMenuBar: true,
frame: debug || !WEBVIEW.statusEnabled,
width: Math.floor(WEBVIEW.display.width * 0.9),
height: Math.floor(WEBVIEW.display.height * 0.9),
minWidth: 136,
minHeight: 136,
}); });
WEBVIEW.window.setMenuBarVisibility(false); WEBVIEW.window.setMenuBarVisibility(false);
WEBVIEW.window.setFullScreen(fullscreen);
if (!fullscreen) {
WEBVIEW.window.maximize();
}
// Init global webview // Init global webview
WEBVIEW.views = []; WEBVIEW.views = [];
@@ -105,6 +148,18 @@ const init = async () => {
WEBVIEW.window.contentView.addChildView(WEBVIEW.widget); WEBVIEW.window.contentView.addChildView(WEBVIEW.widget);
WEBVIEW.widget.webContents.loadFile(path.join(APP.path, "html", "widget.html")); WEBVIEW.widget.webContents.loadFile(path.join(APP.path, "html", "widget.html"));
// Init global status
WEBVIEW.status = new WebContentsView({
transparent: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
WEBVIEW.status.setBackgroundColor("#00000000");
WEBVIEW.window.contentView.addChildView(WEBVIEW.status);
WEBVIEW.status.webContents.loadFile(path.join(APP.path, "html", "status.html"));
// Init global navigation // Init global navigation
WEBVIEW.navigation = new WebContentsView({ WEBVIEW.navigation = new WebContentsView({
transparent: true, transparent: true,
@@ -118,18 +173,9 @@ const init = async () => {
WEBVIEW.navigation.webContents.loadFile(path.join(APP.path, "html", "navigation.html")); WEBVIEW.navigation.webContents.loadFile(path.join(APP.path, "html", "navigation.html"));
// Register global events // Register global events
EVENTS.on("reloadView", reloadView);
EVENTS.on("updateView", updateView);
EVENTS.on("updateDisplay", () => {
const status = hardware.getDisplayStatus();
if (status) {
WEBVIEW.tracker.display[status.toLowerCase()] = new Date();
}
});
// Register local events
await windowEvents(); await windowEvents();
await widgetEvents(); await widgetEvents();
await statusEvents();
await navigationEvents(); await navigationEvents();
await viewEvents(); await viewEvents();
await appEvents(); await appEvents();
@@ -141,20 +187,12 @@ const init = async () => {
* Updates the shared webview properties. * Updates the shared webview properties.
*/ */
const update = async () => { const update = async () => {
if (!WEBVIEW.initialized) { if (!WEBVIEW.initialized || APP.exiting) {
return; return;
} }
// Update window status // Update window status
if (WEBVIEW.window.isFullScreen()) { updateStatus();
WEBVIEW.tracker.status = "Fullscreen";
} else if (WEBVIEW.window.isMinimized()) {
WEBVIEW.tracker.status = "Minimized";
} else if (WEBVIEW.window.isMaximized()) {
WEBVIEW.tracker.status = "Maximized";
} else {
WEBVIEW.tracker.status = "Framed";
}
// Update widget status // Update widget status
updateWidget(); updateWidget();
@@ -163,7 +201,6 @@ const update = async () => {
updateNavigation(); updateNavigation();
// Update integration sensor // Update integration sensor
console.log("Update Kiosk Status:", WEBVIEW.tracker.status);
integration.update(); integration.update();
}; };
@@ -177,9 +214,13 @@ const updateView = () => {
const url = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL(); const url = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL();
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();
// Update window title // Update window title
console.log(`Update View: ${title}`); if (previous !== title) {
console.info(`Update View: ${title}`);
}
WEBVIEW.status.webContents.send("text-content", { id: "title", content: title });
WEBVIEW.window.setTitle(title); WEBVIEW.window.setTitle(title);
// Hide all other webviews and show only the active one // Hide all other webviews and show only the active one
@@ -189,10 +230,40 @@ const updateView = () => {
update(); update();
}; };
/**
* Updates the window status.
*/
const updateStatus = () => {
const previous = WEBVIEW.tracker.window.status;
// Set window status
if (WEBVIEW.window.isFullScreen()) {
WEBVIEW.tracker.window.status = "Fullscreen";
} else if (WEBVIEW.window.isMinimized()) {
WEBVIEW.tracker.window.status = "Minimized";
} else if (WEBVIEW.window.isMaximized()) {
WEBVIEW.tracker.window.status = "Maximized";
} else {
WEBVIEW.tracker.window.status = "Framed";
}
// Update window status
if (previous !== WEBVIEW.tracker.window.status) {
console.info(`Update Kiosk Status: ${WEBVIEW.tracker.window.status}`);
}
EVENTS.emit("updateStatus");
};
/** /**
* 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",
@@ -240,6 +311,10 @@ const updateNavigation = () => {
text: currentUrl.startsWith("data:") ? "" : currentUrl, text: currentUrl.startsWith("data:") ? "" : currentUrl,
placeholder: defaultUrl.startsWith("data:") ? "" : defaultUrl, placeholder: defaultUrl.startsWith("data:") ? "" : defaultUrl,
}); });
WEBVIEW.navigation.webContents.send("input-readonly", {
id: "url",
readonly: !!HARDWARE.support.keyboardVisibility,
});
// Disable zoom buttons // Disable zoom buttons
WEBVIEW.navigation.webContents.send("button-disabled", { WEBVIEW.navigation.webContents.send("button-disabled", {
@@ -264,24 +339,60 @@ const updateNavigation = () => {
/** /**
* Shows or hides the webview navigation bar. * Shows or hides the webview navigation bar.
*
* @param {string} force - Force the navigation bar visibility to 'ON' or 'OFF'.
*/ */
const toggleNavigation = () => { const toggleNavigation = (force = null) => {
if (!WEBVIEW.navigationEnabled) {
return;
}
const window = WEBVIEW.window.getBounds(); const window = WEBVIEW.window.getBounds();
const status = WEBVIEW.status.getBounds();
const navigation = WEBVIEW.navigation.getBounds(); const navigation = WEBVIEW.navigation.getBounds();
const height = navigation.height > 0 ? 0 : 60;
// Calculate navigation height
const height = force === "ON" ? 50 : force === "OFF" ? 0 : navigation.height > 0 ? 0 : 50;
if (height === navigation.height) {
return;
}
// Show or hide navigation based on height // Show or hide navigation based on height
WEBVIEW.navigation.setBounds({ WEBVIEW.navigation.setBounds({
x: 0, x: 0,
y: window.height - height, y: window.height - status.height - height,
width: window.width, width: window.width,
height: height, height: height,
}); });
if (height > 0) {
WEBVIEW.navigation.webContents.focus(); // Resize webview
} else { resizeView();
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus(); };
/**
* Shows or hides the webview status bar.
*
* @param {string} force - Force the status bar visibility to 'ON' or 'OFF'.
*/
const toggleStatus = (force = null) => {
if (!WEBVIEW.statusEnabled) {
return;
} }
const window = WEBVIEW.window.getBounds();
const status = WEBVIEW.status.getBounds();
// Calculate status height
const height = force === "ON" ? 40 : force === "OFF" ? 0 : status.height > 0 ? 0 : 40;
if (height === status.height) {
return;
}
// Show or hide status based on height
WEBVIEW.status.setBounds({
x: 0,
y: 0,
width: window.width,
height: height,
});
// Resize webview // Resize webview
resizeView(); resizeView();
@@ -292,7 +403,8 @@ const toggleNavigation = () => {
*/ */
const zoomMinus = () => { const zoomMinus = () => {
const view = WEBVIEW.views[WEBVIEW.viewActive]; const view = WEBVIEW.views[WEBVIEW.viewActive];
view.webContents.setZoomFactor(Math.max(0.25, view.webContents.getZoomFactor() - 0.1)); const zoom = view.webContents.getZoomFactor() - 0.1;
view.webContents.setZoomFactor(Math.max(0.25, Math.round(zoom * 10) / 10));
update(); update();
}; };
@@ -301,7 +413,8 @@ const zoomMinus = () => {
*/ */
const zoomPlus = () => { const zoomPlus = () => {
const view = WEBVIEW.views[WEBVIEW.viewActive]; const view = WEBVIEW.views[WEBVIEW.viewActive];
view.webContents.setZoomFactor(Math.min(4.0, view.webContents.getZoomFactor() + 0.1)); const zoom = view.webContents.getZoomFactor() + 0.1;
view.webContents.setZoomFactor(Math.min(4.0, Math.round(zoom * 10) / 10));
update(); update();
}; };
@@ -349,6 +462,9 @@ const homeView = () => {
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
APP.logs = [];
// Reload the default url or refresh the page // Reload the default url or refresh the page
if (currentUrl != defaultUrl) { if (currentUrl != defaultUrl) {
view.webContents.loadURL(defaultUrl); view.webContents.loadURL(defaultUrl);
@@ -372,6 +488,9 @@ const reloadView = () => {
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
APP.logs = [];
// Reload the default url or refresh the page // Reload the default url or refresh the page
if (currentUrl.startsWith("data:")) { if (currentUrl.startsWith("data:")) {
view.webContents.loadURL(defaultUrl); view.webContents.loadURL(defaultUrl);
@@ -386,6 +505,7 @@ const reloadView = () => {
*/ */
const resizeView = () => { const resizeView = () => {
const window = WEBVIEW.window.getBounds(); const window = WEBVIEW.window.getBounds();
const status = WEBVIEW.status.getBounds();
const navigation = WEBVIEW.navigation.getBounds(); const navigation = WEBVIEW.navigation.getBounds();
const pager = { width: 20, height: window.height }; const pager = { width: 20, height: window.height };
const widget = { width: 60, height: 200 }; const widget = { width: 60, height: 200 };
@@ -394,9 +514,9 @@ const resizeView = () => {
WEBVIEW.views.forEach((view) => { WEBVIEW.views.forEach((view) => {
view.setBounds({ view.setBounds({
x: 0, x: 0,
y: 0, y: status.height,
width: window.width, width: window.width,
height: window.height - navigation.height, height: window.height - status.height - navigation.height,
}); });
}); });
@@ -404,22 +524,39 @@ const resizeView = () => {
if (WEBVIEW.pagerEnabled) { if (WEBVIEW.pagerEnabled) {
WEBVIEW.pager.setBounds({ WEBVIEW.pager.setBounds({
x: window.width - pager.width, x: window.width - pager.width,
y: 0, y: status.height,
width: pager.width, width: pager.width,
height: pager.height, height: pager.height - status.height - navigation.height,
});
WEBVIEW.pager.webContents.send("data-theme", {
theme: "hidden",
}); });
WEBVIEW.pager.webContents.send("data-theme", { theme: "hidden" });
} }
// Update widget size // Update widget size
if (WEBVIEW.widgetEnabled) { if (WEBVIEW.widgetEnabled) {
WEBVIEW.widget.setBounds({ WEBVIEW.widget.setBounds({
x: window.width - 20, x: WEBVIEW.tracker.widget.focused ? window.width - widget.width : window.width - pager.width,
y: parseInt(window.height / 2 - widget.height / 2, 10), y: status.height + parseInt((window.height - status.height - widget.height) / 2, 10),
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
}); });
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" }); WEBVIEW.widget.webContents.send("data-theme", {
theme: WEBVIEW.tracker.widget.focused ? WEBVIEW.widgetTheme : "hidden",
});
}
// Update status size
if (WEBVIEW.statusEnabled) {
WEBVIEW.status.setBounds({
x: 0,
y: 0,
width: window.width,
height: status.height,
});
WEBVIEW.status.webContents.send("data-theme", {
theme: WEBVIEW.statusTheme,
});
} }
// Update navigation size // Update navigation size
@@ -430,7 +567,9 @@ const resizeView = () => {
width: window.width, width: window.width,
height: navigation.height, height: navigation.height,
}); });
WEBVIEW.navigation.webContents.send("data-theme", { theme: WEBVIEW.navigationTheme }); WEBVIEW.navigation.webContents.send("data-theme", {
theme: WEBVIEW.navigationTheme,
});
} }
}; };
@@ -444,12 +583,62 @@ const windowEvents = async () => {
resizeView(); resizeView();
// Handle window status updates // Handle window status updates
WEBVIEW.window.on("minimize", update); WEBVIEW.window.setStatus = async (status) => {
WEBVIEW.window.on("restore", update); const apply = (func, ...args) => {
WEBVIEW.window.on("maximize", update); func.apply(WEBVIEW.window, args);
WEBVIEW.window.on("unmaximize", update); return new Promise((r) => setTimeout(r, 50));
WEBVIEW.window.on("enter-full-screen", update); };
WEBVIEW.window.on("leave-full-screen", update); if (WEBVIEW.window.isMinimized()) {
await apply(WEBVIEW.window.restore);
}
switch (status) {
case "Framed":
if (WEBVIEW.window.isFullScreen()) {
await apply(WEBVIEW.window.setFullScreen, false);
}
if (WEBVIEW.window.isMaximized()) {
await apply(WEBVIEW.window.unmaximize);
}
break;
case "Fullscreen":
if (!WEBVIEW.window.isMaximized()) {
await apply(WEBVIEW.window.maximize);
}
if (!WEBVIEW.window.isFullScreen()) {
await apply(WEBVIEW.window.setFullScreen, true);
}
break;
case "Maximized":
if (WEBVIEW.window.isFullScreen()) {
await apply(WEBVIEW.window.setFullScreen, false);
}
if (!WEBVIEW.window.isMaximized()) {
await apply(WEBVIEW.window.maximize);
}
break;
case "Minimized":
if (WEBVIEW.window.isFullScreen()) {
await apply(WEBVIEW.window.setFullScreen, false);
}
if (!WEBVIEW.window.isMinimized()) {
await apply(WEBVIEW.window.minimize);
}
break;
case "Terminated":
app.quit();
break;
}
};
WEBVIEW.window.onStatus = () => {
clearTimeout(WEBVIEW.window.onStatus.timeout);
WEBVIEW.window.onStatus.timeout = setTimeout(update, 200);
};
WEBVIEW.window.on("restore", WEBVIEW.window.onStatus);
WEBVIEW.window.on("minimize", WEBVIEW.window.onStatus);
WEBVIEW.window.on("maximize", WEBVIEW.window.onStatus);
WEBVIEW.window.on("unmaximize", WEBVIEW.window.onStatus);
WEBVIEW.window.on("enter-full-screen", WEBVIEW.window.onStatus);
WEBVIEW.window.on("leave-full-screen", WEBVIEW.window.onStatus);
// Handle global shortcut events // Handle global shortcut events
globalShortcut.register("Control+Left", () => { globalShortcut.register("Control+Left", () => {
@@ -473,16 +662,19 @@ const windowEvents = async () => {
// Check for window touch events (1s) // Check for window touch events (1s)
setInterval(() => { setInterval(() => {
if (APP.exiting) {
return;
}
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;
// Auto-hide navigation // Auto-hide controls
if (delta > 60) { if (delta > 60) {
const navigation = WEBVIEW.navigation.getBounds(); if (WEBVIEW.tracker.widget.focused) {
if (navigation.height > 0) { WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
toggleNavigation();
} }
toggleNavigation("OFF");
} }
}, 1000); }, 1000);
}; };
@@ -497,6 +689,7 @@ const widgetEvents = async () => {
// Handle widget focus events // Handle widget focus events
WEBVIEW.widget.webContents.on("focus", () => { WEBVIEW.widget.webContents.on("focus", () => {
WEBVIEW.tracker.pointer.time = new Date();
const window = WEBVIEW.window.getBounds(); const window = WEBVIEW.window.getBounds();
const widget = WEBVIEW.widget.getBounds(); const widget = WEBVIEW.widget.getBounds();
@@ -507,7 +700,10 @@ const widgetEvents = async () => {
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
}); });
WEBVIEW.widget.webContents.send("data-theme", { theme: WEBVIEW.widgetTheme }); WEBVIEW.widget.webContents.send("data-theme", {
theme: WEBVIEW.widgetTheme,
});
WEBVIEW.tracker.widget.focused = true;
}); });
// Handle widget blur events // Handle widget blur events
@@ -522,49 +718,21 @@ const widgetEvents = async () => {
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
}); });
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" }); WEBVIEW.widget.webContents.send("data-theme", {
theme: "hidden",
});
WEBVIEW.tracker.widget.focused = false;
}); });
// Handle widget button click events // Handle widget button click events
ipcMain.on("button-click", (e, button) => { ipcMain.on("button-click", (e, button) => {
WEBVIEW.tracker.pointer.time = new Date();
switch (button.id) { switch (button.id) {
case "theme":
WEBVIEW.theme.toggle();
break;
case "keyboard": case "keyboard":
const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON"; hardware.setKeyboardVisibility(hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON");
switch (toggle) {
case "OFF":
WEBVIEW.window.restore();
WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(true);
break;
case "ON":
WEBVIEW.window.restore();
WEBVIEW.window.setFullScreen(false);
WEBVIEW.window.maximize();
break;
}
hardware.setKeyboardVisibility(toggle, () => {
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
});
break;
case "fullscreen":
if (WEBVIEW.window.isFullScreen()) {
WEBVIEW.window.restore();
WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(false);
} else {
WEBVIEW.window.restore();
WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(true);
}
hardware.setKeyboardVisibility("OFF", () => {
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
});
break;
case "minimize":
WEBVIEW.window.restore();
WEBVIEW.window.setFullScreen(false);
WEBVIEW.window.minimize();
hardware.setKeyboardVisibility("OFF");
break; break;
case "navigation": case "navigation":
toggleNavigation(); toggleNavigation();
@@ -573,6 +741,48 @@ const widgetEvents = async () => {
}); });
}; };
/**
* Register status events and handler.
*/
const statusEvents = async () => {
if (!WEBVIEW.statusEnabled) {
return;
}
// Handle status button click events
ipcMain.on("button-click", (e, button) => {
WEBVIEW.tracker.pointer.time = new Date();
switch (button.id) {
case "fullscreen":
WEBVIEW.window.setStatus(WEBVIEW.tracker.window.status === "Fullscreen" ? "Framed" : "Fullscreen");
break;
case "minimize":
WEBVIEW.window.setStatus("Minimized");
break;
case "terminate":
WEBVIEW.status.webContents.send("button-disabled", { id: "fullscreen", disabled: true });
WEBVIEW.status.webContents.send("button-disabled", { id: "minimize", disabled: true });
WEBVIEW.status.webContents.send("button-disabled", { id: "terminate", disabled: true });
const button = dialog.showMessageBoxSync(WEBVIEW.window, {
type: "question",
title: "Confirm",
message: `\nExit ${APP.title}?`,
buttons: ["No", "Yes"],
});
switch (button) {
case 1:
WEBVIEW.window.setStatus("Terminated");
break;
default:
WEBVIEW.status.webContents.send("button-disabled", { id: "fullscreen", disabled: false });
WEBVIEW.status.webContents.send("button-disabled", { id: "minimize", disabled: false });
WEBVIEW.status.webContents.send("button-disabled", { id: "terminate", disabled: false });
}
break;
}
});
};
/** /**
* Register navigation events and handler. * Register navigation events and handler.
*/ */
@@ -581,37 +791,35 @@ const navigationEvents = async () => {
return; return;
} }
// Handle navigation blur events // Handle input blur events
let ignoreBlur = false; let selected = false;
WEBVIEW.navigation.webContents.on("blur", () => { ipcMain.on("input-blur", (e, input) => {
const visibility = hardware.getKeyboardVisibility(); switch (input.id) {
if (visibility === "ON" && !ignoreBlur) { case "url":
WEBVIEW.window.restore(); if (selected) {
WEBVIEW.window.unmaximize(); hardware.setKeyboardVisibility("OFF", () => {
WEBVIEW.window.setFullScreen(true); selected = false;
hardware.setKeyboardVisibility("OFF"); WEBVIEW.navigation.webContents.send("input-select", { id: "url", select: selected });
WEBVIEW.navigation.webContents.send("input-blur", { id: "url" }); WEBVIEW.navigation.webContents.send("input-readonly", { id: "url", readonly: !selected });
});
}
break;
} }
}); });
// Handle input focus events // Handle input focus events
ipcMain.on("input-focus", (e, input) => { ipcMain.on("input-focus", (e, input) => {
const visibility = hardware.getKeyboardVisibility(); WEBVIEW.tracker.pointer.time = new Date();
switch (input.id) { switch (input.id) {
case "url": case "url":
if (visibility === "OFF") { hardware.setKeyboardVisibility("ON", () => {
ignoreBlur = true; setTimeout(() => {
WEBVIEW.window.restore(); selected = true;
WEBVIEW.window.setFullScreen(false); WEBVIEW.navigation.webContents.focus();
WEBVIEW.window.maximize(); WEBVIEW.navigation.webContents.send("input-select", { id: "url", select: selected });
hardware.setKeyboardVisibility("ON", () => { WEBVIEW.navigation.webContents.send("input-readonly", { id: "url", readonly: !selected });
setTimeout(() => { }, 400);
WEBVIEW.navigation.webContents.focus(); });
WEBVIEW.navigation.webContents.send("input-select", { id: "url" });
ignoreBlur = false;
}, 500);
});
}
break; break;
} }
}); });
@@ -634,39 +842,31 @@ 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) => {
const view = WEBVIEW.views[WEBVIEW.viewActive]; WEBVIEW.tracker.pointer.time = new Date();
switch (button.id) { switch (button.id) {
case "home": case "home":
homeView(); homeView();
hardware.setKeyboardVisibility("OFF");
break; break;
case "refresh": case "refresh":
reloadView(); reloadView();
hardware.setKeyboardVisibility("OFF");
break; break;
case "previous": case "previous":
previousView(); previousView();
hardware.setKeyboardVisibility("OFF");
break; break;
case "next": case "next":
nextView(); nextView();
hardware.setKeyboardVisibility("OFF");
break; break;
case "minus": case "minus":
zoomMinus(); zoomMinus();
hardware.setKeyboardVisibility("OFF");
break; break;
case "plus": case "plus":
zoomPlus(); zoomPlus();
hardware.setKeyboardVisibility("OFF");
break; break;
case "backward": case "backward":
historyBackward(); historyBackward();
hardware.setKeyboardVisibility("OFF");
break; break;
case "forward": case "forward":
historyForward(); historyForward();
hardware.setKeyboardVisibility("OFF");
break; break;
} }
}); });
@@ -705,7 +905,7 @@ const viewEvents = async () => {
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) { if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
nextView(); nextView();
} }
if (ARGS.app_debug === "true") { if ("app_debug" in ARGS) {
setTimeout(() => { setTimeout(() => {
view.webContents.openDevTools(); view.webContents.openDevTools();
}, 2000); }, 2000);
@@ -718,7 +918,14 @@ const viewEvents = async () => {
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) { if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
nextView(); nextView();
} }
view.webContents.loadURL(errorHtml(code, text, url)); switch (code) {
case -3:
console.warn(`Load Warning: ${url}, ERR_ABORTED (${code})`);
break;
default:
console.error(`Load Error: ${url}, ${text} (${code})`);
view.webContents.loadURL(errorHtml(code, text, url, WEBVIEW.theme.get()));
}
} }
}); });
@@ -749,7 +956,7 @@ const viewEvents = async () => {
WEBVIEW.tracker.pointer.time = now; WEBVIEW.tracker.pointer.time = now;
WEBVIEW.tracker.pointer.position = posNew; WEBVIEW.tracker.pointer.position = posNew;
if (delta > 30) { if (delta > 30) {
console.log("Update Last Active"); console.info("Update Last Active");
integration.update(); integration.update();
} }
} }
@@ -759,7 +966,7 @@ const viewEvents = async () => {
case "left": case "left":
// Ignore touch event if display was off // Ignore touch event if display was off
if (WEBVIEW.tracker.display.off > WEBVIEW.tracker.display.on) { if (WEBVIEW.tracker.display.off > WEBVIEW.tracker.display.on) {
console.log("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
@@ -784,6 +991,33 @@ const viewEvents = async () => {
* Register app events and handler. * Register app events and handler.
*/ */
const appEvents = async () => { const appEvents = async () => {
// Handle global events
EVENTS.on("reloadView", reloadView);
EVENTS.on("updateView", updateView);
EVENTS.on("updateDisplay", () => {
const status = hardware.getDisplayStatus();
if (status) {
WEBVIEW.tracker.display[status.toLowerCase()] = new Date();
}
});
EVENTS.on("updateStatus", () => {
const status = WEBVIEW.tracker.window.status;
const visibility = hardware.getKeyboardVisibility();
if (visibility === "ON" && ["Fullscreen", "Minimized"].includes(status)) {
hardware.setKeyboardVisibility("OFF");
}
toggleStatus(["Framed", "Minimized"].includes(status) ? "ON" : "OFF");
});
EVENTS.on("updateKeyboard", () => {
const status = WEBVIEW.tracker.window.status;
const visibility = hardware.getKeyboardVisibility();
if (visibility === "ON" && ["Fullscreen"].includes(status)) {
WEBVIEW.window.setStatus("Maximized");
} else if (visibility === "OFF" && ["Maximized"].includes(status)) {
WEBVIEW.window.setStatus("Fullscreen");
}
});
// Handle multiple instances // Handle multiple instances
app.on("second-instance", () => { app.on("second-instance", () => {
if (WEBVIEW.window.isMinimized()) { if (WEBVIEW.window.isMinimized()) {
@@ -792,11 +1026,10 @@ const appEvents = async () => {
WEBVIEW.window.focus(); WEBVIEW.window.focus();
}); });
// Handle signal and exit events // Handle exit events
app.on("before-quit", () => { app.on("before-quit", () => {
WEBVIEW.tracker.status = "Terminated"; APP.exiting = true;
console.warn(`${APP.title} Terminated`); console.warn(`${APP.title} Terminated`);
integration.update();
}); });
process.on("SIGINT", app.quit); process.on("SIGINT", app.quit);
@@ -805,6 +1038,9 @@ const appEvents = async () => {
// Check for latest release infos (2h) // Check for latest release infos (2h)
setInterval(() => { setInterval(() => {
if (APP.exiting) {
return;
}
latestRelease(); latestRelease();
}, 7200 * 1000); }, 7200 * 1000);
await latestRelease(); await latestRelease();
@@ -815,17 +1051,18 @@ const appEvents = async () => {
*/ */
const latestRelease = async () => { const latestRelease = async () => {
try { try {
const response = await axios.get(`${APP.releases.url}/latest`, { timeout: 10000 }); const response = await axios.get(APP.releases.url, { timeout: 10000 });
const data = response ? response.data : null; const release = response?.data?.find((item) => {
if (!data || data.draft || data.prerelease) { return !item.draft && (!item.prerelease || "app_early" in ARGS);
return; });
if (release) {
APP.releases.latest = {
title: APP.title,
version: (release.tag_name || " ").replace(/^v/i, ""),
summary: release.body || " ",
url: release.html_url || " ",
};
} }
APP.releases.latest = {
title: APP.title,
version: (data.tag_name || data.name || " ").replace(/^v/i, ""),
summary: data.body || " ",
url: data.html_url || " ",
};
} catch (error) { } catch (error) {
console.warn("Github Error:", error.message); console.warn("Github Error:", error.message);
} }
@@ -917,9 +1154,14 @@ const loaderHtml = (size, speed, theme) => {
* @param {number} code - The error code of the response. * @param {number} code - The error code of the response.
* @param {string} text - The error text of the response. * @param {string} text - The error text of the response.
* @param {string} url - The url of the requested page. * @param {string} url - The url of the requested page.
* @param {string} theme - The theme used for text colors.
* @returns {string} A data string with the generated html. * @returns {string} A data string with the generated html.
*/ */
const errorHtml = (code, text, url) => { const errorHtml = (code, text, url, theme) => {
const color = {
dark: { icon: "#FFA500", text: "#E5E5E5", background: "#111111" },
light: { icon: "#FFA500", text: "#1A1A1A", background: "#FAFAFA" },
}[theme];
const html = ` const html = `
<html> <html>
<head> <head>
@@ -927,17 +1169,25 @@ const errorHtml = (code, text, url) => {
body { body {
display: flex; display: flex;
align-items: center; align-items: center;
text-align: center;
justify-content: center; justify-content: center;
font-family: sans-serif; font-family: sans-serif;
text-align: center; background-color: ${color.background};
} }
.icon { .icon {
margin: 0; margin: 0;
font-size: 5rem; font-size: 5rem;
color: orange; color: ${color.icon};
} }
.title { .title {
margin: 0; margin: 0;
color: ${color.text};
}
.url {
color: ${color.text};
}
.error {
color: ${color.text};
} }
</style> </style>
</head> </head>
@@ -945,8 +1195,8 @@ const errorHtml = (code, text, url) => {
<div> <div>
<p class="icon">&#9888;</p> <p class="icon">&#9888;</p>
<h1 class="title">Whoopsie!</h1> <h1 class="title">Whoopsie!</h1>
<p><strong>Loading:</strong> ${url}</p> <p class="url"><strong>Loading:</strong> ${url}</p>
<p><strong>Error:</strong> ${text} (${code})</p> <p class="error"><strong>Error:</strong> ${text} (${code})</p>
</div> </div>
</body> </body>
</html>`; </html>`;
+7 -7
View File
@@ -4,22 +4,22 @@
"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.3.3", "version": "1.4.0",
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "electron . --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app --app-debug=false --trace-warnings --ignore-certificate-errors", "start": "electron . --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app --trace-warnings",
"build": "electron-forge make" "build": "electron-forge make"
}, },
"prettier": { "prettier": {
"printWidth": 120 "printWidth": 120
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.9.0", "@electron-forge/cli": "^7.10.0",
"@electron-forge/maker-deb": "^7.9.0", "@electron-forge/maker-deb": "^7.10.0",
"@electron-forge/maker-zip": "^7.9.0", "@electron-forge/maker-zip": "^7.10.0",
"@electron-forge/publisher-github": "^7.9.0", "@electron-forge/publisher-github": "^7.10.0",
"electron": "^38.1.2", "electron": "^38.2.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"prettier": "^3.6.2" "prettier": "^3.6.2"
}, },
+166 -165
View File
@@ -2,19 +2,19 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/runtime@^7.28.3": "@babel/runtime@^7.28.4":
version "7.28.4" version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
"@electron-forge/cli@^7.9.0": "@electron-forge/cli@^7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/cli/-/cli-7.9.0.tgz#de79184bbcdceaa5a8beb35c27bf15f4ba022ab7" resolved "https://registry.yarnpkg.com/@electron-forge/cli/-/cli-7.10.0.tgz#66edfe8bcea7d875d1944d6773d07eda2278eab3"
integrity sha512-FIs9rV6gN1v8rY73j/dvhQWF8tht035bN2LatY/z99H2KpWr0lhD03MOkIM2lRgrPEqVTExA4JEl9ja9yXBD3Q== integrity sha512-48lBvgQjkhj/S/TwbtvAauD0oSDrtIAqYMUZ3gyW1TgiZEMmBk4E56zbpjQ7AU0krGE7NUiLj8i8hvFpEJSljw==
dependencies: dependencies:
"@electron-forge/core" "7.9.0" "@electron-forge/core" "7.10.0"
"@electron-forge/core-utils" "7.9.0" "@electron-forge/core-utils" "7.10.0"
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@electron/get" "^3.0.0" "@electron/get" "^3.0.0"
"@inquirer/prompts" "^6.0.1" "@inquirer/prompts" "^6.0.1"
"@listr2/prompt-adapter-inquirer" "^2.0.22" "@listr2/prompt-adapter-inquirer" "^2.0.22"
@@ -26,12 +26,12 @@
log-symbols "^4.0.0" log-symbols "^4.0.0"
semver "^7.2.1" semver "^7.2.1"
"@electron-forge/core-utils@7.9.0": "@electron-forge/core-utils@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/core-utils/-/core-utils-7.9.0.tgz#0c9fa40460182ad03e6eb392d72e7a65f3c04471" resolved "https://registry.yarnpkg.com/@electron-forge/core-utils/-/core-utils-7.10.0.tgz#e61e79721d5e63122a4bb480868167905a3e51f1"
integrity sha512-votOf1+20cUJim0smYZW7OylXrl89JlpuFTgFpn5WFpDfAV6C0iH9202K1sa33v9OQ989JnpWDZtQNQzQHHNtg== integrity sha512-9byO4RGuP2na3xHzzzbup3Q4jC3QyXIgmZadL4N+A2Q2QH/DIMp/rOIB/zytqV0pNsdbeDSMvee7nj4jO0PeZg==
dependencies: dependencies:
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@electron/rebuild" "^3.7.0" "@electron/rebuild" "^3.7.0"
"@malept/cross-spawn-promise" "^2.0.0" "@malept/cross-spawn-promise" "^2.0.0"
chalk "^4.0.0" chalk "^4.0.0"
@@ -39,24 +39,25 @@
find-up "^5.0.0" find-up "^5.0.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
log-symbols "^4.0.0" log-symbols "^4.0.0"
parse-author "^2.0.0"
semver "^7.2.1" semver "^7.2.1"
"@electron-forge/core@7.9.0": "@electron-forge/core@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/core/-/core-7.9.0.tgz#96e7b0990cc661b2344648be8108c45a815a910c" resolved "https://registry.yarnpkg.com/@electron-forge/core/-/core-7.10.0.tgz#dbc06244f16cd1429358783fc7aaf1b873d010c1"
integrity sha512-S5uFv9sFAVYSO80/K4HZsJL9L2Bs51IxCqR0a2Lk4NdKr9Fpzp6txwuALSJzM1bIpohtqbjmgut49o95wg30ZA== integrity sha512-jM4HZWCyJeHpqh4Oc0gjPwMj7/BpO10Dex9L3RLUrR/pxsf5RrfmNOYbezTr1LlH1E4cTWGg5ziG23XMhZDjnA==
dependencies: dependencies:
"@electron-forge/core-utils" "7.9.0" "@electron-forge/core-utils" "7.10.0"
"@electron-forge/maker-base" "7.9.0" "@electron-forge/maker-base" "7.10.0"
"@electron-forge/plugin-base" "7.9.0" "@electron-forge/plugin-base" "7.10.0"
"@electron-forge/publisher-base" "7.9.0" "@electron-forge/publisher-base" "7.10.0"
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@electron-forge/template-base" "7.9.0" "@electron-forge/template-base" "7.10.0"
"@electron-forge/template-vite" "7.9.0" "@electron-forge/template-vite" "7.10.0"
"@electron-forge/template-vite-typescript" "7.9.0" "@electron-forge/template-vite-typescript" "7.10.0"
"@electron-forge/template-webpack" "7.9.0" "@electron-forge/template-webpack" "7.10.0"
"@electron-forge/template-webpack-typescript" "7.9.0" "@electron-forge/template-webpack-typescript" "7.10.0"
"@electron-forge/tracer" "7.9.0" "@electron-forge/tracer" "7.10.0"
"@electron/get" "^3.0.0" "@electron/get" "^3.0.0"
"@electron/packager" "^18.3.5" "@electron/packager" "^18.3.5"
"@electron/rebuild" "^3.7.0" "@electron/rebuild" "^3.7.0"
@@ -81,57 +82,57 @@
source-map-support "^0.5.13" source-map-support "^0.5.13"
username "^5.1.0" username "^5.1.0"
"@electron-forge/maker-base@7.9.0": "@electron-forge/maker-base@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-base/-/maker-base-7.9.0.tgz#0bcdac4573f88ba493beb2c42b382fc6f010f095" resolved "https://registry.yarnpkg.com/@electron-forge/maker-base/-/maker-base-7.10.0.tgz#0c30b963f6502df1a74aa0f46fa5ab1afe56403d"
integrity sha512-u0jo2kaYRxh/Rai6DyFSGJcNLRVWxiKaGUjMhX1LrKolufUkCxxR4TEmv4Hvl03WTr9pQb06umlIrVMaNb+j9A== integrity sha512-ydpwyCsNXS4xDknf5Aas7j2CyUmdGIBJRr760VemM11ts55zu3HOndvLCfsBSCBq4wpOIa6plQ4CaQQn15SrgQ==
dependencies: dependencies:
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
which "^2.0.2" which "^2.0.2"
"@electron-forge/maker-deb@^7.9.0": "@electron-forge/maker-deb@^7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-deb/-/maker-deb-7.9.0.tgz#6309d8e6b78010a32a55f2eb51a5e952f233b7e3" resolved "https://registry.yarnpkg.com/@electron-forge/maker-deb/-/maker-deb-7.10.0.tgz#79b62e16b2d4e929116972301481d296493b3a5c"
integrity sha512-b1TRMKkeuZ2PTKVsTMWLY+82O6UQVPQ17x5hkWe0lyIxRyCfJr6hD+AqYbvEJ50ncDkvyCtENBT6GnhfbKOGxA== integrity sha512-RlzhL/NtbYqphBuNWINwCR2kuCIE0RpYvkqa1ynDdu0n/OOiR/E1dpT0T/ZzSoyTIAZLYIcX3cHXwObzvV9bHQ==
dependencies: dependencies:
"@electron-forge/maker-base" "7.9.0" "@electron-forge/maker-base" "7.10.0"
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
optionalDependencies: optionalDependencies:
electron-installer-debian "^3.2.0" electron-installer-debian "^3.2.0"
"@electron-forge/maker-zip@^7.9.0": "@electron-forge/maker-zip@^7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/maker-zip/-/maker-zip-7.9.0.tgz#bf198f8b6316649f33f83d77ae539c904136636f" resolved "https://registry.yarnpkg.com/@electron-forge/maker-zip/-/maker-zip-7.10.0.tgz#0a2a031ce2796b57aeb1fb1f0d013a724ea79e0b"
integrity sha512-UGeziReiz8yuDTjliOjvbdyulIHpKAWkDeW3kOcMTUmRcCgrCkBNr+Pp6ih8Q3aBhG+CCd4++oe2rDnnuVvxFw== integrity sha512-e+NpgbEGo0xJzWxEvr2bewg/khTIVTdL75YFH2mayURKj1wia+bI8ukFdJvga5kkr842loUm38uW7dALYN3jQQ==
dependencies: dependencies:
"@electron-forge/maker-base" "7.9.0" "@electron-forge/maker-base" "7.10.0"
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
cross-zip "^4.0.0" cross-zip "^4.0.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
got "^11.8.5" got "^11.8.5"
"@electron-forge/plugin-base@7.9.0": "@electron-forge/plugin-base@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/plugin-base/-/plugin-base-7.9.0.tgz#a39f0e0a9e406686f298e0595ee514091e6144f9" resolved "https://registry.yarnpkg.com/@electron-forge/plugin-base/-/plugin-base-7.10.0.tgz#5588b2a1b78bb0e446affdab15d4255be6a6e3ec"
integrity sha512-2cnShgfes0sqH7A3+54fWhfJEfU++1OC2HE50a4sWtWEDwyWLGbwW7tp9BgSXrvIexO2AGKHQ1pKIjpZYVC0fA== integrity sha512-WxIwQZVhvCNHXePFvzE79OBKw7OrLzzxk4bb/PVnAyiDSHGdUyd35EzH5TAYENUACdM96B+nlIIU1PqTbPx63Q==
dependencies: dependencies:
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@electron-forge/publisher-base@7.9.0": "@electron-forge/publisher-base@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/publisher-base/-/publisher-base-7.9.0.tgz#f129cc24d916c27adbee35f5cdf868172b5cc263" resolved "https://registry.yarnpkg.com/@electron-forge/publisher-base/-/publisher-base-7.10.0.tgz#06207d8bc018ea1567241cad96b7bceb13375dfa"
integrity sha512-z3eH4+C++LiDqlKmri04IbSNNWLRYLvc49uNiOgfvnejplLsos720TTbwgruyI3D2fbpmhrVmz9kp9H9YzXdVg== integrity sha512-YAWnpTgzrf3xnjjk6pUwNfYAEMZ6YuOASEfPTKCKRo0MUpcUVvP1kB/LYlwI0LItyrGoTk0cfAw5fN/kFlB1Ng==
dependencies: dependencies:
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@electron-forge/publisher-github@^7.9.0": "@electron-forge/publisher-github@^7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/publisher-github/-/publisher-github-7.9.0.tgz#759f44a7cbc8c7fcd524ccc5cc57a99f9ad3a4fc" resolved "https://registry.yarnpkg.com/@electron-forge/publisher-github/-/publisher-github-7.10.0.tgz#6436438abb2bc3a4ff8ca45ec144025c336de516"
integrity sha512-kJzRLRaYmRa9D0e/PCnnhDChWhs3nipYgx3b4QFDVUFmbQ1ytBCmZnCO2vH0NXr7HZE3ozcAHm+Zw0TvEpXmeA== integrity sha512-ORiEcpPxSHOqu7oR8HlEnwLrdoi/F+w/RHBBiCsCS9EXiOFsVD4g0qY7UIYL8EKSpAQ1mBd81TFoleWxoL0YPA==
dependencies: dependencies:
"@electron-forge/publisher-base" "7.9.0" "@electron-forge/publisher-base" "7.10.0"
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@octokit/core" "^5.2.1" "@octokit/core" "^5.2.1"
"@octokit/plugin-retry" "^6.1.0" "@octokit/plugin-retry" "^6.1.0"
"@octokit/request-error" "^5.1.1" "@octokit/request-error" "^5.1.1"
@@ -143,69 +144,69 @@
log-symbols "^4.0.0" log-symbols "^4.0.0"
mime-types "^2.1.25" mime-types "^2.1.25"
"@electron-forge/shared-types@7.9.0": "@electron-forge/shared-types@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/shared-types/-/shared-types-7.9.0.tgz#ac3dd22da586957fcd6fb1c035155bd65bf2ffd3" resolved "https://registry.yarnpkg.com/@electron-forge/shared-types/-/shared-types-7.10.0.tgz#61204880a895938cd505d947838e06c598ab8580"
integrity sha512-6jZF+zq3SYMnweQpgr5fwlSgOd2yOZ5qlfz/CgXyVljiv0e0UThzpOjfTLuwuVgZX7a60xV+h0mg1h82Glu3wQ== integrity sha512-FpLBZ1kdIQjRmo09dD/zwDAARGLTzRB2Ja4eplUx5c8ctoblCnvC+KEOYA7R5DaRpKChaW0lqpDRF3ou2W+hHA==
dependencies: dependencies:
"@electron-forge/tracer" "7.9.0" "@electron-forge/tracer" "7.10.0"
"@electron/packager" "^18.3.5" "@electron/packager" "^18.3.5"
"@electron/rebuild" "^3.7.0" "@electron/rebuild" "^3.7.0"
listr2 "^7.0.2" listr2 "^7.0.2"
"@electron-forge/template-base@7.9.0": "@electron-forge/template-base@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-base/-/template-base-7.9.0.tgz#1716913b7485f61a8a1ec73132b9e91c7cfb1ea4" resolved "https://registry.yarnpkg.com/@electron-forge/template-base/-/template-base-7.10.0.tgz#7739164ea9266868f6e04f22e0a74a65df4a1283"
integrity sha512-DvXdJHh4qP+LBX6xNBlO0nfljvJNTmiQNfLRfSouRdYCWbr5hC7wyWAX803HqwRsVLRZj2oiLYZjmyG3jESp8Q== integrity sha512-wdQc17WwBS2RR97Nmjfuo4dqopMKmwDhqVmZd3VgKrR+5iQyxupjND+nf9VMzXd8exoyZzot06hj/ZQ/BAkJOA==
dependencies: dependencies:
"@electron-forge/core-utils" "7.9.0" "@electron-forge/core-utils" "7.10.0"
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@malept/cross-spawn-promise" "^2.0.0" "@malept/cross-spawn-promise" "^2.0.0"
debug "^4.3.1" debug "^4.3.1"
fs-extra "^10.0.0" fs-extra "^10.0.0"
semver "^7.2.1" semver "^7.2.1"
username "^5.1.0" username "^5.1.0"
"@electron-forge/template-vite-typescript@7.9.0": "@electron-forge/template-vite-typescript@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.9.0.tgz#1c5cedc3392e1dc2fdec2eec4549b0dea06d1d27" resolved "https://registry.yarnpkg.com/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.10.0.tgz#c6a6573e0f08abfc3553629b5ab7b1d70a4d3c76"
integrity sha512-qGK649YnC3iOVrj/hHUu/TXG7Nn/a7QykEOPrCrxROWL7mtw8CMUE+FOVETd+eew7/4ldUNzl+5b9ebP8jHOmg== integrity sha512-OtLNgq2/DUV6G02ERqhJ/RPmd3QgcRiM/KQd0dIkQMtvJ+X8TBgD/WKvr0iVtlrsPH7Y4DQtGJHx1a25tofAIA==
dependencies: dependencies:
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@electron-forge/template-base" "7.9.0" "@electron-forge/template-base" "7.10.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
"@electron-forge/template-vite@7.9.0": "@electron-forge/template-vite@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-vite/-/template-vite-7.9.0.tgz#0233840a305280efbecf8d8a4930a6f031594895" resolved "https://registry.yarnpkg.com/@electron-forge/template-vite/-/template-vite-7.10.0.tgz#437e49427e46a996ebbe68e4d2902bd344b23602"
integrity sha512-m8Fi40XaF7YDJo7YqmfJbkjRRsttN9EEHkmq+McLrDUbln15Ppm59RAwUhizDQzs4enmLcNQPaFllbAg/2t8vQ== integrity sha512-inSwFTo8zULrFsyXk9fLjNc6eK4xiHUj3GBaKwGt6emJzErO2mXXrUB8C6eTfWhVSDXtCHnjadkRSyNxAfgMcw==
dependencies: dependencies:
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@electron-forge/template-base" "7.9.0" "@electron-forge/template-base" "7.10.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
"@electron-forge/template-webpack-typescript@7.9.0": "@electron-forge/template-webpack-typescript@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.9.0.tgz#ea4d31626d7790960857d119a3f1687bcbb69ada" resolved "https://registry.yarnpkg.com/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.10.0.tgz#1357bedd4279cfaaa9bdccabc6400767cf3baf0c"
integrity sha512-z7PB72sI11LBSPz9w9nwwk0le/lZ3VT8TrLc0NYh4F4LbZ19Z+lxcraaAdf21efvWzodhWx8khAyA8WvkNxVSw== integrity sha512-9cs371U7ktuRkQUWQPRBM02nRjAVVBWAiEGYkF8eeDiDoJkHfYk1jgsmZkJufasONhNvNbX41VlxfCPJ2iDf+g==
dependencies: dependencies:
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@electron-forge/template-base" "7.9.0" "@electron-forge/template-base" "7.10.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
"@electron-forge/template-webpack@7.9.0": "@electron-forge/template-webpack@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/template-webpack/-/template-webpack-7.9.0.tgz#2e48610cf28508c13f5b7adb64e743590df267bc" resolved "https://registry.yarnpkg.com/@electron-forge/template-webpack/-/template-webpack-7.10.0.tgz#10b68d8598b1540ca8dfb5ef8d37f1be0c183e9f"
integrity sha512-ser22QczYGor7N6bnMUwexdWbLMNApr6zpBoT+Xfn6P4Ks67ttzcfkAges7GXHNjXwox4ePYZ3+D7cWQem5C4g== integrity sha512-sOyYDoRjxMq6g+eoy9JZsMXc8vMlY+Qc0vkWtgzYfsVbuVnWU0ktVfeHeeALxb9iwWB/dXupn6XITExChsnzzg==
dependencies: dependencies:
"@electron-forge/shared-types" "7.9.0" "@electron-forge/shared-types" "7.10.0"
"@electron-forge/template-base" "7.9.0" "@electron-forge/template-base" "7.10.0"
fs-extra "^10.0.0" fs-extra "^10.0.0"
"@electron-forge/tracer@7.9.0": "@electron-forge/tracer@7.10.0":
version "7.9.0" version "7.10.0"
resolved "https://registry.yarnpkg.com/@electron-forge/tracer/-/tracer-7.9.0.tgz#a512ea5e0c4564ace978546b50e8ec1aa150fa68" resolved "https://registry.yarnpkg.com/@electron-forge/tracer/-/tracer-7.10.0.tgz#a1b7ec3f9d629ed14526613093ab331f5733760d"
integrity sha512-7itsjW1WJQADg7Ly61ggI5CCRt+QDVx3HOZC1w69jMUtnipKyPRCbvTBf1oplsNqbIzZxceXdfex6W53YNehvA== integrity sha512-Qoj8bFBcwH3etG3YwJXHVeauKO3zyiZEelXMXgOp/du2QA23fc2+Ar7mzxcjY5fWDScd/8CN7M3+LxpovcKIDA==
dependencies: dependencies:
chrome-trace-event "^1.0.3" chrome-trace-event "^1.0.3"
@@ -733,16 +734,16 @@
"@types/node" "*" "@types/node" "*"
"@types/node@*": "@types/node@*":
version "24.5.2" version "24.7.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.5.2.tgz#52ceb83f50fe0fcfdfbd2a9fab6db2e9e7ef6446" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.7.0.tgz#a34c9f0d3401db396782e440317dd5d8373c286f"
integrity sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ== integrity sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==
dependencies: dependencies:
undici-types "~7.12.0" undici-types "~7.14.0"
"@types/node@^22.5.5", "@types/node@^22.7.7": "@types/node@^22.5.5", "@types/node@^22.7.7":
version "22.18.6" version "22.18.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.18.6.tgz#38172ef0b65e09d1a4fc715eb09a7d5decfdc748" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.18.8.tgz#738d9dafa38f6e0c467687c158f8e1ca2d7d8eaa"
integrity sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ== integrity sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==
dependencies: dependencies:
undici-types "~6.21.0" undici-types "~6.21.0"
@@ -954,15 +955,15 @@ braces@^3.0.3:
dependencies: dependencies:
fill-range "^7.1.1" fill-range "^7.1.1"
broker-factory@^3.1.9: broker-factory@^3.1.10:
version "3.1.9" version "3.1.10"
resolved "https://registry.yarnpkg.com/broker-factory/-/broker-factory-3.1.9.tgz#4fcc0ae5f62595076dccda868b7500e63071ef5a" resolved "https://registry.yarnpkg.com/broker-factory/-/broker-factory-3.1.10.tgz#583005d7bca34c32d828dfcadc51badf5d9a376b"
integrity sha512-MzvndyD6EcbkBtX4NXm/HfdO1+cOR5ONNdMCXEKfHpxGdMtuDz7+o+nJf7HMtyPH1sUVf/lEIP+DMluC5PgaBQ== integrity sha512-BzqK5GYFhvVFvO13uzPN0SCiOsOQuhMUbsGvTXDJMA2/N4GvIlFdxEuueE+60Zk841bBU5G3+fl2cqYEo0wgGg==
dependencies: dependencies:
"@babel/runtime" "^7.28.3" "@babel/runtime" "^7.28.4"
fast-unique-numbers "^9.0.23" fast-unique-numbers "^9.0.24"
tslib "^2.8.1" tslib "^2.8.1"
worker-factory "^7.0.45" worker-factory "^7.0.46"
buffer-crc32@~0.2.3: buffer-crc32@~0.2.3:
version "0.2.13" version "0.2.13"
@@ -1291,9 +1292,9 @@ deprecation@^2.0.0:
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
detect-libc@^2.0.1: detect-libc@^2.0.1:
version "2.1.0" version "2.1.2"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.0.tgz#3ca811f60a7b504b0480e5008adacc660b0b8c4f" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
integrity sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg== integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
detect-node@^2.0.4: detect-node@^2.0.4:
version "2.1.0" version "2.1.0"
@@ -1358,10 +1359,10 @@ electron-log@^5.4.3:
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.4.3.tgz#02a90baf4256950ca416095db6e5745268584d20" resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.4.3.tgz#02a90baf4256950ca416095db6e5745268584d20"
integrity sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ== integrity sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==
electron@^38.1.2: electron@^38.2.2:
version "38.1.2" version "38.2.2"
resolved "https://registry.yarnpkg.com/electron/-/electron-38.1.2.tgz#3509999a1b635fb92e99a8e5eaf239110a9a7734" resolved "https://registry.yarnpkg.com/electron/-/electron-38.2.2.tgz#117a516fa2215604e35a60f3cc2543f3f5404a16"
integrity sha512-WXUcN3W8h8NTTZViA3KNX0rV2YBU0X0mEUM3ubupXTDY4QtIN7tmiqYVOKSKpR2LckTmBWGuEeY4D6xVoffwKQ== integrity sha512-OXSaVNXDlonXDjMRsFNQo1j5tzTKwKXh5/m46IjAFccBcZJZMISI+EjSI07oexIuhvKM8AZLuFuihVn4YjWWrA==
dependencies: dependencies:
"@electron/get" "^2.0.0" "@electron/get" "^2.0.0"
"@types/node" "^22.7.7" "@types/node" "^22.7.7"
@@ -1519,12 +1520,12 @@ fast-glob@^3.2.7:
merge2 "^1.3.0" merge2 "^1.3.0"
micromatch "^4.0.8" micromatch "^4.0.8"
fast-unique-numbers@^9.0.23: fast-unique-numbers@^9.0.24:
version "9.0.23" version "9.0.24"
resolved "https://registry.yarnpkg.com/fast-unique-numbers/-/fast-unique-numbers-9.0.23.tgz#d6c260a1d1d2575ae20c5b524b2771b92f1e1eba" resolved "https://registry.yarnpkg.com/fast-unique-numbers/-/fast-unique-numbers-9.0.24.tgz#9f86bdbfa99b1b6546239202641196c78f81fd55"
integrity sha512-jcRIaHo46nfvyvKRMaFSKXmez4jALQ3Qw49gxM5F4siz8HqkyKPPEexpCOYwBSJI1HovrDr4fEedM8QAJ7oX3w== integrity sha512-Dv0BYn4waOWse94j16rsZ5w/0zoaCa74O3q6IZjMqaXbtT92Q+Sb6pPk+phGzD8Xh+nueQmSRI3tSCaHKidzKw==
dependencies: dependencies:
"@babel/runtime" "^7.28.3" "@babel/runtime" "^7.28.4"
tslib "^2.8.1" tslib "^2.8.1"
fastq@^1.6.0: fastq@^1.6.0:
@@ -2034,9 +2035,9 @@ isexe@^2.0.0:
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jiti@^2.4.2: jiti@^2.4.2:
version "2.5.1" version "2.6.1"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.5.1.tgz#bd099c1c2be1c59bbea4e5adcd127363446759d0" resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92"
integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w== integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
js-sdsl@4.3.0: js-sdsl@4.3.0:
version "4.3.0" version "4.3.0"
@@ -2416,9 +2417,9 @@ nice-try@^1.0.4:
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-abi@^3.45.0: node-abi@^3.45.0:
version "3.77.0" version "3.78.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.77.0.tgz#3ad90d5c9d45663420e5aa4ff58dbf4e3625419a" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.78.0.tgz#fd0ecbd0aa89857b98da06bd3909194abb0821ba"
integrity sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ== integrity sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==
dependencies: dependencies:
semver "^7.3.5" semver "^7.3.5"
@@ -2910,9 +2911,9 @@ semver@^6.2.0:
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5: semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5:
version "7.7.2" version "7.7.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
serialize-error@^7.0.1: serialize-error@^7.0.1:
version "7.0.1" version "7.0.1"
@@ -3213,10 +3214,10 @@ undici-types@~6.21.0:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
undici-types@~7.12.0: undici-types@~7.14.0:
version "7.12.0" version "7.14.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.12.0.tgz#15c5c7475c2a3ba30659529f5cdb4674b622fafb" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840"
integrity sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ== integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==
unique-filename@^2.0.0: unique-filename@^2.0.0:
version "2.0.1" version "2.0.1"
@@ -3307,44 +3308,44 @@ word-wrap@^1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
worker-factory@^7.0.45: worker-factory@^7.0.46:
version "7.0.45" version "7.0.46"
resolved "https://registry.yarnpkg.com/worker-factory/-/worker-factory-7.0.45.tgz#0e54ce238af035fceabb6f817b2cc7f4d3e576ee" resolved "https://registry.yarnpkg.com/worker-factory/-/worker-factory-7.0.46.tgz#452d18cabf629aaa5c1da78ecda42004da4a8558"
integrity sha512-FFPCiSv7MD6ZDEfiik/ErM8IrIAWajaXhezLyCo3v0FjhUWud6GXnG2BiTE91jLywXGAVCT8IF48Hhr+D/omMw== integrity sha512-Sr1hq2FMgNa04UVhYQacsw+i58BtMimzDb4+CqYphZ97OfefRpURu0UZ+JxMr/H36VVJBfuVkxTK7MytsanC3w==
dependencies: dependencies:
"@babel/runtime" "^7.28.3" "@babel/runtime" "^7.28.4"
fast-unique-numbers "^9.0.23" fast-unique-numbers "^9.0.24"
tslib "^2.8.1" tslib "^2.8.1"
worker-timers-broker@^8.0.10: worker-timers-broker@^8.0.11:
version "8.0.10" version "8.0.11"
resolved "https://registry.yarnpkg.com/worker-timers-broker/-/worker-timers-broker-8.0.10.tgz#db03254753ea3fd0a0e01552e2dbfed63a4a43f3" resolved "https://registry.yarnpkg.com/worker-timers-broker/-/worker-timers-broker-8.0.11.tgz#70fbb7eb299c53af895eff3edd21676888c6eb5e"
integrity sha512-xvo/9GiuduENbJNdWnvZtkriIkjBKKVbMyw7GXvrBu3n1JHemzZgxqaCcCBNlpfXnRXXF4ekqvXWLh1gb65b8w== integrity sha512-uwhxKru8BI9m2tsogxr2fB6POZ8LB2xH+Pu3R0mvQnAZLPgLD6K3IX4LNKPTEgTJ/j5VsuQPB+gLI1NBNKkPlg==
dependencies: dependencies:
"@babel/runtime" "^7.28.3" "@babel/runtime" "^7.28.4"
broker-factory "^3.1.9" broker-factory "^3.1.10"
fast-unique-numbers "^9.0.23" fast-unique-numbers "^9.0.24"
tslib "^2.8.1" tslib "^2.8.1"
worker-timers-worker "^9.0.10" worker-timers-worker "^9.0.11"
worker-timers-worker@^9.0.10: worker-timers-worker@^9.0.11:
version "9.0.10" version "9.0.11"
resolved "https://registry.yarnpkg.com/worker-timers-worker/-/worker-timers-worker-9.0.10.tgz#1955b3d93b7d10317ea9cad8fe7a6dd041a30b9a" resolved "https://registry.yarnpkg.com/worker-timers-worker/-/worker-timers-worker-9.0.11.tgz#ec479e0067f8aacc3e42524301e3a0d0d8b844d5"
integrity sha512-cfCmAkuoN+nGGJShta/g7CQVP3h7rvQA642EQg72fOHCWP5S2P83rLxDiaGv811Hd+19Cgdqt/tpRBIZ5kj/dw== integrity sha512-pArb5xtgHWImYpXhjg1OFv7JFG0ubmccb73TFoXHXjG830fFj+16N57q9YeBnZX52dn+itRrMoJZ9HaZBVzDaA==
dependencies: dependencies:
"@babel/runtime" "^7.28.3" "@babel/runtime" "^7.28.4"
tslib "^2.8.1" tslib "^2.8.1"
worker-factory "^7.0.45" worker-factory "^7.0.46"
worker-timers@^8.0.23: worker-timers@^8.0.23:
version "8.0.24" version "8.0.25"
resolved "https://registry.yarnpkg.com/worker-timers/-/worker-timers-8.0.24.tgz#4d0ed4d99c09cebd126cfff4945719bee6693004" resolved "https://registry.yarnpkg.com/worker-timers/-/worker-timers-8.0.25.tgz#261b1e9356fbf583c9debf28efa01a01982d6946"
integrity sha512-Ydu/7TRHlxIRjYSGDge1F92L7y9kzInpwR4CkocRVObPE0eRqC6d+0GFh52Hm+m520RHVKiytOERtCUu5sQDVQ== integrity sha512-X7Z5dmM6PlrEnaadtFQOyXHGD/IysPA3HZzaC2koqsU1VI+RvyGmjiiLiUBQixK8PH5R7ilkOzZupWskNRaXmA==
dependencies: dependencies:
"@babel/runtime" "^7.28.3" "@babel/runtime" "^7.28.4"
tslib "^2.8.1" tslib "^2.8.1"
worker-timers-broker "^8.0.10" worker-timers-broker "^8.0.11"
worker-timers-worker "^9.0.10" worker-timers-worker "^9.0.11"
wrap-ansi@^6.2.0: wrap-ansi@^6.2.0:
version "6.2.0" version "6.2.0"