30 Commits
Author SHA1 Message Date
leukipp 19348b4b60 build: disable zip update 2025-09-06 20:27:01 +02:00
leukipp ce1eed391d build: update version 2025-09-06 20:08:16 +02:00
leukipp fe6fe92656 docs: update readme 2025-09-06 20:07:41 +02:00
leukipp 3aea938cfb fix: validate arguments file #69 2025-09-06 20:03:49 +02:00
leukipp c39428fa42 build: update build.json 2025-09-06 19:48:02 +02:00
leukipp 7caa5bfc8a ref: update images 2025-09-06 19:22:19 +02:00
leukipp 5f69961a9f feat: mqtt app update #70 2025-09-06 19:21:51 +02:00
leukipp f44d998016 build: improve update logic 2025-09-06 19:07:57 +02:00
leukipp 3ea65a4af8 build: update dependencies 2025-09-06 19:04:16 +02:00
leukipp 2ad49c3167 feat: add network address mqtt sensor #66 2025-09-03 22:13:45 +02:00
leukipp ef2093a25e feat: check sudo rights for mqtt #62 2025-09-03 16:01:47 +02:00
leukipp 2737eabd8a fix: mqtt keyboard state #65 2025-09-01 17:22:26 +02:00
leukipp b8f809c50e build: update version 2025-08-31 22:04:56 +02:00
leukipp c3e625a8f6 docs: update readme 2025-08-31 22:01:18 +02:00
leukipp 7c15707e3b docs: add hardware 2025-08-31 21:06:57 +02:00
leukipp dbd3756793 ref: update images 2025-08-31 20:33:07 +02:00
leukipp 85fe5a910a fix: update service file #62 2025-08-31 20:31:38 +02:00
leukipp d78088e6af feat: side panel pager #46 2025-08-31 19:24:50 +02:00
leukipp bf77d9b2a0 fix: mqtt memory leak #63 2025-08-31 16:09:39 +02:00
leukipp f4ca7bc412 feat: basic keyboard navigation #46 2025-08-31 11:41:12 +02:00
leukipp d0ea377ead feat: add page zoom buttons #48 2025-08-31 09:24:30 +02:00
leukipp 9a829f72e3 build: update dependencies 2025-08-31 08:48:24 +02:00
leukipp 482a56c9b9 build: update dependencies 2025-08-27 20:03:26 +02:00
leukipp 32409983b2 fix: remember zoom level on refresh #52 2025-08-27 20:02:59 +02:00
leukipp fe0667919e feat: check online status #62 2025-08-27 18:30:15 +02:00
leukipp 0037a892cd ref: display status fallback 2025-08-27 18:26:40 +02:00
leukipp ad22dd3215 feat: kde wayland support #57 2025-08-26 17:15:21 +02:00
leukipp 619179c3c8 fix: event based communication #39 #48 #55 2025-08-25 18:52:25 +02:00
afsy-d 7e5e669b09 Added hardware information (#56)
* Update HARDWARE.md

Added CM4 light 2GB information

* Update HARDWARE.md

Updated information regarding CM4
2025-08-25 13:31:51 +02:00
Tom Carpenter 437c4ab1c5 Make Webview Zoom Changeable from Home Assistant (#52)
* Add Page Zoom Control

Adds MQTT discovery for a "Page Zoom" input to control the `WEBVIEW.viewZoom` parameter.

The MQTT value has been configured to be in % which is then converted to a scale factor when assigned or read from `WEBVIEW.viewZoom`.

* Correct Type for page_zoom publishConfig

Accidentally replaced "number" with "zoom" when changing paramters.

* Add updateZoom Functionality to webview

Add a setter function for the viewZoom property that updates the zoom factor for all views when the property is changed.
2025-08-25 13:31:28 +02:00
14 changed files with 1663 additions and 632 deletions
+1
View File
@@ -1,3 +1,4 @@
node_modules/ node_modules/
docs/ docs/
out/ out/
build.json
+16 -14
View File
@@ -27,20 +27,22 @@ If you are running Linux with a graphical user interface (Wayland or X11), you s
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Official 7" Touch Display 2 (720x1280)](https://www.raspberrypi.com/products/touch-display-2/) | 🟩 |
### 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 5" Touch Display (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 8" CAPLCD (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 |
| 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 7" 7EP-CAPLCD (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 |
| 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 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 | [GeeekPi 10.1" Capacitive Touch (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 | | 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 | [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 10.1" Capacitive Touch (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 | [Prechen Portable Touch 18.5" FHD (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 | [Akntzcs Portable Touch 16" HD (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
| Generic PC (x64) | Debian KDE (64-bit), X11 | Generic Non-Touch | 🟧 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [ELECROW Portable Monitor 10.1" (1280x800)](https://www.amazon.co.uk/dp/B0BHHQLKPY) | 🟨 |
| Generic PC (x64) | Ubuntu GNOME (64-bit), X11 | Generic Non-Touch | 🟧 | | Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | Generic Non-Touch | 🟨 |
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 | | Generic PC (x64) | Debian KDE (64-bit), Wayland, X11 | Generic Non-Touch | 🟧 |
| Generic PC (x64) | Ubuntu GNOME (64-bit), X11 | Generic Non-Touch | 🟧 |
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 |
## 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.
+49 -12
View File
@@ -43,7 +43,7 @@ For a comprehensive guide on setting up MQTT with Home Assistant, please refer t
## Installation ## Installation
On the first run of the application, you may encounter a **setup procedure** and the Home Assistant **login screen**. On the first run of the application, you may encounter a **setup procedure** and the Home Assistant **login screen**.
It's recommended to create a **dedicated user** (local access only) for your kiosk device. It's recommended to create a dedicated Home Assistant user (local access only) for your kiosk device.
You might also need a physical keyboard or remote VNC access to input these credentials once. You might also need a physical keyboard or remote VNC access to input these credentials once.
If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) you may be able to activate the on-screen keyboard using the side [widget](https://github.com/leukipp/touchkio/issues/16). If your hardware is [supported](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) you may be able to activate the on-screen keyboard using the side [widget](https://github.com/leukipp/touchkio/issues/16).
@@ -56,7 +56,6 @@ bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install
``` ```
Make sure that you run this with your **standard user** and not with root (sudo). Make sure that you run this with your **standard user** and not with root (sudo).
If you are paranoid, or smart, or both, have a look into the [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script before executing external code on your machine. If you are paranoid, or smart, or both, have a look into the [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script before executing external code on your machine.
This command can also be re-run to **update** an existing installation to the latest version.
The systemd **user service** is enabled by default and the kiosk application should start automatically the next time you boot. The systemd **user service** is enabled by default and the kiosk application should start automatically the next time you boot.
If you need manual control use: If you need manual control use:
@@ -89,6 +88,14 @@ yarn build
</div></details> </div></details>
#### Update
If you have already installed TouchKio and want to upgrade to the latest version, simply install the newer version over the existing one.
The [install.sh](https://github.com/leukipp/touchkio/blob/main/install.sh) script can also be run to update an existing installation to the **latest version**. The setup procedure can be skipped to use the existing default arguments from the configuration file:
```bash
bash <(wget -qO- https://raw.githubusercontent.com/leukipp/touchkio/main/install.sh) update
```
## Configuration ## Configuration
Running `touchkio --setup` will prompt you to enter arguments that will be used when the application starts without any specified arguments. Running `touchkio --setup` will prompt you to enter arguments that will be used when the application starts without any specified arguments.
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.
@@ -109,16 +116,16 @@ For example:
touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0 touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0
``` ```
> <a href="#ref1">↩ </a><a id="foot1"></a>**[1]:** Not necessarily a Home Assistant Url. > <a id="foot1"></a><a href="#ref1">[1]</a>: This doesn't necessarily have to be a Home Assistant Url.
> You can load 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://imvbh.github.io/FlipClock`. > `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": [
"https://demo.home-assistant.io", "https://demo.home-assistant.io",
"https://imvbh.github.io/FlipClock" "https://demo.immichkiosk.app"
] ]
} }
``` ```
@@ -140,6 +147,29 @@ For example:
touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883 --mqtt-user=kiosk --mqtt-password=password touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883 --mqtt-user=kiosk --mqtt-password=password
``` ```
## User Interface
TouchKio provides a simple webview window designed specifically for Touch Displays. Electron apps are known to be resource intensive due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
### Touch Controls
Additional controls can be found along the right edge of the kiosk application. For more details, you may want to have a closer look here:
| Name | Description |
| ------------ | -------------------------------------------------------- |
| `Widget` | [See #16](https://github.com/leukipp/touchkio/issues/16) |
| `Navigation` | [See #45](https://github.com/leukipp/touchkio/issues/45) |
| `Pager` | [See #64](https://github.com/leukipp/touchkio/issues/64) |
### Keyboard Shortcuts
The application also supports basic shortcuts to enhance navigation and usability for users who prefer or require non-touch input methods:
| Name | Description |
| --------------------------- | ---------------------------- |
| `Control+Left` | Navigate to previous page |
| `Control+Right` | Navigate to next page |
| `Control+Num_Subtract` | Decrease the page zoom |
| `Control+Num_Add` | Increase the page zoom |
| `Alt+Left`/`Mouse+Back` | Navigate backward in history |
| `Alt+Right`/`Mouse+Forward` | Navigate forward in history |
## Development ## Development
To create your own local build, you first need to install [Node.js](https://pimylifeup.com/raspberry-pi-nodejs) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install). To create your own local build, you first need to install [Node.js](https://pimylifeup.com/raspberry-pi-nodejs) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install).
@@ -226,8 +256,6 @@ This way, the first click only turns the screen on and focuses the window, allow
Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`. Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
This adjustment provides a user experience similar to that of a proper mobile device. This adjustment provides a user experience similar to that of a proper mobile device.
Electron apps are known to be **resource intensive** due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
</div></details> </div></details>
## Issues ## Issues
@@ -238,16 +266,25 @@ Please have a look into the [hardware](https://github.com/leukipp/touchkio/blob/
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported: **Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors. - If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
- On some Debian based systems (e.g. Ubuntu), the display status control may only be available when using X11. - 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.
- `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 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 (e.g. presence detection) to manage the screen status.
- When using a Raspberry Pi, the on-screen keyboard doesn't automatically pop-out 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](https://github.com/leukipp/touchkio/issues/4) 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.
- 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 run `touchkio` directly on the terminal to view the log outputs.
If you encounter any problems, please [create an issue](https://github.com/leukipp/touchkio/issues) and include the output of `touchkio --version` along with your system setup and any additional terminal logs.
## Credits ## Credits
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.
+35 -2
View File
@@ -1,4 +1,22 @@
const fs = require("fs"); const fs = require("fs");
const jszip = require("jszip");
const crypto = require("crypto");
const path = require("path");
const date = new Date().toISOString();
const generateBuildFile = (platform, arch, maker) => {
const package = fs.readFileSync(path.join(__dirname, "package.json"), "utf8");
const hash = crypto.createHash("sha256").update(package);
const data = {
id: hash.digest("hex").slice(-6),
platform: platform,
arch: arch,
maker: maker,
date: date,
};
return JSON.stringify(data, null, 2);
};
module.exports = { module.exports = {
packagerConfig: { packagerConfig: {
@@ -33,10 +51,25 @@ module.exports = {
}, },
], ],
hooks: { hooks: {
postPackage: async (config, results) => {
for (const outputPath of results.outputPaths) {
const [name, platform, arch] = path.basename(outputPath).split("-");
const buildFile = path.join(outputPath, "resources", "app", "build.json");
fs.writeFileSync(buildFile, generateBuildFile(platform, arch, "deb"), { encoding: "utf8" });
}
},
postMake: async (config, results) => { postMake: async (config, results) => {
for (const result of results) { for (const result of results) {
const artifacts = []; const artifacts = [];
result.artifacts.forEach((artifact) => { for (const artifact of result.artifacts) {
// Currently disabled for zip files as it seems to corrupt them
if (artifact.includes(".zip") && false) {
const [name, platform, arch] = path.basename(artifact).split("-");
const buildFile = path.join(`${name}-${platform}-${arch}`, "resources", "app", "build.json");
const zip = await jszip.loadAsync(fs.readFileSync(artifact, "utf8"));
zip.file(buildFile, generateBuildFile(platform, arch, "zip"));
fs.writeFileSync(artifact, await zip.generateAsync({ type: "nodebuffer" }));
}
if (artifact.includes("amd64")) { if (artifact.includes("amd64")) {
const renamed = artifact.replace("amd64", "x64"); const renamed = artifact.replace("amd64", "x64");
fs.renameSync(artifact, renamed); fs.renameSync(artifact, renamed);
@@ -44,7 +77,7 @@ module.exports = {
} else { } else {
artifacts.push(artifact); artifacts.push(artifact);
} }
}); }
result.artifacts = artifacts; result.artifacts = artifacts;
} }
}, },
+59 -6
View File
@@ -46,11 +46,19 @@
li.dynamic { li.dynamic {
flex: 1 1 0%; flex: 1 1 0%;
} }
li.separator {
height: 32px;
border: 1px solid var(--border-color);
}
li.separator:has(+ li.hidden) {
display: none;
}
li.hidden { li.hidden {
display: none; display: none;
} }
li.disabled { li.disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed;
pointer-events: none; pointer-events: none;
} }
button { button {
@@ -62,12 +70,13 @@
input { input {
width: 100%; width: 100%;
min-width: 0; min-width: 0;
margin: 0 10px; margin: 0 5px;
padding: 6px 10px; padding: 6px 10px;
border-radius: 4px; border-radius: 4px;
font-size: 18px; font-size: 18px;
outline: none; outline: none;
border: none; border: none;
cursor: text;
box-sizing: border-box; box-sizing: border-box;
color: var(--primary-color); color: var(--primary-color);
background-color: var(--border-color); background-color: var(--border-color);
@@ -91,7 +100,28 @@
</button> </button>
</li> </li>
<li> <li>
<button id="pager"> <button id="refresh">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>refresh-circle</title>
<path
d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2M18 11H13L14.81 9.19A3.94 3.94 0 0 0 12 8A4 4 0 1 0 15.86 13H17.91A6 6 0 1 1 12 6A5.91 5.91 0 0 1 16.22 7.78L18 6Z"
/>
</svg>
</button>
</li>
<li class="separator"></li>
<li>
<button id="previous">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>page-previous</title>
<path
d="M4,21H19A2,2 0 0,0 21,19V13H17V15L13,12L17,9V11H21V5A2,2 0 0,0 19,3H4A2,2 0 0,0 2,5V19A2,2 0 0,0 4,21M4,15H8V17H4V15M4,11H11V13H4V11M4,7H11V9H4V7M21,11H24V13H21V11Z"
/>
</svg>
</button>
</li>
<li>
<button id="next">
<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>page-next</title> <title>page-next</title>
<path <path
@@ -100,15 +130,17 @@
</svg> </svg>
</button> </button>
</li> </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" />
</li> </li>
<li class="separator"></li>
<li> <li>
<button id="backward"> <button id="backward">
<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>arrow-left-circle</title> <title>history</title>
<path <path
d="M2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12M18,11H10L13.5,7.5L12.08,6.08L6.16,12L12.08,17.92L13.5,16.5L10,13H18V11Z" d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3"
/> />
</svg> </svg>
</button> </button>
@@ -116,9 +148,30 @@
<li> <li>
<button id="forward"> <button id="forward">
<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>arrow-right-circle</title> <title>update</title>
<path <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,12M6,13H14L10.5,16.5L11.92,17.92L17.84,12L11.92,6.08L10.5,7.5L14,11H6V13Z" d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z"
/>
</svg>
</button>
</li>
<li class="separator"></li>
<li>
<button id="minus">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>magnify-minus</title>
<path
d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M5,8V10H13V8H5Z"
/>
</svg>
</button>
</li>
<li>
<button id="plus">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>magnify-plus</title>
<path
d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M8,5V8H5V10H8V13H10V10H13V8H10V5H8Z"
/> />
</svg> </svg>
</button> </button>
+82
View File
@@ -0,0 +1,82 @@
<!doctype html>
<html>
<head>
<style>
[data-theme="hidden"] {
--primary-color: rgba(0, 0, 0, 0);
--border-color: rgba(0, 0, 0, 0);
--background-color: rgba(0, 0, 0, 0);
}
::-webkit-scrollbar {
display: none;
}
* {
margin: 0;
padding: 0;
cursor: none;
}
body {
display: flex;
align-items: center;
width: 100vw;
height: 100vh;
}
menu {
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
height: 100vh;
list-style-type: none;
background: var(--background-color);
}
li {
display: flex;
align-items: center;
flex: 0 0 auto;
}
button {
width: 100vw;
height: 50vh;
border: none;
background-color: transparent;
}
#previous {
cursor: w-resize;
}
#next {
cursor: e-resize;
}
</style>
</head>
<body>
<menu>
<li>
<button id="previous"></button>
</li>
<li>
<button id="next"></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("data-theme", (e, data) => {
document.documentElement.setAttribute("data-theme", data.theme);
});
};
registerSendEvents();
registerReceiveEvents();
</script>
</body>
</html>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 78 KiB

+131 -52
View File
@@ -7,6 +7,7 @@ const hardware = require("./js/hardware");
const webview = require("./js/webview"); const webview = require("./js/webview");
const { app } = require("electron"); const { app } = require("electron");
global.APP = global.APP || {};
global.ARGS = global.ARGS || {}; global.ARGS = global.ARGS || {};
if (!process.env.DISPLAY) { if (!process.env.DISPLAY) {
@@ -23,20 +24,85 @@ if (!process.env.DISPLAY) {
* initialization tasks. * initialization tasks.
*/ */
app.whenReady().then(async () => { app.whenReady().then(async () => {
const lock = app.requestSingleInstanceLock(); if (!(await initApp()) || !(await initArgs())) {
if (!lock) { return;
console.error("TouchKio is already running"); }
// Show used arguments
console.log(`Arguments: ${JSON.stringify(ARGS, null, 2)}`);
// Chained init functions
const chained = [webview.init, hardware.init, integration.init];
for (const init of chained) {
if (!(await init())) {
break;
}
}
});
/**
* Initializes the global app object.
*
* @returns {bool} Returns true if the initialization was successful.
*/
const initApp = async () => {
const packageJsonPath = path.join(app.getAppPath(), "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const buildJsonPath = path.join(app.getAppPath(), "build.json");
const buildFileExists = fs.existsSync(buildJsonPath);
// Set required app infos
APP.name = app.getName();
APP.title = packageJson.title;
APP.version = app.getVersion();
APP.path = app.getAppPath();
APP.config = app.getPath("userData");
// Set additional update infos
APP.homepage = `https://github.com/${packageJson.author}/${packageJson.name}`;
APP.releases = {
url: `https://api.github.com/repos/${packageJson.author}/${packageJson.name}/releases`,
latest: null,
};
APP.scripts = {
install: `https://raw.githubusercontent.com/${packageJson.author}/${packageJson.name}/main/install.sh`,
};
// Set additional build infos
APP.build = {};
if (buildFileExists) {
APP.build = JSON.parse(fs.readFileSync(buildJsonPath, "utf8"));
}
// Request single app instance lock
if (!app.requestSingleInstanceLock()) {
console.error(`${APP.title} is already running`);
return app.quit(); return app.quit();
} }
return true;
};
/**
* Initializes the global args object.
*
* @returns {bool} Returns true if the initialization was successful.
*/
const initArgs = async () => {
let args = parseArgs(process); let args = parseArgs(process);
let argsProvided = !!Object.keys(args).length; let argsProvided = !!Object.keys(args).length;
let argsFilePath = path.join(app.getPath("userData"), "Arguments.json"); let argsFilePath = path.join(APP.config, "Arguments.json");
let argsFileExists = fs.existsSync(argsFilePath); let argsFileExists = fs.existsSync(argsFilePath);
// Show version and release info // Show version and release info
if ("help" in args || "version" in args) { if ("help" in args || "version" in args) {
console.log(`${app.getName()}-v${app.getVersion()}, https://github.com/leukipp/touchkio`); let build = "";
if (APP.build.id) {
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}`);
return app.quit(); return app.quit();
} }
@@ -51,24 +117,21 @@ app.whenReady().then(async () => {
args = readArgs(argsFilePath); args = readArgs(argsFilePath);
} }
// Check arguments object
if (!Object.keys(args).length) {
console.error(`No arguments provided`);
return app.quit();
}
// Split url arguments // Split url arguments
args.web_url = args.web_url || []; args.web_url = args.web_url || [];
if (!Array.isArray(args.web_url)) { if (!Array.isArray(args.web_url)) {
args.web_url = args.web_url.split(",").map((url) => url.trim()); args.web_url = args.web_url.split(",").map((url) => url.trim());
} }
global.ARGS = args; ARGS = args;
// Show used arguments return true;
console.log(`Arguments: ${JSON.stringify(global.ARGS, null, 2)}`); };
// Chained init functions
const chained = [webview.init, hardware.init, integration.init];
for (const init of chained) {
if (!(await init())) {
break;
}
}
});
/** /**
* Parses command-line arguments from the given process object. * Parses command-line arguments from the given process object.
@@ -155,34 +218,41 @@ const promptArgs = async (proc) => {
// Prompt questions and wait for the answers // Prompt questions and wait for the answers
let args = {}; let args = {};
let ignore = []; let ignore = [];
for (const { key, question, fallback } of prompts) { try {
if (key === "mqtt") { for (const { key, question, fallback } of prompts) {
const prompt = `${question} (${fallback}): `; if (key === "mqtt") {
const answer = await read.question(prompt); const prompt = `${question} (${fallback}): `;
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase(); const answer = await read.question(prompt);
if (!["y", "yes"].includes(value)) { const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
ignore = ignore.concat(["mqtt_url", "mqtt_user", "mqtt_password", "mqtt_discovery"]); if (!["y", "yes"].includes(value)) {
} ignore = ignore.concat(["mqtt_url", "mqtt_user", "mqtt_password", "mqtt_discovery"]);
} else if (key === "check") { }
const json = JSON.stringify(args, null, 2); } else if (key === "check") {
const prompt = `${question}\n${json}\n(${fallback}): `; const json = JSON.stringify(args, null, 2);
const answer = await read.question(prompt); const prompt = `${question}\n${json}\n(${fallback}): `;
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase(); const answer = await read.question(prompt);
if (!["y", "yes"].includes(value)) { const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
args = {}; if (!["y", "yes"].includes(value)) {
} args = {};
} else if (!ignore.includes(key)) { }
const prompt = `${question} (${fallback}): `; } else if (!ignore.includes(key)) {
const answer = await read.question(prompt); const prompt = `${question} (${fallback}): `;
const value = answer.trim() || fallback; const answer = await read.question(prompt);
if (key === "web_url") { const value = answer.trim() || fallback;
args[key] = value.split(",").map((v) => v.trim()); if (key === "web_url") {
} else { args[key] = value.split(",").map((v) => v.trim());
args[key] = value; } else {
args[key] = value;
}
} }
} }
} catch (error) {
console.error(`\n${error.message}`);
args = {};
app.quit();
} finally {
read.close();
} }
read.close();
return args; return args;
}; };
@@ -194,11 +264,15 @@ const promptArgs = async (proc) => {
* @param {Object} args - The arguments object. * @param {Object} args - The arguments object.
*/ */
const writeArgs = (path, args) => { const writeArgs = (path, args) => {
const argc = Object.assign({}, args); try {
if ("mqtt_password" in argc) { const argc = Object.assign({}, args);
argc.mqtt_password = encrypt(argc.mqtt_password); if ("mqtt_password" in argc) {
argc.mqtt_password = encrypt(argc.mqtt_password);
}
fs.writeFileSync(path, JSON.stringify(argc, null, 2));
} catch (error) {
console.error(`Failed to write ${path}:`, error.message);
} }
fs.writeFileSync(path, JSON.stringify(argc, null, 2));
}; };
/** /**
@@ -208,11 +282,16 @@ const writeArgs = (path, args) => {
* @returns {Object} The arguments object. * @returns {Object} The arguments object.
*/ */
const readArgs = (path) => { const readArgs = (path) => {
const args = JSON.parse(fs.readFileSync(path)); try {
if ("mqtt_password" in args) { const args = JSON.parse(fs.readFileSync(path, "utf8"));
args.mqtt_password = decrypt(args.mqtt_password); if ("mqtt_password" in args) {
args.mqtt_password = decrypt(args.mqtt_password);
}
return args;
} catch (error) {
console.error(`Failed to parse ${path}:`, error.message);
} }
return args; return {};
}; };
/** /**
@@ -223,7 +302,7 @@ const readArgs = (path) => {
*/ */
const encrypt = (value) => { const encrypt = (value) => {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const key = crypto.scryptSync(hardware.getMachineId(), app.getName(), 32); const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32);
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
let encrypted = cipher.update(value, "utf8", "hex"); let encrypted = cipher.update(value, "utf8", "hex");
encrypted += cipher.final("hex"); encrypted += cipher.final("hex");
@@ -239,7 +318,7 @@ const encrypt = (value) => {
const decrypt = (value) => { const decrypt = (value) => {
const p = Buffer.from(value, "base64").toString("utf8").split(":"); const p = Buffer.from(value, "base64").toString("utf8").split(":");
const iv = Buffer.from(p.shift(), "hex"); const iv = Buffer.from(p.shift(), "hex");
const key = crypto.scryptSync(hardware.getMachineId(), app.getName(), 32); const key = crypto.scryptSync(hardware.getMachineId(), APP.name, 32);
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
const buffer = Buffer.from(p.join(":"), "hex"); const buffer = Buffer.from(p.join(":"), "hex");
let decrypted = decipher.update(buffer, "binary", "utf8"); let decrypted = decipher.update(buffer, "binary", "utf8");
+54 -41
View File
@@ -1,5 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Read arguments
ARG_UPDATE=false
for arg in "$@"; do
if [ "$arg" = "update" ]; then
ARG_UPDATE=true
fi
done
# Determine system architecture # Determine system architecture
echo -e "Determining system architecture..." echo -e "Determining system architecture..."
@@ -12,62 +20,43 @@ case "$(uname -m)" in
ARCH="x64" ARCH="x64"
;; ;;
*) *)
echo "Architecture $(uname -m) running $BITS-bit operating system is not supported." { echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."; exit 1; }
exit 1
;; ;;
esac esac
if [ "$BITS" -ne 64 ]; then [ "$BITS" -eq 64 ] || { echo "Architecture $ARCH running $BITS-bit operating system is not supported."; exit 1; }
echo "Architecture $ARCH running $BITS-bit operating system is not supported."
exit 1
fi
echo "Architecture $ARCH running $BITS-bit operating system is supported." echo "Architecture $ARCH running $BITS-bit operating system is supported."
# Download the latest .deb package # Download the latest .deb package
echo -e "\nDownloading the latest release..." echo -e "\nDownloading the latest release..."
TMP_DIR=$(mktemp -d)
DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \ DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \
grep -o "\"browser_download_url\": \"[^\"]*_${ARCH}\.deb\"" | \ grep -o "\"browser_download_url\": \"[^\"]*_${ARCH}\.deb\"" | \
sed 's/"browser_download_url": "//;s/"//g') sed 's/"browser_download_url": "//;s/"//g')
TMP_DIR=$(mktemp -d)
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")" DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
chmod 755 "$TMP_DIR" chmod 755 "$TMP_DIR"
if [ -z "$DEB_URL" ]; then [ -z "$DEB_URL" ] && { echo "Download url for .deb file not found."; exit 1; }
echo "Download url for .deb file not found." wget --show-progress -q -O "$DEB_PATH" "$DEB_URL" || { echo "Failed to download the .deb file."; exit 1; }
exit 1
fi
if ! wget --show-progress -q -O "$DEB_PATH" "$DEB_URL"; then
echo "Failed to download the .deb file."
exit 1
fi
# Install the latest .deb package # Install the latest .deb package
echo -e "\nInstalling the latest release..." echo -e "\nInstalling the latest release..."
if ! command -v apt &> /dev/null; then command -v apt &> /dev/null || { echo "Package manager apt was not found."; exit 1; }
echo "Package manager apt was not found." sudo apt install -y "$DEB_PATH" || { echo "Installation of .deb file failed."; exit 1; }
exit 1
fi
if ! sudo apt install "$DEB_PATH"; then
echo "Installation of .deb file failed."
exit 1
fi
# Create the systemd user service # Create the systemd user service
echo -e "\nCreating systemd user service..." echo -e "\nCreating systemd user service..."
SERVICE_FILE="$HOME/.config/systemd/user/touchkio.service" SERVICE_NAME="touchkio.service"
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE"; exit 1; } SERVICE_FILE="$HOME/.config/systemd/user/$SERVICE_NAME"
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE."; exit 1; }
bash -c "cat << EOF > \"$SERVICE_FILE\" SERVICE_CONTENT="[Unit]
[Unit]
Description=TouchKio Description=TouchKio
After=graphical.target After=graphical.target
Wants=network-online.target
[Service] [Service]
ExecStart=/usr/bin/touchkio ExecStart=/usr/bin/touchkio
@@ -75,35 +64,59 @@ Restart=on-failure
RestartSec=5s RestartSec=5s
[Install] [Install]
WantedBy=default.target WantedBy=default.target"
EOF"
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE"; exit 1; } if $ARG_UPDATE; then
echo "Service $SERVICE_FILE enabled" if systemctl --user --quiet is-active "${SERVICE_NAME}"; then
systemctl --user restart "${SERVICE_NAME}"
echo "Existing $SERVICE_NAME restarted."
else
echo "Existing $SERVICE_NAME not running, start touchkio manually."
fi
exit 0
fi
SERVICE_CREATE=true
if [ -f "$SERVICE_FILE" ]; then
read -p "Service $SERVICE_FILE exists, overwrite? (y/N) " overwrite
[[ ${overwrite:-n} == [Yy]* ]] || SERVICE_CREATE=false
fi
if $SERVICE_CREATE; then
echo "$SERVICE_CONTENT" > "$SERVICE_FILE" || { echo "Failed to write to $SERVICE_FILE."; exit 1; }
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE."; exit 1; }
echo "Service $SERVICE_FILE enabled."
else
echo "Service $SERVICE_FILE not created."
fi
# Export display variables # Export display variables
echo -e "\nExporting display variables..." echo -e "\nExporting display variables..."
if [ -z "$DISPLAY" ]; then if [ -z "$DISPLAY" ]; then
export DISPLAY=":0" export DISPLAY=":0"
echo "DISPLAY was not set, defaulting to \"$DISPLAY\"" echo "DISPLAY was not set, defaulting to \"$DISPLAY\"."
else else
echo "DISPLAY is set to \"$DISPLAY\"" echo "DISPLAY is set to \"$DISPLAY\"."
fi fi
if [ -z "$WAYLAND_DISPLAY" ]; then if [ -z "$WAYLAND_DISPLAY" ]; then
export WAYLAND_DISPLAY="wayland-0" export WAYLAND_DISPLAY="wayland-0"
echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\"" echo "WAYLAND_DISPLAY was not set, defaulting to \"$WAYLAND_DISPLAY\"."
else else
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"" echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"."
fi fi
# Start the setup mode # Start the setup mode
echo "" echo ""
read -p "Start touchkio setup? (Y/n) " answer read -p "Start touchkio setup? (Y/n) " setup
if [[ ${answer:-y} == [Yy]* ]]; then
if [[ ${setup:-y} == [Yy]* ]]; then
echo "/usr/bin/touchkio --setup" echo "/usr/bin/touchkio --setup"
/usr/bin/touchkio --setup /usr/bin/touchkio --setup
else else
echo "/usr/bin/touchkio" echo "/usr/bin/touchkio"
/usr/bin/touchkio /usr/bin/touchkio
fi fi
exit 0
+253 -81
View File
@@ -3,10 +3,11 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const fsp = require("fs/promises"); const fsp = require("fs/promises");
const cpr = require("child_process"); const cpr = require("child_process");
const Events = require("events");
global.HARDWARE = global.HARDWARE || { global.HARDWARE = global.HARDWARE || {
initialized: false, initialized: false,
status: null, events: new Events(),
session: {}, session: {},
support: {}, support: {},
battery: { battery: {
@@ -16,19 +17,18 @@ global.HARDWARE = global.HARDWARE || {
}, },
display: { display: {
status: { status: {
path: null,
value: null, value: null,
path: null,
command: null,
}, },
brightness: { brightness: {
path: null,
value: null, value: null,
path: null,
max: null, max: null,
}, },
notifiers: [],
}, },
keyboard: { keyboard: {
visible: null, visible: null,
notifiers: [],
}, },
}; };
@@ -38,36 +38,36 @@ global.HARDWARE = global.HARDWARE || {
* @returns {bool} Returns true if the initialization was successful. * @returns {bool} Returns true if the initialization was successful.
*/ */
const init = async () => { const init = async () => {
if (!compatibleDevice()) { if (!compatibleSystem()) {
console.warn("Device not supported"); console.warn("Operating system is not supported\n");
return false; return false;
} }
// Init globals // Init globals
HARDWARE.session.user = sessionUser(); HARDWARE.session.user = sessionUser();
HARDWARE.session.type = sessionType(); HARDWARE.session.type = sessionType();
HARDWARE.session.desktop = sessionDesktop();
HARDWARE.battery.level.path = getBatteryLevelPath(); HARDWARE.battery.level.path = getBatteryLevelPath();
HARDWARE.support.batteryLevel = HARDWARE.battery.level.path !== null;
HARDWARE.display.status.path = getDisplayStatusPath(); HARDWARE.display.status.path = getDisplayStatusPath();
HARDWARE.support.displayStatus = HARDWARE.display.status.path !== null; HARDWARE.display.status.command = getDisplayStatusCommand();
HARDWARE.display.brightness.path = getDisplayBrightnessPath(); HARDWARE.display.brightness.path = getDisplayBrightnessPath();
HARDWARE.support.displayBrightness = HARDWARE.display.brightness.path !== null;
HARDWARE.display.brightness.max = getDisplayBrightnessMax(); HARDWARE.display.brightness.max = getDisplayBrightnessMax();
HARDWARE.support.keyboardVisibility = processRuns("squeekboard"); HARDWARE.support = checkSupport();
HARDWARE.initialized = true; HARDWARE.initialized = true;
HARDWARE.status = "Supported";
// Show supported features // Show supported features
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`); console.log(`\nSupported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
// Show session infos // Show session infos
console.log("\nUser:", HARDWARE.session.user); console.log("\nUser:", HARDWARE.session.user);
console.log("Session:", HARDWARE.session.type); console.log("Session:", HARDWARE.session.type);
console.log("Desktop:", HARDWARE.session.desktop);
// Show device infos // Show device infos
console.log("\nModel:", getModel()); console.log("\nModel:", getModel());
console.log("Vendor:", getVendor()); console.log("Vendor:", getVendor());
console.log("Serial Number:", getSerialNumber()); console.log("Serial Number:", getSerialNumber());
console.log("Network Addresses:", getNetworkAddresses());
console.log("Host Name:", getHostName()); console.log("Host Name:", getHostName());
// Show system infos // Show system infos
@@ -78,9 +78,23 @@ const init = async () => {
console.log("Processor Temperature:", getProcessorTemperature()); console.log("Processor Temperature:", getProcessorTemperature());
// Show hardware infos // Show hardware infos
console.log(`\nBattery Level [${HARDWARE.battery.level.path}]:`, getBatteryLevel()); const unsupported = "unsupported";
console.log(`Display Status [${HARDWARE.display.status.path}]:`, getDisplayStatus()); console.log(
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n"); `\nBattery Level [${HARDWARE.battery.level.path || unsupported}]:`,
`${getBatteryLevel() || unsupported}`,
);
console.log(
`Display Status [${HARDWARE.display.status.path || unsupported}]:`,
`${getDisplayStatus() || unsupported} (${HARDWARE.display.status.command || unsupported})`,
);
console.log(
`Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`,
`${getDisplayBrightness() || unsupported}`,
);
console.log(
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`,
`${getKeyboardVisibility() || unsupported}\n`,
);
// Check for keyboard visibility // Check for keyboard visibility
setKeyboardVisibility("OFF", (reply, error) => { setKeyboardVisibility("OFF", (reply, error) => {
@@ -92,9 +106,7 @@ const init = async () => {
return; return;
} }
HARDWARE.keyboard.visibility = property.Visible === "true"; HARDWARE.keyboard.visibility = property.Visible === "true";
HARDWARE.keyboard.notifiers.forEach((notifier) => { HARDWARE.events.emit("updateKeyboard");
notifier();
});
}); });
}); });
@@ -118,9 +130,7 @@ const update = async () => {
if (status !== HARDWARE.display.status.value) { if (status !== HARDWARE.display.status.value) {
console.log("Update Display Status:", getDisplayStatus()); console.log("Update Display Status:", getDisplayStatus());
HARDWARE.display.status.value = status; HARDWARE.display.status.value = status;
HARDWARE.display.notifiers.forEach((notifier) => { HARDWARE.events.emit("updateDisplay");
notifier();
});
} }
} }
@@ -130,23 +140,21 @@ const update = async () => {
if (brightness !== HARDWARE.display.brightness.value) { if (brightness !== HARDWARE.display.brightness.value) {
console.log("Update Display Brightness:", getDisplayBrightness()); console.log("Update Display Brightness:", getDisplayBrightness());
HARDWARE.display.brightness.value = brightness; HARDWARE.display.brightness.value = brightness;
HARDWARE.display.notifiers.forEach((notifier) => { HARDWARE.events.emit("updateDisplay");
notifier();
});
} }
} }
}; };
/** /**
* Verifies device compatibility by checking the presence of necessary sys paths. * Verifies system compatibility by checking the presence of necessary sys paths.
* *
* @returns {bool} Returns true if all paths exists. * @returns {bool} Returns true if all paths exists.
*/ */
const compatibleDevice = () => { const compatibleSystem = () => {
if (os.platform() !== "linux") { if (os.platform() !== "linux") {
return false; return false;
} }
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/thermal"]; const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/power_supply", "/sys/class/thermal"];
return paths.every((path) => fs.existsSync(path)); return paths.every((path) => fs.existsSync(path));
}; };
@@ -179,12 +187,37 @@ const sessionType = () => {
}; };
/** /**
* Checks the session type for the logged in user. * Gets the desktop environment name by checking environment variables.
* *
* @returns {bool} Returns true if the session matches the type. * @returns {string} Returns desktop environment name or 'unknown' if not detected.
*/ */
const session = (type) => { const sessionDesktop = () => {
return HARDWARE.session.type === type; const envs = ["XDG_CURRENT_DESKTOP", "XDG_DESKTOP_SESSION", "DESKTOP_SESSION"];
const names = envs.map((env) => process.env[env]).filter(Boolean);
return (names.join(":") || "unknown").toLowerCase();
};
/**
* Checks supported features based on hardware and software.
*
* @returns {Object} Returns support object with boolean values.
*/
const checkSupport = () => {
const battery = HARDWARE.battery.level;
const status = HARDWARE.display.status;
const brightness = HARDWARE.display.brightness;
const keyboard = processRuns("squeekboard");
const service = serviceRuns(APP.name);
const sudo = sudoRights();
const deb = APP.build.maker === "deb";
return {
batteryLevel: !!battery.path,
displayStatus: !!status.path && !!status.command,
displayBrightness: sudo && !!brightness.path && !!brightness.max,
keyboardVisibility: keyboard,
sudoRights: sudo,
appUpdate: sudo && service && deb,
};
}; };
/** /**
@@ -196,7 +229,7 @@ const getModel = () => {
const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"]; const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"];
for (const path of paths) { for (const path of paths) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
return execSyncCommand("sudo", ["cat", path]) || "Generic"; return execSyncCommand("cat", [path]) || "Generic";
} }
} }
return "Generic"; return "Generic";
@@ -211,7 +244,7 @@ const getVendor = () => {
const paths = ["/sys/class/dmi/id/board_vendor"]; const paths = ["/sys/class/dmi/id/board_vendor"];
for (const path of paths) { for (const path of paths) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
return execSyncCommand("sudo", ["cat", path]) || "Generic"; return execSyncCommand("cat", [path]) || "Generic";
} }
} }
const model = getModel(); const model = getModel();
@@ -222,22 +255,22 @@ const getVendor = () => {
}; };
/** /**
* Gets the serial number using `/sys/firmware/devicetree/base/serial-number` or `/sys/class/dmi/id/product_serial`. * Gets the serial number using `/sys/firmware/devicetree/base/serial-number`.
* *
* @returns {string} The serial number of the device or '123456' if not found. * @returns {string} The serial number or machine id parts of the device or '123456' if not found.
*/ */
const getSerialNumber = () => { const getSerialNumber = () => {
const paths = ["/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial"]; const paths = ["/sys/firmware/devicetree/base/serial-number"];
for (const path of paths) { for (const path of paths) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
return execSyncCommand("sudo", ["cat", path]) || "123456"; return execSyncCommand("cat", [path]) || "123456";
} }
} }
return "123456"; return getMachineId().slice(-6);
}; };
/** /**
* Gets the host machine id using `/etc/machine-id`. * Gets the machine id using `/etc/machine-id`.
* *
* @returns {string} The machine id of the system or '123456' if not found. * @returns {string} The machine id of the system or '123456' if not found.
*/ */
@@ -245,12 +278,38 @@ const getMachineId = () => {
const paths = ["/etc/machine-id"]; const paths = ["/etc/machine-id"];
for (const path of paths) { for (const path of paths) {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
return execSyncCommand("sudo", ["cat", path]) || "123456"; return execSyncCommand("cat", [path]) || "123456";
} }
} }
return "123456"; return "123456";
}; };
/**
* Gets the network interfaces addresses using `os.networkInterfaces()`.
*
* @returns {Object} The network addresses of all interfaces.
*/
const getNetworkAddresses = () => {
const addresses = {};
for (const [key, interfaces] of Object.entries(os.networkInterfaces())) {
for (const interface of interfaces) {
if (interface.internal || !interface.family || !interface.address) {
continue;
}
const name = key.charAt(0).toUpperCase() + key.slice(1);
if (!addresses[name]) {
addresses[name] = {};
}
const family = interface.family;
if (!addresses[name][family]) {
addresses[name][family] = [];
}
addresses[name][family].push(interface.address);
}
}
return addresses;
};
/** /**
* Gets the host name of the current system using `os.hostname()`. * Gets the host name of the current system using `os.hostname()`.
* *
@@ -371,7 +430,31 @@ const getDisplayStatusPath = () => {
}; };
/** /**
* Gets the current display power status using `wlopm` or `xset`. * Gets the available display status command checking for `wlopm`, `kscreen-doctor` and `xset`.
*
* @returns {string|null} The display status command or null if nothing was found.
*/
const getDisplayStatusCommand = () => {
const type = HARDWARE.session.type;
const desktop = HARDWARE.session.desktop;
const mapping = {
wayland: [
{ command: "wlopm", desktops: ["labwc", "wayfire", "unknown"] },
{ command: "kscreen-doctor", desktops: ["kde", "plasma", "unknown"] },
],
x11: [{ command: "xset", desktops: ["*"] }],
}[type];
for (const map of mapping || []) {
if (commandExists(map.command) && map.desktops.some((d) => d === "*" || desktop.includes(d))) {
return map.command;
}
}
console.warn("No display command available");
return null;
};
/**
* Gets the current display power status using the available command.
* *
* @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs. * @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
*/ */
@@ -379,24 +462,34 @@ const getDisplayStatus = () => {
if (!HARDWARE.support.displayStatus) { if (!HARDWARE.support.displayStatus) {
return null; return null;
} }
if (session("wayland") && commandExists("wlopm")) { switch (HARDWARE.display.status.command) {
const status = execSyncCommand("wlopm", []); case "wlopm":
if (status !== null) { const wlopm = execSyncCommand("wlopm", []);
const out = status.split("\n")[0].split(" "); if (wlopm !== null) {
return out.pop().toUpperCase(); const output = wlopm.split("\n")[0].split(" ");
} return output.pop().toUpperCase();
} else if (session("x11") && commandExists("xset")) { }
const status = execSyncCommand("xset", ["-q"]); break;
if (status !== null) { case "kscreen-doctor":
const on = /Monitor is On/.test(status); const kdoc = execSyncCommand("kscreen-doctor", ["--dpms", "show"]);
return on ? "ON" : "OFF"; if (kdoc !== null) {
} const output = kdoc.split("\n")[0].split(" ");
return output.pop().toUpperCase();
}
break;
case "xset":
const xset = execSyncCommand("xset", ["-q"]);
if (xset !== null) {
const output = /Monitor is On/.test(xset);
return output ? "ON" : "OFF";
}
break;
} }
return null; return null;
}; };
/** /**
* Sets the display power status using `wlopm` or `xset`. * Sets the display power status using the available command.
* *
* This function takes a desired status ('ON' or 'OFF') and executes * This function takes a desired status ('ON' or 'OFF') and executes
* the appropriate command to set the display status. * the appropriate command to set the display status.
@@ -414,10 +507,16 @@ const setDisplayStatus = (status, callback = null) => {
if (typeof callback === "function") callback(null, "Invalid status"); if (typeof callback === "function") callback(null, "Invalid status");
return; return;
} }
if (session("wayland") && commandExists("wlopm")) { switch (HARDWARE.display.status.command) {
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback); case "wlopm":
} else if (session("x11") && commandExists("xset")) { execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback); break;
case "kscreen-doctor":
execAsyncCommand("kscreen-doctor", ["--dpms", status.toLowerCase()], callback);
break;
case "xset":
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
break;
} }
}; };
@@ -444,7 +543,7 @@ const getDisplayBrightnessPath = () => {
* @returns {number|null} The brightness maximum value or null if an error occurs. * @returns {number|null} The brightness maximum value or null if an error occurs.
*/ */
const getDisplayBrightnessMax = () => { const getDisplayBrightnessMax = () => {
if (!HARDWARE.support.displayBrightness) { if (!HARDWARE.display.brightness.path) {
return null; return null;
} }
const max = fs.readFileSync(`${HARDWARE.display.brightness.path}/max_brightness`, "utf8").trim(); const max = fs.readFileSync(`${HARDWARE.display.brightness.path}/max_brightness`, "utf8").trim();
@@ -490,7 +589,7 @@ const setDisplayBrightness = (brightness, callback = null) => {
if (typeof callback === "function") callback(null, "Invalid brightness"); if (typeof callback === "function") callback(null, "Invalid brightness");
return; return;
} }
const max = HARDWARE.display.brightness.max; const max = HARDWARE.display.brightness.max || 1;
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max)); const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback); const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
proc.stdin.write(value.toString()); proc.stdin.write(value.toString());
@@ -557,6 +656,10 @@ const checkPackageUpgrades = () => {
* @param {Function} callback - A callback function that receives the output or error. * @param {Function} callback - A callback function that receives the output or error.
*/ */
const shutdownSystem = (callback = null) => { const shutdownSystem = (callback = null) => {
if (!HARDWARE.support.sudoRights) {
if (typeof callback === "function") callback(null, "Not supported");
return;
}
execAsyncCommand("sudo", ["shutdown", "-h", "now"], callback); execAsyncCommand("sudo", ["shutdown", "-h", "now"], callback);
}; };
@@ -569,9 +672,39 @@ const shutdownSystem = (callback = null) => {
* @param {Function} callback - A callback function that receives the output or error. * @param {Function} callback - A callback function that receives the output or error.
*/ */
const rebootSystem = (callback = null) => { const rebootSystem = (callback = null) => {
if (!HARDWARE.support.sudoRights) {
if (typeof callback === "function") callback(null, "Not supported");
return;
}
execAsyncCommand("sudo", ["reboot"], callback); execAsyncCommand("sudo", ["reboot"], callback);
}; };
/**
* Checks if sudo commands can run without a password.
*
* @returns {bool} Returns true if password-less sudo rights exists.
*/
const sudoRights = () => {
try {
const output = cpr.execSync(`sudo -n true`, { encoding: "utf8" });
return output !== null && !output.includes("password is required");
} catch {}
return false;
};
/**
* Checks if a service is running using `systemctl`.
*
* @param {string} name - The service name to check.
* @returns {bool} Returns true if the service runs.
*/
const serviceRuns = (name) => {
try {
return !!cpr.execSync(`systemctl --user is-active ${name}`, { encoding: "utf8" });
} catch {}
return false;
};
/** /**
* Checks if a process is running using `pidof`. * Checks if a process is running using `pidof`.
* *
@@ -648,14 +781,61 @@ const execAsyncCommand = (cmd, args = [], callback = null) => {
} }
} catch (error) { } catch (error) {
console.error("Execute Async:", error.message); console.error("Execute Async:", error.message);
if (typeof callback === "function") { if (typeof callback === "function") callback(null, error.message);
callback(null, error.message);
}
} }
}); });
return proc; return proc;
}; };
/**
* Executes a script command asynchronously.
*
* @param {string} cmd - The script to execute.
* @param {Array<string>} args - The arguments for the command.
* @param {Function} callback - A callback function that receives the progress or error.
* @returns {Object} The spawned process object.
*/
const execScriptCommand = (cmd, args = [], callback = null) => {
let progress = 10;
let proc = cpr.spawn(cmd, args);
if (typeof callback === "function") callback(progress, null);
proc.stdout.on("data", (data) => {
if (data) {
const lines = data.toString().trim();
const output = lines.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, "");
if (output) {
console.log(output);
}
}
});
proc.stderr.on("data", (data) => {
if (data) {
const lines = data.toString().trim();
const output = lines.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, "");
if (output) {
const matches = output.match(/(\d{1,3})%/g);
if (matches) {
const match = Math.max(...matches.map((p) => parseInt(p)));
const percent = Math.floor(match / 10) * 10;
if (percent > 10 && percent > progress) {
progress = percent;
if (typeof callback === "function") callback(progress - 10, null);
}
}
}
}
});
proc.on("close", (code) => {
if (code !== 0) {
console.log(`Script exited with error code ${code}.`);
if (typeof callback === "function") callback(null, code);
} else {
console.log("Script exited successfully.");
if (typeof callback === "function") callback(100, null);
}
});
};
/** /**
* Executes a D-Bus method call synchronously using `dbus-send`. * Executes a D-Bus method call synchronously using `dbus-send`.
* *
@@ -670,15 +850,11 @@ const dbusCall = (path, method, values, callback = null) => {
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`; const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`]; const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
try { try {
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" }); const output = cpr.execSync([cmd, ...args].join(" ").trim(), { encoding: "utf8" });
if (typeof callback === "function") { if (typeof callback === "function") callback(output.trim().replace(/\0/g, ""), null);
callback(output.trim().replace(/\0/g, ""), null);
}
} catch (error) { } catch (error) {
console.error("Call D-Bus:", error.message); console.error("Call D-Bus:", error.message);
if (typeof callback === "function") { if (typeof callback === "function") callback(null, error.message);
callback(null, error.message);
}
} }
}; };
@@ -712,17 +888,13 @@ const dbusMonitor = (path, callback) => {
} }
} catch (error) { } catch (error) {
console.error("Monitor D-Bus:", error.message); console.error("Monitor D-Bus:", error.message);
if (typeof callback === "function") { if (typeof callback === "function") callback(null, error.message);
callback(null, error.message);
}
} }
}); });
proc.stderr.on("data", (data) => { proc.stderr.on("data", (data) => {
if (data) { if (data) {
console.error("Monitor D-Bus:", data.toString()); console.error("Monitor D-Bus:", data.toString());
if (typeof callback === "function") { if (typeof callback === "function") callback(null, data.toString());
callback(null, data.toString());
}
} }
}); });
return proc; return proc;
@@ -755,19 +927,16 @@ module.exports = {
getVendor, getVendor,
getSerialNumber, getSerialNumber,
getMachineId, getMachineId,
getNetworkAddresses,
getHostName, getHostName,
getUpTime, getUpTime,
getMemorySize, getMemorySize,
getMemoryUsage, getMemoryUsage,
getProcessorUsage, getProcessorUsage,
getProcessorTemperature, getProcessorTemperature,
getBatteryLevelPath,
getBatteryLevel, getBatteryLevel,
getDisplayStatusPath,
getDisplayStatus, getDisplayStatus,
setDisplayStatus, setDisplayStatus,
getDisplayBrightnessPath,
getDisplayBrightnessMax,
getDisplayBrightness, getDisplayBrightness,
setDisplayBrightness, setDisplayBrightness,
getKeyboardVisibility, getKeyboardVisibility,
@@ -775,4 +944,7 @@ module.exports = {
checkPackageUpgrades, checkPackageUpgrades,
shutdownSystem, shutdownSystem,
rebootSystem, rebootSystem,
execSyncCommand,
execAsyncCommand,
execScriptCommand,
}; };
+186 -45
View File
@@ -1,10 +1,11 @@
const mqtt = require("mqtt"); const mqtt = require("mqtt");
const hardware = require("./hardware"); const hardware = require("./hardware");
const { app } = require("electron"); const { app } = require("electron");
const Events = require("events");
global.INTEGRATION = global.INTEGRATION || { global.INTEGRATION = global.INTEGRATION || {
initialized: false, initialized: false,
status: null, events: new Events(),
}; };
/** /**
@@ -38,14 +39,15 @@ const init = async () => {
// Init globals // Init globals
INTEGRATION.discovery = discovery; INTEGRATION.discovery = discovery;
INTEGRATION.node = `rpi_${deviceId}`; INTEGRATION.node = `rpi_${deviceId}`;
INTEGRATION.root = `${app.getName()}/${INTEGRATION.node}`; INTEGRATION.root = `${APP.name}/${INTEGRATION.node}`;
INTEGRATION.device = { INTEGRATION.device = {
name: `TouchKio ${deviceName}`, name: `${APP.title} ${deviceName}`,
model: model, model: model,
manufacturer: vendor, manufacturer: vendor,
serial_number: serialNumber, serial_number: serialNumber,
identifiers: [INTEGRATION.node], identifiers: [INTEGRATION.node],
sw_version: `${app.getName()}-v${app.getVersion()}`, sw_version: `${APP.name}-v${APP.version}`,
configuration_url: APP.homepage,
}; };
// Connection settings // Connection settings
@@ -56,10 +58,11 @@ const init = async () => {
// Client connected // Client connected
INTEGRATION.client INTEGRATION.client
.on("connect", () => { .once("connect", () => {
console.log(`MQTT Connected: ${url.toString()}\n`); console.log(`MQTT Connected: ${url.toString()}\n`);
// Init client controls // Init client controls
initApp();
initShutdown(); initShutdown();
initReboot(); initReboot();
initRefresh(); initRefresh();
@@ -67,12 +70,14 @@ const init = async () => {
initDisplay(); initDisplay();
initKeyboard(); initKeyboard();
initPageNumber(); initPageNumber();
initPageZoom();
initPageUrl(); initPageUrl();
// Init client sensors // Init client sensors
initModel(); initModel();
initSerialNumber(); initSerialNumber();
initHostName(); initHostName();
initNetworkAddress();
initUpTime(); initUpTime();
initMemorySize(); initMemorySize();
initMemoryUsage(); initMemoryUsage();
@@ -85,20 +90,14 @@ const init = async () => {
// Integration initialized // Integration initialized
INTEGRATION.initialized = true; INTEGRATION.initialized = true;
INTEGRATION.status = "Online";
}) })
.on("error", (error) => { .on("error", (error) => {
console.error("MQTT:", error); console.error("MQTT", error.message);
INTEGRATION.status = "Offline";
}); });
// Update sensor states from notifiers // Update sensor states from events
HARDWARE.display.notifiers.push(() => { HARDWARE.events.on("updateDisplay", updateDisplay);
updateDisplay(); HARDWARE.events.on("updateKeyboard", updateKeyboard);
});
HARDWARE.keyboard.notifiers.push(() => {
updateKeyboard();
});
// Update time sensors periodically (30s) // Update time sensors periodically (30s)
setInterval(() => { setInterval(() => {
@@ -111,8 +110,9 @@ const init = async () => {
update(); update();
}, 60 * 1000); }, 60 * 1000);
// Update package sensors periodically (60min) // Update upgrade sensors periodically (1h)
setInterval(() => { setInterval(() => {
updateApp();
updatePackageUpgrades(); updatePackageUpgrades();
}, 3600 * 1000); }, 3600 * 1000);
@@ -130,7 +130,9 @@ const update = async () => {
// Update client sensors // Update client sensors
updateKiosk(); updateKiosk();
updatePageNumber(); updatePageNumber();
updatePageZoom();
updatePageUrl(); updatePageUrl();
updateNetworkAddress();
updateUpTime(); updateUpTime();
updateLastActive(); updateLastActive();
updateMemoryUsage(); updateMemoryUsage();
@@ -185,10 +187,64 @@ const publishState = (path, state) => {
return INTEGRATION.client.publish(root, `${state}`, { qos: 1, retain: true }); return INTEGRATION.client.publish(root, `${state}`, { qos: 1, retain: true });
}; };
/**
* Initializes the app update entity and handles the execute logic.
*/
const initApp = () => {
if (!HARDWARE.support.appUpdate) {
return;
}
const root = `${INTEGRATION.root}/app`;
const config = {
name: "App",
unique_id: `${INTEGRATION.node}_app`,
command_topic: `${root}/install`,
state_topic: `${root}/version/state`,
payload_install: "update",
device: INTEGRATION.device,
};
publishConfig("update", config)
.on("message", (topic, message) => {
if (topic === config.command_topic) {
console.log("Update App...");
hardware.setDisplayStatus("ON");
const args = ["-c", `bash <(wget -qO- ${APP.scripts.install}) update`];
hardware.execScriptCommand("bash", args, (progress, error) => {
updateApp(progress);
});
}
})
.subscribe(config.command_topic);
updateApp();
};
/**
* Updates the app update entity via the mqtt connection.
*/
const updateApp = async (progress = 0) => {
const latest = APP.releases.latest;
if (!latest) {
return;
}
const version = {
title: latest.title,
latest_version: latest.version,
installed_version: APP.version,
release_summary: latest.summary.slice(0, 250) + "...",
release_url: latest.url,
update_percentage: progress || null,
in_progress: progress && progress > 0 && progress < 100,
};
publishState("app/version", JSON.stringify(version));
};
/** /**
* Initializes the shutdown button and handles the execute logic. * Initializes the shutdown button and handles the execute logic.
*/ */
const initShutdown = () => { const initShutdown = () => {
if (!HARDWARE.support.sudoRights) {
return;
}
const root = `${INTEGRATION.root}/shutdown`; const root = `${INTEGRATION.root}/shutdown`;
const config = { const config = {
name: "Shutdown", name: "Shutdown",
@@ -212,6 +268,9 @@ const initShutdown = () => {
* Initializes the reboot button and handles the execute logic. * Initializes the reboot button and handles the execute logic.
*/ */
const initReboot = () => { const initReboot = () => {
if (!HARDWARE.support.sudoRights) {
return;
}
const root = `${INTEGRATION.root}/reboot`; const root = `${INTEGRATION.root}/reboot`;
const config = { const config = {
name: "Reboot", name: "Reboot",
@@ -248,7 +307,7 @@ const initRefresh = () => {
if (topic === config.command_topic) { if (topic === config.command_topic) {
console.log("Refreshing webview..."); console.log("Refreshing webview...");
hardware.setDisplayStatus("ON"); hardware.setDisplayStatus("ON");
WEBVIEW.views[WEBVIEW.viewActive].webContents.reloadIgnoringCache(); WEBVIEW.events.emit("reloadView");
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -308,7 +367,7 @@ const initKiosk = () => {
/** /**
* Updates the kiosk status via the mqtt connection. * Updates the kiosk status via the mqtt connection.
*/ */
const updateKiosk = () => { const updateKiosk = async () => {
const kiosk = WEBVIEW.status; const kiosk = WEBVIEW.status;
publishState("kiosk", kiosk); publishState("kiosk", kiosk);
}; };
@@ -340,13 +399,23 @@ const initDisplay = () => {
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.log("Set Display Status:", status);
hardware.setDisplayStatus(status); hardware.setDisplayStatus(status, (reply, error) => {
hardware.update(); if (error) {
console.warn("Failed:", error);
} else {
hardware.update();
}
});
} 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.log("Set Display Brightness:", brightness);
hardware.setDisplayBrightness(brightness); hardware.setDisplayBrightness(brightness, (reply, error) => {
hardware.update(); if (error) {
console.warn("Failed:", error);
} else {
hardware.update();
}
});
} }
}) })
.subscribe(config.command_topic) .subscribe(config.command_topic)
@@ -357,7 +426,7 @@ const initDisplay = () => {
/** /**
* Updates the display status, brightness via the mqtt connection. * Updates the display status, brightness via the mqtt connection.
*/ */
const updateDisplay = () => { const updateDisplay = async () => {
const status = hardware.getDisplayStatus(); const status = hardware.getDisplayStatus();
const brightness = hardware.getDisplayBrightness(); const brightness = hardware.getDisplayBrightness();
publishState("display/power", status); publishState("display/power", status);
@@ -407,7 +476,7 @@ const initKeyboard = () => {
/** /**
* Updates the keyboard visibility via the mqtt connection. * Updates the keyboard visibility via the mqtt connection.
*/ */
const updateKeyboard = () => { const updateKeyboard = async () => {
const visibility = hardware.getKeyboardVisibility(); const visibility = hardware.getKeyboardVisibility();
publishState("keyboard", visibility); publishState("keyboard", visibility);
}; };
@@ -416,7 +485,7 @@ const updateKeyboard = () => {
* Initializes the page number and handles the execute logic. * Initializes the page number and handles the execute logic.
*/ */
const initPageNumber = () => { const initPageNumber = () => {
if (WEBVIEW.viewUrls.length < 3) { if (WEBVIEW.viewUrls.length <= 2) {
return; return;
} }
const root = `${INTEGRATION.root}/page_number`; const root = `${INTEGRATION.root}/page_number`;
@@ -438,7 +507,8 @@ const initPageNumber = () => {
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.log("Set Page Number:", number);
WEBVIEW.viewActive = number; WEBVIEW.viewActive = number || 1;
WEBVIEW.events.emit("updateView");
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -448,11 +518,50 @@ const initPageNumber = () => {
/** /**
* Updates the page number via the mqtt connection. * Updates the page number via the mqtt connection.
*/ */
const updatePageNumber = () => { const updatePageNumber = async () => {
const pageNumber = WEBVIEW.viewActive; const pageNumber = WEBVIEW.viewActive || 1;
publishState("page_number", pageNumber); publishState("page_number", pageNumber);
}; };
/**
* Initializes the page zoom and handles the execute logic.
*/
const initPageZoom = () => {
const root = `${INTEGRATION.root}/page_zoom`;
const config = {
name: "Page Zoom",
unique_id: `${INTEGRATION.node}_page_zoom`,
command_topic: `${root}/set`,
state_topic: `${root}/state`,
value_template: "{{ value | int }}",
mode: "slider",
min: 25,
max: 400,
unit_of_measurement: "%",
icon: "mdi:magnify-plus",
device: INTEGRATION.device,
};
publishConfig("number", config)
.on("message", (topic, message) => {
if (topic === config.command_topic) {
const zoom = parseInt(message, 10);
console.log("Set Page Zoom:", zoom);
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.setZoomFactor(zoom / 100.0);
WEBVIEW.events.emit("updateView");
}
})
.subscribe(config.command_topic);
updatePageZoom();
};
/**
* Updates the page zoom via the mqtt connection.
*/
const updatePageZoom = async () => {
const pageZoom = Math.round(WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getZoomFactor() * 100.0);
publishState("page_zoom", pageZoom);
};
/** /**
* Initializes the page url and handles the execute logic. * Initializes the page url and handles the execute logic.
*/ */
@@ -473,7 +582,7 @@ const initPageUrl = () => {
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.log("Set Page Url:", url);
WEBVIEW.views[WEBVIEW.viewActive].webContents.loadURL(url); WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.loadURL(url);
} }
}) })
.subscribe(config.command_topic); .subscribe(config.command_topic);
@@ -483,9 +592,9 @@ const initPageUrl = () => {
/** /**
* Updates the page url via the mqtt connection. * Updates the page url via the mqtt connection.
*/ */
const updatePageUrl = () => { const updatePageUrl = async () => {
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive || 1];
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL(); const currentUrl = WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getURL();
const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl; const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl;
publishState("page_url", pageUrl); publishState("page_url", pageUrl);
}; };
@@ -499,6 +608,7 @@ const initModel = () => {
name: "Model", name: "Model",
unique_id: `${INTEGRATION.node}_model`, unique_id: `${INTEGRATION.node}_model`,
state_topic: `${root}/state`, state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value }}", value_template: "{{ value }}",
icon: "mdi:raspberry-pi", icon: "mdi:raspberry-pi",
device: INTEGRATION.device, device: INTEGRATION.device,
@@ -510,9 +620,10 @@ const initModel = () => {
/** /**
* Updates the model sensor via the mqtt connection. * Updates the model sensor via the mqtt connection.
*/ */
const updateModel = () => { const updateModel = async () => {
const model = hardware.getModel(); const model = hardware.getModel();
publishState("model", model); publishState("model", model);
publishAttributes("model", HARDWARE.support);
}; };
/** /**
@@ -535,11 +646,41 @@ const initSerialNumber = () => {
/** /**
* Updates the serial number sensor via the mqtt connection. * Updates the serial number sensor via the mqtt connection.
*/ */
const updateSerialNumber = () => { const updateSerialNumber = async () => {
const serialNumber = hardware.getSerialNumber(); const serialNumber = hardware.getSerialNumber();
publishState("serial_number", serialNumber); publishState("serial_number", serialNumber);
}; };
/**
* Initializes the network address sensor.
*/
const initNetworkAddress = () => {
const root = `${INTEGRATION.root}/network_address`;
const config = {
name: "Network Address",
unique_id: `${INTEGRATION.node}_network_address`,
state_topic: `${root}/state`,
json_attributes_topic: `${root}/attributes`,
value_template: "{{ value }}",
icon: "mdi:ip-network",
device: INTEGRATION.device,
};
publishConfig("sensor", config);
updateNetworkAddress();
};
/**
* Updates the network address sensor via the mqtt connection.
*/
const updateNetworkAddress = async () => {
const networkAddresses = hardware.getNetworkAddresses();
const [name] = Object.keys(networkAddresses);
const [family] = name ? Object.keys(networkAddresses[name]) : [];
const networkAddress = networkAddresses[name]?.[family]?.[0] || null;
publishState("network_address", networkAddress);
publishAttributes("network_address", networkAddresses);
};
/** /**
* Initializes the host name sensor. * Initializes the host name sensor.
*/ */
@@ -560,7 +701,7 @@ const initHostName = () => {
/** /**
* Updates the host name sensor via the mqtt connection. * Updates the host name sensor via the mqtt connection.
*/ */
const updateHostName = () => { const updateHostName = async () => {
const hostName = hardware.getHostName(); const hostName = hardware.getHostName();
publishState("host_name", hostName); publishState("host_name", hostName);
}; };
@@ -586,7 +727,7 @@ const initUpTime = () => {
/** /**
* Updates the up time sensor via the mqtt connection. * Updates the up time sensor via the mqtt connection.
*/ */
const updateUpTime = () => { const updateUpTime = async () => {
const upTime = hardware.getUpTime(); const upTime = hardware.getUpTime();
publishState("up_time", upTime); publishState("up_time", upTime);
}; };
@@ -612,7 +753,7 @@ const initMemorySize = () => {
/** /**
* Updates the memory size sensor via the mqtt connection. * Updates the memory size sensor via the mqtt connection.
*/ */
const updateMemorySize = () => { const updateMemorySize = async () => {
const memorySize = hardware.getMemorySize(); const memorySize = hardware.getMemorySize();
publishState("memory_size", memorySize); publishState("memory_size", memorySize);
}; };
@@ -638,7 +779,7 @@ const initMemoryUsage = () => {
/** /**
* Updates the memory usage sensor via the mqtt connection. * Updates the memory usage sensor via the mqtt connection.
*/ */
const updateMemoryUsage = () => { const updateMemoryUsage = async () => {
const memoryUsage = hardware.getMemoryUsage(); const memoryUsage = hardware.getMemoryUsage();
publishState("memory_usage", memoryUsage); publishState("memory_usage", memoryUsage);
}; };
@@ -664,7 +805,7 @@ const initProcessorUsage = () => {
/** /**
* Updates the processor usage sensor via the mqtt connection. * Updates the processor usage sensor via the mqtt connection.
*/ */
const updateProcessorUsage = () => { const updateProcessorUsage = async () => {
const processorUsage = hardware.getProcessorUsage(); const processorUsage = hardware.getProcessorUsage();
publishState("processor_usage", processorUsage); publishState("processor_usage", processorUsage);
}; };
@@ -690,7 +831,7 @@ const initProcessorTemperature = () => {
/** /**
* Updates the processor temperature sensor via the mqtt connection. * Updates the processor temperature sensor via the mqtt connection.
*/ */
const updateProcessorTemperature = () => { const updateProcessorTemperature = async () => {
const processorTemperature = hardware.getProcessorTemperature(); const processorTemperature = hardware.getProcessorTemperature();
publishState("processor_temperature", processorTemperature); publishState("processor_temperature", processorTemperature);
}; };
@@ -719,7 +860,7 @@ const initBatteryLevel = () => {
/** /**
* Updates the battery level sensor via the mqtt connection. * Updates the battery level sensor via the mqtt connection.
*/ */
const updateBatteryLevel = () => { const updateBatteryLevel = async () => {
const batteryLevel = hardware.getBatteryLevel(); const batteryLevel = hardware.getBatteryLevel();
publishState("battery_level", batteryLevel); publishState("battery_level", batteryLevel);
}; };
@@ -745,14 +886,14 @@ const initPackageUpgrades = () => {
/** /**
* Updates the package upgrades sensor via the mqtt connection. * Updates the package upgrades sensor via the mqtt connection.
*/ */
const updatePackageUpgrades = () => { const updatePackageUpgrades = async () => {
const packages = hardware.checkPackageUpgrades(); const packages = hardware.checkPackageUpgrades();
const attributes = { const attributes = {
total: packages.length, total: packages.length,
packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()), packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()),
}; };
publishAttributes("package_upgrades", attributes);
publishState("package_upgrades", attributes.total); publishState("package_upgrades", attributes.total);
publishAttributes("package_upgrades", attributes);
}; };
/** /**
@@ -775,7 +916,7 @@ const initHeartbeat = () => {
/** /**
* Updates the heartbeat sensor via the mqtt connection. * Updates the heartbeat sensor via the mqtt connection.
*/ */
const updateHeartbeat = () => { const updateHeartbeat = async () => {
const now = new Date(); const now = new Date();
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000); const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, ""); const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
@@ -803,7 +944,7 @@ const initLastActive = () => {
/** /**
* Updates the last active sensor via the mqtt connection. * Updates the last active sensor via the mqtt connection.
*/ */
const updateLastActive = () => { const updateLastActive = async () => {
const now = new Date(); const now = new Date();
const then = WEBVIEW.pointer.time; const then = WEBVIEW.pointer.time;
const lastActive = Math.abs(now - then) / (1000 * 60); const lastActive = Math.abs(now - then) / (1000 * 60);
+300 -70
View File
@@ -1,14 +1,16 @@
const path = require("path"); const path = require("path");
const axios = require("axios");
const hardware = require("./hardware"); const hardware = require("./hardware");
const integration = require("./integration"); const integration = require("./integration");
const { app, screen, nativeTheme, ipcMain, session, BaseWindow, WebContentsView } = require("electron"); const { app, nativeTheme, globalShortcut, ipcMain, session, BaseWindow, WebContentsView } = require("electron");
const Events = require("events");
global.WEBVIEW = global.WEBVIEW || { global.WEBVIEW = global.WEBVIEW || {
initialized: false, initialized: false,
events: new Events(),
status: null, status: null,
locked: false, locked: false,
pointer: { pointer: {
position: {},
time: new Date(), time: new Date(),
}, },
}; };
@@ -37,28 +39,23 @@ const init = async () => {
const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url); const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url);
// Init global properties // Init global properties
WEBVIEW.viewActive = 0;
WEBVIEW.viewUrls = urls; WEBVIEW.viewUrls = urls;
WEBVIEW.viewZoom = zoom; WEBVIEW.viewZoom = zoom;
WEBVIEW.viewTheme = theme; WEBVIEW.viewTheme = theme;
WEBVIEW.pagerEnabled = widget;
WEBVIEW.widgetTheme = theme; WEBVIEW.widgetTheme = theme;
WEBVIEW.widgetEnabled = widget; WEBVIEW.widgetEnabled = widget;
WEBVIEW.navigationTheme = theme; WEBVIEW.navigationTheme = theme;
WEBVIEW.navigationEnabled = widget; WEBVIEW.navigationEnabled = widget;
nativeTheme.themeSource = WEBVIEW.viewTheme; nativeTheme.themeSource = WEBVIEW.viewTheme;
Object.defineProperty(WEBVIEW, "viewActive", { WEBVIEW.events.on("reloadView", reloadView);
get() { WEBVIEW.events.on("updateView", updateView);
return this._viewActive || 0;
},
set(v) {
this._viewActive = Math.max(0, Math.min(v, WEBVIEW.viewUrls.length - 1));
updateView();
},
});
// Init global root window // Init global root window
WEBVIEW.window = new BaseWindow({ WEBVIEW.window = new BaseWindow({
title: "TouchKio", title: APP.title,
icon: path.join(__dirname, "..", "img", "icon.png"), icon: path.join(APP.path, "img", "icon.png"),
}); });
WEBVIEW.window.setMenuBarVisibility(false); WEBVIEW.window.setMenuBarVisibility(false);
WEBVIEW.window.setFullScreen(fullscreen); WEBVIEW.window.setFullScreen(fullscreen);
@@ -78,10 +75,24 @@ const init = async () => {
view.setVisible(i === 0); view.setVisible(i === 0);
view.setBackgroundColor("#FFFFFFFF"); view.setBackgroundColor("#FFFFFFFF");
WEBVIEW.window.contentView.addChildView(view); WEBVIEW.window.contentView.addChildView(view);
view.webContents.loadURL(url);
WEBVIEW.views.push(view); WEBVIEW.views.push(view);
onlineStatus(url).then(() => {
view.webContents.loadURL(url);
});
}); });
// Init global pager
WEBVIEW.pager = new WebContentsView({
transparent: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
WEBVIEW.pager.setBackgroundColor("#00000000");
WEBVIEW.window.contentView.addChildView(WEBVIEW.pager);
WEBVIEW.pager.webContents.loadFile(path.join(APP.path, "html", "pager.html"));
// Init global widget // Init global widget
WEBVIEW.widget = new WebContentsView({ WEBVIEW.widget = new WebContentsView({
transparent: true, transparent: true,
@@ -92,7 +103,7 @@ const init = async () => {
}); });
WEBVIEW.widget.setBackgroundColor("#00000000"); WEBVIEW.widget.setBackgroundColor("#00000000");
WEBVIEW.window.contentView.addChildView(WEBVIEW.widget); WEBVIEW.window.contentView.addChildView(WEBVIEW.widget);
WEBVIEW.widget.webContents.loadFile(path.join(app.getAppPath(), "html", "widget.html")); WEBVIEW.widget.webContents.loadFile(path.join(APP.path, "html", "widget.html"));
// Init global navigation // Init global navigation
WEBVIEW.navigation = new WebContentsView({ WEBVIEW.navigation = new WebContentsView({
@@ -104,14 +115,14 @@ const init = async () => {
}); });
WEBVIEW.navigation.setBackgroundColor("#00000000"); WEBVIEW.navigation.setBackgroundColor("#00000000");
WEBVIEW.window.contentView.addChildView(WEBVIEW.navigation); WEBVIEW.window.contentView.addChildView(WEBVIEW.navigation);
WEBVIEW.navigation.webContents.loadFile(path.join(app.getAppPath(), "html", "navigation.html")); WEBVIEW.navigation.webContents.loadFile(path.join(APP.path, "html", "navigation.html"));
// Register events // Register events
windowEvents(); await windowEvents();
widgetEvents(); await widgetEvents();
navigationEvents(); await navigationEvents();
domEvents(); await viewEvents();
appEvents(); await appEvents();
return true; return true;
}; };
@@ -150,8 +161,15 @@ const update = async () => {
* Updates the active view. * Updates the active view.
*/ */
const updateView = () => { const updateView = () => {
const title = `TouchKio - ${new URL(WEBVIEW.viewUrls[WEBVIEW.viewActive]).host}`; if (!WEBVIEW.viewActive) {
console.log(`Update View: ${title} (${WEBVIEW.viewActive})`); return;
}
const url = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL();
const host = url.startsWith("data:") ? "whoopsie" : new URL(url).host;
const title = `${APP.title} - ${host} (${WEBVIEW.viewActive})`;
// Update window title
console.log(`Update View: ${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
@@ -186,17 +204,41 @@ const updateNavigation = () => {
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
const currentUrl = view.webContents.getURL(); const currentUrl = view.webContents.getURL();
// Hide pager button // Hide pager buttons
WEBVIEW.navigation.webContents.send("button-hidden", { WEBVIEW.navigation.webContents.send("button-hidden", {
id: "pager", id: "previous",
hidden: WEBVIEW.viewUrls.length < 3, hidden: WEBVIEW.viewUrls.length <= 2,
});
WEBVIEW.navigation.webContents.send("button-hidden", {
id: "next",
hidden: WEBVIEW.viewUrls.length <= 2,
});
// Disable pager buttons
WEBVIEW.navigation.webContents.send("button-disabled", {
id: "previous",
disabled: WEBVIEW.viewActive <= 1,
});
WEBVIEW.navigation.webContents.send("button-disabled", {
id: "next",
disabled: WEBVIEW.viewActive >= WEBVIEW.viewUrls.length - 1,
}); });
// Update url text // Update url text
WEBVIEW.navigation.webContents.send("input-text", { WEBVIEW.navigation.webContents.send("input-text", {
id: "url", id: "url",
text: currentUrl.startsWith("data:") ? "" : currentUrl, text: currentUrl.startsWith("data:") ? "" : currentUrl,
placeholder: defaultUrl, placeholder: defaultUrl.startsWith("data:") ? "" : defaultUrl,
});
// Disable zoom buttons
WEBVIEW.navigation.webContents.send("button-disabled", {
id: "minus",
disabled: view.webContents.getZoomFactor().toFixed(2) <= 0.25,
});
WEBVIEW.navigation.webContents.send("button-disabled", {
id: "plus",
disabled: view.webContents.getZoomFactor().toFixed(2) >= 4.0,
}); });
// Disable history buttons // Disable history buttons
@@ -225,11 +267,34 @@ const toggleNavigation = () => {
width: window.width, width: window.width,
height: height, height: height,
}); });
if (height > 0) {
WEBVIEW.navigation.webContents.focus();
} else {
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
}
// Resize webview // Resize webview
resizeView(); resizeView();
}; };
/**
* Decreases page zoom on the active webview.
*/
const zoomMinus = () => {
const view = WEBVIEW.views[WEBVIEW.viewActive];
view.webContents.setZoomFactor(Math.max(0.25, view.webContents.getZoomFactor() - 0.1));
update();
};
/**
* Increases page zoom on the active webview.
*/
const zoomPlus = () => {
const view = WEBVIEW.views[WEBVIEW.viewActive];
view.webContents.setZoomFactor(Math.min(4.0, view.webContents.getZoomFactor() + 0.1));
update();
};
/** /**
* Navigates backward in the history of the active webview. * Navigates backward in the history of the active webview.
*/ */
@@ -251,16 +316,25 @@ const historyForward = () => {
}; };
/** /**
* Activates the next webview or cycles back to the first. * Activates the previous webview page.
*/ */
const nextView = () => { const previousView = () => {
WEBVIEW.viewActive = (WEBVIEW.viewActive + 1) % WEBVIEW.views.length || 1; if (WEBVIEW.viewActive > 1) WEBVIEW.viewActive--;
updateView();
}; };
/** /**
* Reloads the current or default url on the active webview. * Activates the next webview page.
*/ */
const reloadView = () => { const nextView = () => {
if (WEBVIEW.viewActive < WEBVIEW.views.length - 1) WEBVIEW.viewActive++;
updateView();
};
/**
* Reloads the default url and settings on the active webview.
*/
const homeView = () => {
const view = WEBVIEW.views[WEBVIEW.viewActive]; const view = WEBVIEW.views[WEBVIEW.viewActive];
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
const currentUrl = view.webContents.getURL(); const currentUrl = view.webContents.getURL();
@@ -271,6 +345,30 @@ const reloadView = () => {
} else { } else {
view.webContents.reloadIgnoringCache(); view.webContents.reloadIgnoringCache();
} }
// Reset page zoom and history
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
setTimeout(() => {
view.webContents.navigationHistory.clear();
update();
}, 2000);
};
/**
* Reloads the current url on the active webview.
*/
const reloadView = () => {
const view = WEBVIEW.views[WEBVIEW.viewActive];
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
const currentUrl = view.webContents.getURL();
// Reload the default url or refresh the page
if (currentUrl.startsWith("data:")) {
view.webContents.loadURL(defaultUrl);
} else {
view.webContents.reloadIgnoringCache();
}
update();
}; };
/** /**
@@ -279,6 +377,7 @@ const reloadView = () => {
const resizeView = () => { const resizeView = () => {
const window = WEBVIEW.window.getBounds(); const window = WEBVIEW.window.getBounds();
const navigation = WEBVIEW.navigation.getBounds(); const navigation = WEBVIEW.navigation.getBounds();
const pager = { width: 20, height: window.height };
const widget = { width: 60, height: 200 }; const widget = { width: 60, height: 200 };
// Update view size // Update view size
@@ -289,13 +388,23 @@ const resizeView = () => {
width: window.width, width: window.width,
height: window.height - navigation.height, height: window.height - navigation.height,
}); });
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
}); });
// Update pager size
if (WEBVIEW.pagerEnabled) {
WEBVIEW.pager.setBounds({
x: window.width - pager.width,
y: 0,
width: pager.width,
height: pager.height,
});
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 - 15, x: window.width - 20,
y: parseInt(window.height / 2 - widget.height / 2, 10), y: parseInt(window.height / 2 - widget.height / 2, 10),
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
@@ -318,7 +427,7 @@ const resizeView = () => {
/** /**
* Register window events and handler. * Register window events and handler.
*/ */
const windowEvents = () => { const windowEvents = async () => {
// Handle window resize events // Handle window resize events
WEBVIEW.window.on("ready-to-show", resizeView); WEBVIEW.window.on("ready-to-show", resizeView);
WEBVIEW.window.on("resize", resizeView); WEBVIEW.window.on("resize", resizeView);
@@ -332,7 +441,7 @@ const windowEvents = () => {
} }
WEBVIEW.locked = false; WEBVIEW.locked = false;
}); });
HARDWARE.display.notifiers.push(() => { HARDWARE.events.on("updateDisplay", () => {
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF"; WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
if (WEBVIEW.locked) { if (WEBVIEW.locked) {
WEBVIEW.window.blur(); WEBVIEW.window.blur();
@@ -347,39 +456,46 @@ const windowEvents = () => {
WEBVIEW.window.on("enter-full-screen", update); WEBVIEW.window.on("enter-full-screen", update);
WEBVIEW.window.on("leave-full-screen", update); WEBVIEW.window.on("leave-full-screen", update);
// Handle window touch events for activity tracking // Handle global shortcut events
globalShortcut.register("Control+Left", () => {
previousView();
});
globalShortcut.register("Control+Right", () => {
nextView();
});
globalShortcut.register("Control+numsub", () => {
zoomMinus();
});
globalShortcut.register("Control+numadd", () => {
zoomPlus();
});
globalShortcut.register("Alt+Left", () => {
historyBackward();
});
globalShortcut.register("Alt+Right", () => {
historyForward();
});
// Check for window touch events (1s)
setInterval(() => { setInterval(() => {
const now = new Date(); const now = new Date();
const then = WEBVIEW.pointer.time; const then = WEBVIEW.pointer.time;
const delta = Math.abs(now - then) / 1000; const delta = Math.abs(now - then) / 1000;
const posNew = screen.getCursorScreenPoint();
const posOld = WEBVIEW.pointer.position;
// Cursor movement detected // Auto-hide navigation
if (posOld.x !== posNew.x || posOld.y !== posNew.y) { if (delta > 60) {
WEBVIEW.pointer.time = now;
// Update integration sensor
if (delta > 30) {
console.log("Update Last Active");
integration.update();
}
} else if (delta > 60) {
const navigation = WEBVIEW.navigation.getBounds(); const navigation = WEBVIEW.navigation.getBounds();
// Auto-hide navigation
if (navigation.height > 0) { if (navigation.height > 0) {
toggleNavigation(); toggleNavigation();
} }
} }
WEBVIEW.pointer.position = posNew;
}, 1000); }, 1000);
}; };
/** /**
* Register widget events and handler. * Register widget events and handler.
*/ */
const widgetEvents = () => { const widgetEvents = async () => {
if (!WEBVIEW.widgetEnabled) { if (!WEBVIEW.widgetEnabled) {
return; return;
} }
@@ -406,7 +522,7 @@ const widgetEvents = () => {
// Hide widget // Hide widget
WEBVIEW.widget.setBounds({ WEBVIEW.widget.setBounds({
x: window.width - 15, x: window.width - 20,
y: widget.y, y: widget.y,
width: widget.width, width: widget.width,
height: widget.height, height: widget.height,
@@ -419,7 +535,6 @@ const widgetEvents = () => {
switch (button.id) { switch (button.id) {
case "keyboard": case "keyboard":
const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON"; const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON";
hardware.setKeyboardVisibility(toggle);
switch (toggle) { switch (toggle) {
case "OFF": case "OFF":
WEBVIEW.window.restore(); WEBVIEW.window.restore();
@@ -432,6 +547,9 @@ const widgetEvents = () => {
WEBVIEW.window.maximize(); WEBVIEW.window.maximize();
break; break;
} }
hardware.setKeyboardVisibility(toggle, () => {
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
});
break; break;
case "fullscreen": case "fullscreen":
if (WEBVIEW.window.isFullScreen()) { if (WEBVIEW.window.isFullScreen()) {
@@ -443,7 +561,9 @@ const widgetEvents = () => {
WEBVIEW.window.unmaximize(); WEBVIEW.window.unmaximize();
WEBVIEW.window.setFullScreen(true); WEBVIEW.window.setFullScreen(true);
} }
hardware.setKeyboardVisibility("OFF"); hardware.setKeyboardVisibility("OFF", () => {
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
});
break; break;
case "minimize": case "minimize":
WEBVIEW.window.restore(); WEBVIEW.window.restore();
@@ -461,7 +581,7 @@ const widgetEvents = () => {
/** /**
* Register navigation events and handler. * Register navigation events and handler.
*/ */
const navigationEvents = () => { const navigationEvents = async () => {
if (!WEBVIEW.navigationEnabled) { if (!WEBVIEW.navigationEnabled) {
return; return;
} }
@@ -519,13 +639,30 @@ const navigationEvents = () => {
// 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];
switch (button.id) { switch (button.id) {
case "pager": case "home":
homeView();
hardware.setKeyboardVisibility("OFF");
break;
case "refresh":
reloadView();
hardware.setKeyboardVisibility("OFF");
break;
case "previous":
previousView();
hardware.setKeyboardVisibility("OFF");
break;
case "next":
nextView(); nextView();
hardware.setKeyboardVisibility("OFF"); hardware.setKeyboardVisibility("OFF");
break; break;
case "home": case "minus":
reloadView(); zoomMinus();
hardware.setKeyboardVisibility("OFF");
break;
case "plus":
zoomPlus();
hardware.setKeyboardVisibility("OFF"); hardware.setKeyboardVisibility("OFF");
break; break;
case "backward": case "backward":
@@ -541,10 +678,11 @@ const navigationEvents = () => {
}; };
/** /**
* Register dom events and handler. * Register view events and handler.
*/ */
const domEvents = () => { const viewEvents = async () => {
WEBVIEW.views.forEach((view) => { const ready = [];
WEBVIEW.views.forEach((view, i) => {
// Enable webview touch emulation // Enable webview touch emulation
view.webContents.debugger.attach("1.1"); view.webContents.debugger.attach("1.1");
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", { view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
@@ -558,15 +696,18 @@ const domEvents = () => {
return { action: "deny" }; return { action: "deny" };
}); });
// Remove webview scrollbar // Update webview layout
view.webContents.on("dom-ready", () => { view.webContents.on("dom-ready", () => {
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }"); view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
if (ready.length < WEBVIEW.views.length) {
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
ready.push(i);
}
}); });
// Webview fully loaded // Webview fully loaded
view.webContents.on("did-finish-load", () => { view.webContents.on("did-finish-load", () => {
WEBVIEW.initialized = true; if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
if (WEBVIEW.viewActive === 0) {
nextView(); nextView();
} }
if (ARGS.app_debug === "true") { if (ARGS.app_debug === "true") {
@@ -579,7 +720,7 @@ const domEvents = () => {
// Webview not loaded // Webview not loaded
view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => { view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => {
if (mainframe) { if (mainframe) {
if (WEBVIEW.viewActive === 0) { if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
nextView(); nextView();
} }
view.webContents.loadURL(errorHtml(code, text, url)); view.webContents.loadURL(errorHtml(code, text, url));
@@ -589,11 +730,37 @@ const domEvents = () => {
// Webview url changed // Webview url changed
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => { view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
if (mainframe) { if (mainframe) {
update(); updateView();
} }
}); });
view.webContents.on("did-navigate", () => { view.webContents.on("did-navigate", () => {
update(); updateView();
});
// Handle webview mouse events
view.webContents.on("before-mouse-event", (e, mouse) => {
const now = new Date();
const then = WEBVIEW.pointer.time;
const delta = Math.abs(now - then) / 1000;
WEBVIEW.pointer.time = now;
switch (mouse.type) {
case "mouseMove":
if (delta > 30) {
console.log("Update Last Active");
integration.update();
}
break;
case "mouseDown":
switch (mouse.button) {
case "back":
historyBackward();
break;
case "forward":
historyForward();
break;
}
break;
}
}); });
}); });
}; };
@@ -601,7 +768,7 @@ const domEvents = () => {
/** /**
* Register app events and handler. * Register app events and handler.
*/ */
const appEvents = () => { const appEvents = async () => {
// Handle signal and exit events // Handle signal and exit events
process.on("SIGINT", app.quit); process.on("SIGINT", app.quit);
app.on("before-quit", () => { app.on("before-quit", () => {
@@ -616,6 +783,69 @@ const appEvents = () => {
} }
WEBVIEW.window.focus(); WEBVIEW.window.focus();
}); });
// Check for latest release infos (2h)
setInterval(() => {
latestRelease();
}, 7200 * 1000);
await latestRelease();
// Webview initialized
WEBVIEW.initialized = true;
};
/**
* Fetches the latest app release infos from github.
*/
const latestRelease = async () => {
try {
const response = await axios.get(`${APP.releases.url}/latest`);
const data = response ? response.data : null;
if (!data || data.draft || data.prerelease) {
return;
}
APP.releases.latest = {
title: APP.title,
version: (data.tag_name || data.name || "").replace(/^v/i, ""),
summary: data.body || "",
url: data.html_url || "",
};
} catch (error) {
console.warn("Github Error:", error.message);
}
};
/**
* Checks for network connectivity by requesting a known url.
*
* @param {string} url - Url to request.
* @param {number} interval - Interval between requests in milliseconds.
* @param {number} timeout - Maximum time to repeat requests in milliseconds.
* @returns {Promise<boolean>} Resolves true if online, false on timeout.
*/
const onlineStatus = (url, interval = 1000, timeout = 60000) => {
return new Promise((resolve) => {
const start = Date.now();
const check = async () => {
const elapsed = Date.now() - start;
try {
if (!url.startsWith("data:")) {
await axios.get(url, { timeout: 10000 });
}
resolve(true);
} catch (error) {
if (elapsed >= interval) {
console.warn(`Checking Connection: ${url}`, error.message);
}
if (elapsed >= timeout) {
resolve(false);
} else {
setTimeout(check, interval);
}
}
};
check();
});
}; };
/** /**
+11 -8
View File
@@ -1,27 +1,30 @@
{ {
"name": "touchkio", "name": "touchkio",
"title": "TouchKio",
"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.2.0", "version": "1.3.0",
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "electron . --web-url=https://demo.home-assistant.io --trace-warnings", "start": "electron . --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app --app-debug=false --trace-warnings",
"build": "electron-forge make" "build": "electron-forge make"
}, },
"prettier": { "prettier": {
"printWidth": 120 "printWidth": 120
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.8.1", "@electron-forge/cli": "^7.8.3",
"@electron-forge/maker-deb": "^7.8.1", "@electron-forge/maker-deb": "^7.8.3",
"@electron-forge/maker-zip": "^7.8.1", "@electron-forge/maker-zip": "^7.8.3",
"@electron-forge/publisher-github": "^7.8.1", "@electron-forge/publisher-github": "^7.8.3",
"electron": "^37.2.0", "electron": "^38.0.0",
"jszip": "^3.10.1",
"prettier": "^3.6.2" "prettier": "^3.6.2"
}, },
"dependencies": { "dependencies": {
"mqtt": "^5.13.2" "axios": "^1.11.0",
"mqtt": "^5.14.1"
} }
} }
+484 -299
View File
File diff suppressed because it is too large Load Diff