Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
092697850d | ||
|
|
422e92dd89 | ||
|
|
a15c0fc91d | ||
|
|
358fa3ca9c | ||
|
|
10722fa217 | ||
|
|
7267fafb97 | ||
|
|
b5e95936dc | ||
|
|
047b38a41b | ||
|
|
a93fbb2ad3 | ||
|
|
0b927a3ef6 | ||
|
|
d35cf27de9 | ||
|
|
19348b4b60 | ||
|
|
ce1eed391d | ||
|
|
fe6fe92656 | ||
|
|
3aea938cfb | ||
|
|
c39428fa42 | ||
|
|
7caa5bfc8a | ||
|
|
5f69961a9f | ||
|
|
f44d998016 | ||
|
|
3ea65a4af8 | ||
|
|
2ad49c3167 | ||
|
|
ef2093a25e | ||
|
|
2737eabd8a | ||
|
|
b8f809c50e | ||
|
|
c3e625a8f6 | ||
|
|
7c15707e3b | ||
|
|
dbd3756793 | ||
|
|
85fe5a910a | ||
|
|
d78088e6af | ||
|
|
bf77d9b2a0 | ||
|
|
f4ca7bc412 | ||
|
|
d0ea377ead | ||
|
|
9a829f72e3 | ||
|
|
482a56c9b9 | ||
|
|
32409983b2 | ||
|
|
fe0667919e | ||
|
|
0037a892cd | ||
|
|
ad22dd3215 | ||
|
|
619179c3c8 | ||
|
|
7e5e669b09 | ||
|
|
437c4ab1c5 |
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
docs/
|
||||
out/
|
||||
build.json
|
||||
|
||||
+16
-14
@@ -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/) | 🟩 |
|
||||
|
||||
### HDMI
|
||||
| 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 | [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 | [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 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 | [Prechen Portable Touch 18.5" FHD (1920x1080)](https://www.amazon.de/dp/B0CT2KLDBQ) | 🟨 |
|
||||
| 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 | Generic Non-Touch | 🟨 |
|
||||
| Generic PC (x64) | Debian KDE (64-bit), X11 | Generic Non-Touch | 🟧 |
|
||||
| Generic PC (x64) | Ubuntu GNOME (64-bit), X11 | Generic Non-Touch | 🟧 |
|
||||
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 |
|
||||
| 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 | [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 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 |
|
||||
| Raspberry Pi 400 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [UPerfect Vertical Touch 15.6" (1920x1080)](https://uperfect.com/products/uperfect-y-vertical-monitor-15-6) | 🟨 |
|
||||
| Raspberry Pi CM 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Lenovo ThinkCentre 23.8" (1920x1080)](https://psref.lenovo.com/Detail/ThinkCentre_Tiny_In_One_24_Gen_5?M=12NBGAT1EU) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [GeeekPi 10.1" Capacitive Touch (1280x800)](https://www.amazon.nl/dp/B0DHV6DZC1) | 🟨 |
|
||||
| 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 | [Akntzcs Portable Touch 16" HD (1920x1200)](https://www.amazon.com/dp/B0CTGW6MQ6) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [ELECROW Portable Monitor 10.1" (1280x800)](https://www.amazon.co.uk/dp/B0BHHQLKPY) | 🟨 |
|
||||
| Raspberry Pi 5 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | 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
|
||||
In case your hardware is not listed above don't worry, give it a try.
|
||||
|
||||
@@ -17,13 +17,14 @@ Moreover, the device running the **kiosk application** also offers several **Hom
|
||||
- [x] Configurable browser zoom and theme support.
|
||||
- [x] Extended touch screen wake-up functionality.
|
||||
- [x] Remote controllable via MQTT.
|
||||
- [x] Manage kiosk window status.
|
||||
- [x] Toggle the on-screen keyboard.
|
||||
- [x] Touch display power and brightness.
|
||||
- [x] Manage kiosk window status and zoom.
|
||||
- [x] Show network interfaces and addresses.
|
||||
- [x] List all available system package upgrades.
|
||||
- [x] Multi-webpage switching and url navigation.
|
||||
- [x] Monitor temperature, processor and memory usage.
|
||||
- [x] Execute system reboot and shutdown commands.
|
||||
- [x] Show available system package upgrades.
|
||||
|
||||
The kiosk application can be executed with command line arguments to load a **Home Assistant dashboard in fullscreen** mode.
|
||||
Additionally, a **MQTT endpoint** can be defined, allowing the application to provide controls and sensors for the Linux device and the connected Touch Display.
|
||||
@@ -43,7 +44,7 @@ For a comprehensive guide on setting up MQTT with Home Assistant, please refer t
|
||||
|
||||
## Installation
|
||||
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.
|
||||
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 +57,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).
|
||||
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.
|
||||
If you need manual control use:
|
||||
@@ -89,6 +89,15 @@ yarn build
|
||||
|
||||
</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
|
||||
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.
|
||||
@@ -109,16 +118,16 @@ For example:
|
||||
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.
|
||||
> You can load multiple pages by separating them with a comma:
|
||||
> `touchkio --web-url=https://demo.home-assistant.io,https://imvbh.github.io/FlipClock`.
|
||||
> <a id="foot1"></a><a href="#ref1">[1]</a>: This doesn't necessarily have to be a Home Assistant Url.
|
||||
> You can configure multiple pages by separating them with a comma:
|
||||
> `touchkio --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app`.
|
||||
|
||||
In the `~/.config/touchkio/Arguments.json` file:
|
||||
```json
|
||||
{
|
||||
"web_url": [
|
||||
"https://demo.home-assistant.io",
|
||||
"https://imvbh.github.io/FlipClock"
|
||||
"https://demo.immichkiosk.app"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -140,6 +149,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
|
||||
```
|
||||
|
||||
## 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
|
||||
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).
|
||||
|
||||
@@ -219,35 +251,45 @@ Implementing stronger **security measures** would complicate the setup process a
|
||||
When using the kiosk application without initializing the default arguments, you will need to provide them with every command.
|
||||
This means that the password may be stored as plain text in various files, such as `touchkio.service`, `~/.bash_history`, etc.
|
||||
|
||||
To resolve the issue where the first **touch** on a **turned-off screen** triggers a **click event** (which could inadvertently activate Home Assistant actions), a workaround needed to be implemented.
|
||||
When the screen **turns off** the **focus** is removed from the kiosk window.
|
||||
This way, the first click only turns the screen on and focuses the window, allowing subsequent clicks to work as expected.
|
||||
To address the problem where the first **touch** on a **turned-off screen** may trigger a **click event** (which could inadvertently activate Home Assistant actions), a workaround needed to be implemented.
|
||||
When the screen **turns on/off**, the timestamps of these events are recorded.
|
||||
If a touch event is detected and the **timestamp** of the **last screen-off** is more recent than the **last screen-on** timestamp, the touch event is discarded and the screen is turned on instead.
|
||||
From that point onward, subsequent touch interactions will function normally as expected.
|
||||
|
||||
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.
|
||||
|
||||
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>
|
||||
|
||||
## Issues
|
||||
Please have a look into the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation if you ran into any issues.
|
||||
Please review the [hardware](https://github.com/leukipp/touchkio/blob/main/HARDWARE.md) documentation first if you encounter any problems.
|
||||
|
||||
**Minimal features** are designed to run on any system without issues:
|
||||
- A webview kiosk window launched in fullscreen mode and loading the specified `--web-url` website should not cause any problems.
|
||||
|
||||
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
|
||||
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
|
||||
- On some Debian based systems (e.g. Ubuntu), 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:
|
||||
- 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.
|
||||
- When using a Raspberry Pi, the on-screen keyboard doesn't automatically pop-out 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.
|
||||
- When using a Raspberry Pi, the on-screen keyboard doesn't [automatically pop-out](https://github.com/leukipp/touchkio/issues/4) when entering a text field inside the webview.
|
||||
- As a current workaround you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the keyboard visibility.
|
||||
- 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.
|
||||
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works.
|
||||
|
||||
For debugging, stop the service and launch `touchkio` directly on the terminal to monitor the log output in real-time.
|
||||
This output is also written into `~/.config/touchkio/logs/main.log` for review.
|
||||
|
||||
If you encounter any problems, please [create an issue](https://github.com/leukipp/touchkio/issues) and include the output of `touchkio --version`, additional information's about your system (such as operating system, hardware specs, etc.) and any relevant log files.
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
+35
-2
@@ -1,4 +1,22 @@
|
||||
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 = {
|
||||
packagerConfig: {
|
||||
@@ -33,10 +51,25 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
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) => {
|
||||
for (const result of results) {
|
||||
const artifacts = [];
|
||||
result.artifacts.forEach((artifact) => {
|
||||
for (const artifact of result.artifacts) {
|
||||
if (artifact.includes(".zip")) {
|
||||
const [name, platform, arch] = path.basename(artifact).split("-");
|
||||
const buildFile = path.join(`${name}-${platform}-${arch}`, "resources", "app", "build.json");
|
||||
const options = { type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 9 } };
|
||||
const zip = await jszip.loadAsync(fs.readFileSync(artifact));
|
||||
zip.file(buildFile, generateBuildFile(platform, arch, "zip"));
|
||||
fs.writeFileSync(artifact, await zip.generateAsync(options));
|
||||
}
|
||||
if (artifact.includes("amd64")) {
|
||||
const renamed = artifact.replace("amd64", "x64");
|
||||
fs.renameSync(artifact, renamed);
|
||||
@@ -44,7 +77,7 @@ module.exports = {
|
||||
} else {
|
||||
artifacts.push(artifact);
|
||||
}
|
||||
});
|
||||
}
|
||||
result.artifacts = artifacts;
|
||||
}
|
||||
},
|
||||
|
||||
+59
-6
@@ -46,11 +46,19 @@
|
||||
li.dynamic {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
li.separator {
|
||||
height: 32px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
li.separator:has(+ li.hidden) {
|
||||
display: none;
|
||||
}
|
||||
li.hidden {
|
||||
display: none;
|
||||
}
|
||||
li.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
button {
|
||||
@@ -62,12 +70,13 @@
|
||||
input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin: 0 10px;
|
||||
margin: 0 5px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 18px;
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: text;
|
||||
box-sizing: border-box;
|
||||
color: var(--primary-color);
|
||||
background-color: var(--border-color);
|
||||
@@ -91,7 +100,28 @@
|
||||
</button>
|
||||
</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">
|
||||
<title>page-next</title>
|
||||
<path
|
||||
@@ -100,15 +130,17 @@
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
<li class="separator"></li>
|
||||
<li class="dynamic">
|
||||
<input id="url" type="text" spellcheck="false" />
|
||||
</li>
|
||||
<li class="separator"></li>
|
||||
<li>
|
||||
<button id="backward">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<title>arrow-left-circle</title>
|
||||
<title>history</title>
|
||||
<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>
|
||||
</button>
|
||||
@@ -116,9 +148,30 @@
|
||||
<li>
|
||||
<button id="forward">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<title>arrow-right-circle</title>
|
||||
<title>update</title>
|
||||
<path
|
||||
d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,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>
|
||||
</button>
|
||||
|
||||
@@ -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>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 78 KiB |
@@ -5,9 +5,13 @@ const readline = require("readline/promises");
|
||||
const integration = require("./js/integration");
|
||||
const hardware = require("./js/hardware");
|
||||
const webview = require("./js/webview");
|
||||
const log = require("electron-log");
|
||||
const { app } = require("electron");
|
||||
const Events = require("events");
|
||||
|
||||
global.APP = global.APP || {};
|
||||
global.ARGS = global.ARGS || {};
|
||||
global.EVENTS = global.EVENTS || new Events();
|
||||
|
||||
if (!process.env.DISPLAY) {
|
||||
console.error(`\n$DISPLAY variable not set to run the GUI application, are you connected via SSH?\n`);
|
||||
@@ -23,20 +27,90 @@ if (!process.env.DISPLAY) {
|
||||
* initialization tasks.
|
||||
*/
|
||||
app.whenReady().then(async () => {
|
||||
const lock = app.requestSingleInstanceLock();
|
||||
if (!lock) {
|
||||
console.error("TouchKio is already running");
|
||||
if (!(await initApp()) || !(await initArgs()) || !(await initLog())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show used arguments
|
||||
const args = Object.assign({}, ARGS);
|
||||
if ("mqtt_password" in args) {
|
||||
args.mqtt_password = "*".repeat((args.mqtt_password || "").length);
|
||||
}
|
||||
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");
|
||||
APP.log = path.join(app.getPath("logs"), "main.log");
|
||||
|
||||
// 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 true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the global args object.
|
||||
*
|
||||
* @returns {bool} Returns true if the initialization was successful.
|
||||
*/
|
||||
const initArgs = async () => {
|
||||
let args = parseArgs(process);
|
||||
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);
|
||||
|
||||
// Show version and release info
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -51,24 +125,44 @@ app.whenReady().then(async () => {
|
||||
args = readArgs(argsFilePath);
|
||||
}
|
||||
|
||||
// Check arguments object
|
||||
if (!Object.keys(args).length) {
|
||||
console.error(`No arguments provided`);
|
||||
return app.quit();
|
||||
}
|
||||
|
||||
// Split url arguments
|
||||
args.web_url = args.web_url || [];
|
||||
if (!Array.isArray(args.web_url)) {
|
||||
args.web_url = args.web_url.split(",").map((url) => url.trim());
|
||||
}
|
||||
global.ARGS = args;
|
||||
ARGS = args;
|
||||
|
||||
// Show used arguments
|
||||
console.log(`Arguments: ${JSON.stringify(global.ARGS, null, 2)}`);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Chained init functions
|
||||
const chained = [webview.init, hardware.init, integration.init];
|
||||
for (const init of chained) {
|
||||
if (!(await init())) {
|
||||
break;
|
||||
/**
|
||||
* Initializes the global log object.
|
||||
*
|
||||
* @returns {bool} Returns true if the initialization was successful.
|
||||
*/
|
||||
const initLog = async () => {
|
||||
try {
|
||||
if (fs.existsSync(APP.log)) {
|
||||
fs.unlinkSync(APP.log);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete log file:", error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Overwrite console log settings
|
||||
log.transports.file.resolvePathFn = () => {
|
||||
return APP.log;
|
||||
};
|
||||
Object.assign(console, log.functions);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses command-line arguments from the given process object.
|
||||
@@ -155,34 +249,41 @@ const promptArgs = async (proc) => {
|
||||
// Prompt questions and wait for the answers
|
||||
let args = {};
|
||||
let ignore = [];
|
||||
for (const { key, question, fallback } of prompts) {
|
||||
if (key === "mqtt") {
|
||||
const prompt = `${question} (${fallback}): `;
|
||||
const answer = await read.question(prompt);
|
||||
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
||||
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);
|
||||
const prompt = `${question}\n${json}\n(${fallback}): `;
|
||||
const answer = await read.question(prompt);
|
||||
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
||||
if (!["y", "yes"].includes(value)) {
|
||||
args = {};
|
||||
}
|
||||
} else if (!ignore.includes(key)) {
|
||||
const prompt = `${question} (${fallback}): `;
|
||||
const answer = await read.question(prompt);
|
||||
const value = answer.trim() || fallback;
|
||||
if (key === "web_url") {
|
||||
args[key] = value.split(",").map((v) => v.trim());
|
||||
} else {
|
||||
args[key] = value;
|
||||
try {
|
||||
for (const { key, question, fallback } of prompts) {
|
||||
if (key === "mqtt") {
|
||||
const prompt = `${question} (${fallback}): `;
|
||||
const answer = await read.question(prompt);
|
||||
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
||||
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);
|
||||
const prompt = `${question}\n${json}\n(${fallback}): `;
|
||||
const answer = await read.question(prompt);
|
||||
const value = (answer.trim() || fallback.match(/[YN]/)[0]).toLowerCase();
|
||||
if (!["y", "yes"].includes(value)) {
|
||||
args = {};
|
||||
}
|
||||
} else if (!ignore.includes(key)) {
|
||||
const prompt = `${question} (${fallback}): `;
|
||||
const answer = await read.question(prompt);
|
||||
const value = answer.trim() || fallback;
|
||||
if (key === "web_url") {
|
||||
args[key] = value.split(",").map((v) => v.trim());
|
||||
} else {
|
||||
args[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`\n${error.message}`);
|
||||
args = {};
|
||||
app.quit();
|
||||
} finally {
|
||||
read.close();
|
||||
}
|
||||
read.close();
|
||||
|
||||
return args;
|
||||
};
|
||||
@@ -194,11 +295,15 @@ const promptArgs = async (proc) => {
|
||||
* @param {Object} args - The arguments object.
|
||||
*/
|
||||
const writeArgs = (path, args) => {
|
||||
const argc = Object.assign({}, args);
|
||||
if ("mqtt_password" in argc) {
|
||||
argc.mqtt_password = encrypt(argc.mqtt_password);
|
||||
try {
|
||||
const argc = Object.assign({}, args);
|
||||
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 +313,16 @@ const writeArgs = (path, args) => {
|
||||
* @returns {Object} The arguments object.
|
||||
*/
|
||||
const readArgs = (path) => {
|
||||
const args = JSON.parse(fs.readFileSync(path));
|
||||
if ("mqtt_password" in args) {
|
||||
args.mqtt_password = decrypt(args.mqtt_password);
|
||||
try {
|
||||
const args = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
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 +333,7 @@ const readArgs = (path) => {
|
||||
*/
|
||||
const encrypt = (value) => {
|
||||
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);
|
||||
let encrypted = cipher.update(value, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
@@ -239,7 +349,7 @@ const encrypt = (value) => {
|
||||
const decrypt = (value) => {
|
||||
const p = Buffer.from(value, "base64").toString("utf8").split(":");
|
||||
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 buffer = Buffer.from(p.join(":"), "hex");
|
||||
let decrypted = decipher.update(buffer, "binary", "utf8");
|
||||
|
||||
+54
-41
@@ -1,5 +1,13 @@
|
||||
#!/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
|
||||
echo -e "Determining system architecture..."
|
||||
|
||||
@@ -12,62 +20,43 @@ case "$(uname -m)" in
|
||||
ARCH="x64"
|
||||
;;
|
||||
*)
|
||||
echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."
|
||||
exit 1
|
||||
{ echo "Architecture $(uname -m) running $BITS-bit operating system is not supported."; exit 1; }
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$BITS" -ne 64 ]; then
|
||||
echo "Architecture $ARCH running $BITS-bit operating system is not supported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ "$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 supported."
|
||||
|
||||
# Download the latest .deb package
|
||||
echo -e "\nDownloading the latest release..."
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
DEB_URL=$(wget -qO- https://api.github.com/repos/leukipp/touchkio/releases/latest | \
|
||||
grep -o "\"browser_download_url\": \"[^\"]*_${ARCH}\.deb\"" | \
|
||||
sed 's/"browser_download_url": "//;s/"//g')
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
DEB_PATH="${TMP_DIR}/$(basename "$DEB_URL")"
|
||||
chmod 755 "$TMP_DIR"
|
||||
|
||||
if [ -z "$DEB_URL" ]; then
|
||||
echo "Download url for .deb file not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! wget --show-progress -q -O "$DEB_PATH" "$DEB_URL"; then
|
||||
echo "Failed to download the .deb file."
|
||||
exit 1
|
||||
fi
|
||||
[ -z "$DEB_URL" ] && { echo "Download url for .deb file not found."; exit 1; }
|
||||
wget --show-progress -q -O "$DEB_PATH" "$DEB_URL" || { echo "Failed to download the .deb file."; exit 1; }
|
||||
|
||||
# Install the latest .deb package
|
||||
echo -e "\nInstalling the latest release..."
|
||||
|
||||
if ! command -v apt &> /dev/null; then
|
||||
echo "Package manager apt was not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! sudo apt install "$DEB_PATH"; then
|
||||
echo "Installation of .deb file failed."
|
||||
exit 1
|
||||
fi
|
||||
command -v apt &> /dev/null || { echo "Package manager apt was not found."; exit 1; }
|
||||
sudo apt install -y "$DEB_PATH" || { echo "Installation of .deb file failed."; exit 1; }
|
||||
|
||||
# Create the systemd user service
|
||||
echo -e "\nCreating systemd user service..."
|
||||
|
||||
SERVICE_FILE="$HOME/.config/systemd/user/touchkio.service"
|
||||
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE"; exit 1; }
|
||||
SERVICE_NAME="touchkio.service"
|
||||
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\"
|
||||
[Unit]
|
||||
SERVICE_CONTENT="[Unit]
|
||||
Description=TouchKio
|
||||
After=graphical.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/touchkio
|
||||
@@ -75,35 +64,59 @@ Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF"
|
||||
WantedBy=default.target"
|
||||
|
||||
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE"; exit 1; }
|
||||
echo "Service $SERVICE_FILE enabled"
|
||||
if $ARG_UPDATE; then
|
||||
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
|
||||
echo -e "\nExporting display variables..."
|
||||
|
||||
if [ -z "$DISPLAY" ]; then
|
||||
export DISPLAY=":0"
|
||||
echo "DISPLAY was not set, defaulting to \"$DISPLAY\""
|
||||
echo "DISPLAY was not set, defaulting to \"$DISPLAY\"."
|
||||
else
|
||||
echo "DISPLAY is set to \"$DISPLAY\""
|
||||
echo "DISPLAY is set to \"$DISPLAY\"."
|
||||
fi
|
||||
|
||||
if [ -z "$WAYLAND_DISPLAY" ]; then
|
||||
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
|
||||
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\""
|
||||
echo "WAYLAND_DISPLAY is set to \"$WAYLAND_DISPLAY\"."
|
||||
fi
|
||||
|
||||
# Start the setup mode
|
||||
echo ""
|
||||
read -p "Start touchkio setup? (Y/n) " answer
|
||||
if [[ ${answer:-y} == [Yy]* ]]; then
|
||||
read -p "Start touchkio setup? (Y/n) " setup
|
||||
|
||||
if [[ ${setup:-y} == [Yy]* ]]; then
|
||||
echo "/usr/bin/touchkio --setup"
|
||||
/usr/bin/touchkio --setup
|
||||
else
|
||||
echo "/usr/bin/touchkio"
|
||||
/usr/bin/touchkio
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
+260
-85
@@ -6,9 +6,8 @@ const cpr = require("child_process");
|
||||
|
||||
global.HARDWARE = global.HARDWARE || {
|
||||
initialized: false,
|
||||
status: null,
|
||||
session: {},
|
||||
support: {},
|
||||
session: {},
|
||||
battery: {
|
||||
level: {
|
||||
path: null,
|
||||
@@ -16,19 +15,18 @@ global.HARDWARE = global.HARDWARE || {
|
||||
},
|
||||
display: {
|
||||
status: {
|
||||
path: null,
|
||||
value: null,
|
||||
path: null,
|
||||
command: null,
|
||||
},
|
||||
brightness: {
|
||||
path: null,
|
||||
value: null,
|
||||
path: null,
|
||||
max: null,
|
||||
},
|
||||
notifiers: [],
|
||||
},
|
||||
keyboard: {
|
||||
visible: null,
|
||||
notifiers: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -38,51 +36,68 @@ global.HARDWARE = global.HARDWARE || {
|
||||
* @returns {bool} Returns true if the initialization was successful.
|
||||
*/
|
||||
const init = async () => {
|
||||
if (!compatibleDevice()) {
|
||||
console.warn("Device not supported");
|
||||
if (!compatibleSystem()) {
|
||||
console.error("Operating system is not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Init globals
|
||||
HARDWARE.session.user = sessionUser();
|
||||
HARDWARE.session.type = sessionType();
|
||||
HARDWARE.session.desktop = sessionDesktop();
|
||||
HARDWARE.battery.level.path = getBatteryLevelPath();
|
||||
HARDWARE.support.batteryLevel = HARDWARE.battery.level.path !== null;
|
||||
HARDWARE.display.status.path = getDisplayStatusPath();
|
||||
HARDWARE.support.displayStatus = HARDWARE.display.status.path !== null;
|
||||
HARDWARE.display.status.command = getDisplayStatusCommand();
|
||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
||||
HARDWARE.support.displayBrightness = HARDWARE.display.brightness.path !== null;
|
||||
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
||||
HARDWARE.support.keyboardVisibility = processRuns("squeekboard");
|
||||
HARDWARE.support = checkSupport();
|
||||
HARDWARE.initialized = true;
|
||||
HARDWARE.status = "Supported";
|
||||
|
||||
// Show supported features
|
||||
process.stdout.write("\n");
|
||||
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
|
||||
|
||||
// Show session infos
|
||||
console.log("\nUser:", HARDWARE.session.user);
|
||||
process.stdout.write("\n");
|
||||
console.log("User:", HARDWARE.session.user);
|
||||
console.log("Session:", HARDWARE.session.type);
|
||||
console.log("Desktop:", HARDWARE.session.desktop);
|
||||
|
||||
// Show device infos
|
||||
console.log("\nModel:", getModel());
|
||||
process.stdout.write("\n");
|
||||
console.log("Model:", getModel());
|
||||
console.log("Vendor:", getVendor());
|
||||
console.log("Serial Number:", getSerialNumber());
|
||||
console.log("Network Addresses:", getNetworkAddresses());
|
||||
console.log("Host Name:", getHostName());
|
||||
|
||||
// Show system infos
|
||||
console.log("\nUp Time:", getUpTime());
|
||||
process.stdout.write("\n");
|
||||
console.log("Up Time:", getUpTime());
|
||||
console.log("Memory Size:", getMemorySize());
|
||||
console.log("Memory Usage:", getMemoryUsage());
|
||||
console.log("Processor Usage:", getProcessorUsage());
|
||||
console.log("Processor Temperature:", getProcessorTemperature());
|
||||
|
||||
// Show hardware infos
|
||||
console.log(`\nBattery Level [${HARDWARE.battery.level.path}]:`, getBatteryLevel());
|
||||
console.log(`Display Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
|
||||
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
|
||||
process.stdout.write("\n");
|
||||
const unsupported = "unsupported";
|
||||
console.log(`Battery 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}`,
|
||||
);
|
||||
|
||||
// Check for keyboard visibility
|
||||
process.stdout.write("\n");
|
||||
setKeyboardVisibility("OFF", (reply, error) => {
|
||||
if (!reply || error) {
|
||||
return;
|
||||
@@ -92,14 +107,14 @@ const init = async () => {
|
||||
return;
|
||||
}
|
||||
HARDWARE.keyboard.visibility = property.Visible === "true";
|
||||
HARDWARE.keyboard.notifiers.forEach((notifier) => {
|
||||
notifier();
|
||||
});
|
||||
EVENTS.emit("updateKeyboard");
|
||||
});
|
||||
});
|
||||
|
||||
// Check for display changes
|
||||
interval(update, 1000);
|
||||
setDisplayStatus("ON", () => {
|
||||
interval(update, 1000);
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -118,9 +133,7 @@ const update = async () => {
|
||||
if (status !== HARDWARE.display.status.value) {
|
||||
console.log("Update Display Status:", getDisplayStatus());
|
||||
HARDWARE.display.status.value = status;
|
||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
||||
notifier();
|
||||
});
|
||||
EVENTS.emit("updateDisplay");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,23 +143,21 @@ const update = async () => {
|
||||
if (brightness !== HARDWARE.display.brightness.value) {
|
||||
console.log("Update Display Brightness:", getDisplayBrightness());
|
||||
HARDWARE.display.brightness.value = brightness;
|
||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
||||
notifier();
|
||||
});
|
||||
EVENTS.emit("updateDisplay");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const compatibleDevice = () => {
|
||||
const compatibleSystem = () => {
|
||||
if (os.platform() !== "linux") {
|
||||
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));
|
||||
};
|
||||
|
||||
@@ -179,12 +190,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) => {
|
||||
return HARDWARE.session.type === type;
|
||||
const sessionDesktop = () => {
|
||||
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 +232,7 @@ const getModel = () => {
|
||||
const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]) || "Generic";
|
||||
return execSyncCommand("cat", [path]) || "Generic";
|
||||
}
|
||||
}
|
||||
return "Generic";
|
||||
@@ -211,7 +247,7 @@ const getVendor = () => {
|
||||
const paths = ["/sys/class/dmi/id/board_vendor"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]) || "Generic";
|
||||
return execSyncCommand("cat", [path]) || "Generic";
|
||||
}
|
||||
}
|
||||
const model = getModel();
|
||||
@@ -222,22 +258,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 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) {
|
||||
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.
|
||||
*/
|
||||
@@ -245,12 +281,38 @@ const getMachineId = () => {
|
||||
const paths = ["/etc/machine-id"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]) || "123456";
|
||||
return execSyncCommand("cat", [path]) || "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()`.
|
||||
*
|
||||
@@ -371,7 +433,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.
|
||||
*/
|
||||
@@ -379,24 +465,34 @@ const getDisplayStatus = () => {
|
||||
if (!HARDWARE.support.displayStatus) {
|
||||
return null;
|
||||
}
|
||||
if (session("wayland") && commandExists("wlopm")) {
|
||||
const status = execSyncCommand("wlopm", []);
|
||||
if (status !== null) {
|
||||
const out = status.split("\n")[0].split(" ");
|
||||
return out.pop().toUpperCase();
|
||||
}
|
||||
} else if (session("x11") && commandExists("xset")) {
|
||||
const status = execSyncCommand("xset", ["-q"]);
|
||||
if (status !== null) {
|
||||
const on = /Monitor is On/.test(status);
|
||||
return on ? "ON" : "OFF";
|
||||
}
|
||||
switch (HARDWARE.display.status.command) {
|
||||
case "wlopm":
|
||||
const wlopm = execSyncCommand("wlopm", []);
|
||||
if (wlopm !== null) {
|
||||
const output = wlopm.split("\n")[0].split(" ");
|
||||
return output.pop().toUpperCase();
|
||||
}
|
||||
break;
|
||||
case "kscreen-doctor":
|
||||
const kdoc = execSyncCommand("kscreen-doctor", ["--dpms", "show"]);
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* the appropriate command to set the display status.
|
||||
@@ -414,10 +510,16 @@ const setDisplayStatus = (status, callback = null) => {
|
||||
if (typeof callback === "function") callback(null, "Invalid status");
|
||||
return;
|
||||
}
|
||||
if (session("wayland") && commandExists("wlopm")) {
|
||||
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
|
||||
} else if (session("x11") && commandExists("xset")) {
|
||||
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
|
||||
switch (HARDWARE.display.status.command) {
|
||||
case "wlopm":
|
||||
execAsyncCommand("wlopm", [`--${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 +546,7 @@ const getDisplayBrightnessPath = () => {
|
||||
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
||||
*/
|
||||
const getDisplayBrightnessMax = () => {
|
||||
if (!HARDWARE.support.displayBrightness) {
|
||||
if (!HARDWARE.display.brightness.path) {
|
||||
return null;
|
||||
}
|
||||
const max = fs.readFileSync(`${HARDWARE.display.brightness.path}/max_brightness`, "utf8").trim();
|
||||
@@ -490,7 +592,7 @@ const setDisplayBrightness = (brightness, callback = null) => {
|
||||
if (typeof callback === "function") callback(null, "Invalid brightness");
|
||||
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 proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
||||
proc.stdin.write(value.toString());
|
||||
@@ -557,6 +659,10 @@ const checkPackageUpgrades = () => {
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const shutdownSystem = (callback = null) => {
|
||||
if (!HARDWARE.support.sudoRights) {
|
||||
if (typeof callback === "function") callback(null, "Not supported");
|
||||
return;
|
||||
}
|
||||
execAsyncCommand("sudo", ["shutdown", "-h", "now"], callback);
|
||||
};
|
||||
|
||||
@@ -569,9 +675,39 @@ const shutdownSystem = (callback = null) => {
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const rebootSystem = (callback = null) => {
|
||||
if (!HARDWARE.support.sudoRights) {
|
||||
if (typeof callback === "function") callback(null, "Not supported");
|
||||
return;
|
||||
}
|
||||
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`.
|
||||
*
|
||||
@@ -648,14 +784,61 @@ const execAsyncCommand = (cmd, args = [], callback = null) => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Execute Async:", error.message);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, error.message);
|
||||
}
|
||||
if (typeof callback === "function") callback(null, error.message);
|
||||
}
|
||||
});
|
||||
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 = 1;
|
||||
let proc = cpr.spawn(cmd, args);
|
||||
if (typeof callback === "function") callback(progress, null);
|
||||
proc.stdout.on("data", (data) => {
|
||||
if (data) {
|
||||
const output = data.toString().trim();
|
||||
const lines = output.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, "");
|
||||
for (const line of lines.split("\n").filter(Boolean)) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
proc.stderr.on("data", (data) => {
|
||||
if (data) {
|
||||
const output = data.toString().trim();
|
||||
const lines = output.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, "");
|
||||
for (const line of lines.split("\n").filter(Boolean)) {
|
||||
const matches = line.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`.
|
||||
*
|
||||
@@ -670,15 +853,11 @@ const dbusCall = (path, method, values, callback = null) => {
|
||||
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
||||
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
||||
try {
|
||||
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
||||
if (typeof callback === "function") {
|
||||
callback(output.trim().replace(/\0/g, ""), null);
|
||||
}
|
||||
const output = cpr.execSync([cmd, ...args].join(" ").trim(), { encoding: "utf8" });
|
||||
if (typeof callback === "function") callback(output.trim().replace(/\0/g, ""), null);
|
||||
} catch (error) {
|
||||
console.error("Call D-Bus:", error.message);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, error.message);
|
||||
}
|
||||
if (typeof callback === "function") callback(null, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -712,17 +891,13 @@ const dbusMonitor = (path, callback) => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Monitor D-Bus:", error.message);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, error.message);
|
||||
}
|
||||
if (typeof callback === "function") callback(null, error.message);
|
||||
}
|
||||
});
|
||||
proc.stderr.on("data", (data) => {
|
||||
if (data) {
|
||||
console.error("Monitor D-Bus:", data.toString());
|
||||
if (typeof callback === "function") {
|
||||
callback(null, data.toString());
|
||||
}
|
||||
if (typeof callback === "function") callback(null, data.toString());
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
@@ -755,19 +930,16 @@ module.exports = {
|
||||
getVendor,
|
||||
getSerialNumber,
|
||||
getMachineId,
|
||||
getNetworkAddresses,
|
||||
getHostName,
|
||||
getUpTime,
|
||||
getMemorySize,
|
||||
getMemoryUsage,
|
||||
getProcessorUsage,
|
||||
getProcessorTemperature,
|
||||
getBatteryLevelPath,
|
||||
getBatteryLevel,
|
||||
getDisplayStatusPath,
|
||||
getDisplayStatus,
|
||||
setDisplayStatus,
|
||||
getDisplayBrightnessPath,
|
||||
getDisplayBrightnessMax,
|
||||
getDisplayBrightness,
|
||||
setDisplayBrightness,
|
||||
getKeyboardVisibility,
|
||||
@@ -775,4 +947,7 @@ module.exports = {
|
||||
checkPackageUpgrades,
|
||||
shutdownSystem,
|
||||
rebootSystem,
|
||||
execSyncCommand,
|
||||
execAsyncCommand,
|
||||
execScriptCommand,
|
||||
};
|
||||
|
||||
+242
-92
@@ -4,7 +4,6 @@ const { app } = require("electron");
|
||||
|
||||
global.INTEGRATION = global.INTEGRATION || {
|
||||
initialized: false,
|
||||
status: null,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,14 +37,15 @@ const init = async () => {
|
||||
// Init globals
|
||||
INTEGRATION.discovery = discovery;
|
||||
INTEGRATION.node = `rpi_${deviceId}`;
|
||||
INTEGRATION.root = `${app.getName()}/${INTEGRATION.node}`;
|
||||
INTEGRATION.root = `${APP.name}/${INTEGRATION.node}`;
|
||||
INTEGRATION.device = {
|
||||
name: `TouchKio ${deviceName}`,
|
||||
name: `${APP.title} ${deviceName}`,
|
||||
model: model,
|
||||
manufacturer: vendor,
|
||||
serial_number: serialNumber,
|
||||
identifiers: [INTEGRATION.node],
|
||||
sw_version: `${app.getName()}-v${app.getVersion()}`,
|
||||
sw_version: `${APP.name}-v${APP.version}`,
|
||||
configuration_url: APP.homepage,
|
||||
};
|
||||
|
||||
// Connection settings
|
||||
@@ -56,10 +56,12 @@ const init = async () => {
|
||||
|
||||
// Client connected
|
||||
INTEGRATION.client
|
||||
.on("connect", () => {
|
||||
console.log(`MQTT Connected: ${url.toString()}\n`);
|
||||
.once("connect", () => {
|
||||
console.log(`MQTT Connected: ${url.toString()}`);
|
||||
process.stdout.write("\n");
|
||||
|
||||
// Init client controls
|
||||
initApp();
|
||||
initShutdown();
|
||||
initReboot();
|
||||
initRefresh();
|
||||
@@ -67,12 +69,14 @@ const init = async () => {
|
||||
initDisplay();
|
||||
initKeyboard();
|
||||
initPageNumber();
|
||||
initPageZoom();
|
||||
initPageUrl();
|
||||
|
||||
// Init client sensors
|
||||
initModel();
|
||||
initSerialNumber();
|
||||
initHostName();
|
||||
initNetworkAddress();
|
||||
initUpTime();
|
||||
initMemorySize();
|
||||
initMemoryUsage();
|
||||
@@ -85,20 +89,14 @@ const init = async () => {
|
||||
|
||||
// Integration initialized
|
||||
INTEGRATION.initialized = true;
|
||||
INTEGRATION.status = "Online";
|
||||
})
|
||||
.on("error", (error) => {
|
||||
console.error("MQTT:", error);
|
||||
INTEGRATION.status = "Offline";
|
||||
console.error("MQTT", error.message);
|
||||
});
|
||||
|
||||
// Update sensor states from notifiers
|
||||
HARDWARE.display.notifiers.push(() => {
|
||||
updateDisplay();
|
||||
});
|
||||
HARDWARE.keyboard.notifiers.push(() => {
|
||||
updateKeyboard();
|
||||
});
|
||||
// Update sensor states from events
|
||||
EVENTS.on("updateDisplay", updateDisplay);
|
||||
EVENTS.on("updateKeyboard", updateKeyboard);
|
||||
|
||||
// Update time sensors periodically (30s)
|
||||
setInterval(() => {
|
||||
@@ -111,8 +109,9 @@ const init = async () => {
|
||||
update();
|
||||
}, 60 * 1000);
|
||||
|
||||
// Update package sensors periodically (60min)
|
||||
// Update upgrade sensors periodically (1h)
|
||||
setInterval(() => {
|
||||
updateApp();
|
||||
updatePackageUpgrades();
|
||||
}, 3600 * 1000);
|
||||
|
||||
@@ -130,7 +129,9 @@ const update = async () => {
|
||||
// Update client sensors
|
||||
updateKiosk();
|
||||
updatePageNumber();
|
||||
updatePageZoom();
|
||||
updatePageUrl();
|
||||
updateNetworkAddress();
|
||||
updateUpTime();
|
||||
updateLastActive();
|
||||
updateMemoryUsage();
|
||||
@@ -185,10 +186,68 @@ const publishState = (path, state) => {
|
||||
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) => {
|
||||
if (progress) {
|
||||
console.log(`Progress: ${progress}%`);
|
||||
}
|
||||
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.
|
||||
*/
|
||||
const initShutdown = () => {
|
||||
if (!HARDWARE.support.sudoRights) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/shutdown`;
|
||||
const config = {
|
||||
name: "Shutdown",
|
||||
@@ -201,8 +260,9 @@ const initShutdown = () => {
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
console.log("Shutdown system...");
|
||||
hardware.setDisplayStatus("ON");
|
||||
hardware.shutdownSystem();
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
hardware.shutdownSystem();
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
@@ -212,6 +272,9 @@ const initShutdown = () => {
|
||||
* Initializes the reboot button and handles the execute logic.
|
||||
*/
|
||||
const initReboot = () => {
|
||||
if (!HARDWARE.support.sudoRights) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/reboot`;
|
||||
const config = {
|
||||
name: "Reboot",
|
||||
@@ -224,8 +287,9 @@ const initReboot = () => {
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
console.log("Rebooting system...");
|
||||
hardware.setDisplayStatus("ON");
|
||||
hardware.rebootSystem();
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
hardware.rebootSystem();
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
@@ -247,8 +311,9 @@ const initRefresh = () => {
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === config.command_topic) {
|
||||
console.log("Refreshing webview...");
|
||||
hardware.setDisplayStatus("ON");
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.reloadIgnoringCache();
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
EVENTS.emit("reloadView");
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
@@ -274,31 +339,32 @@ const initKiosk = () => {
|
||||
if (topic === config.command_topic) {
|
||||
const status = message.toString();
|
||||
console.log("Set Kiosk Status:", status);
|
||||
hardware.setDisplayStatus("ON");
|
||||
switch (status) {
|
||||
case "Framed":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
break;
|
||||
case "Fullscreen":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "Maximized":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
case "Minimized":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.minimize();
|
||||
break;
|
||||
case "Terminated":
|
||||
app.quit();
|
||||
}
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
switch (status) {
|
||||
case "Framed":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
break;
|
||||
case "Fullscreen":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "Maximized":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
case "Minimized":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.minimize();
|
||||
break;
|
||||
case "Terminated":
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
@@ -308,8 +374,8 @@ const initKiosk = () => {
|
||||
/**
|
||||
* Updates the kiosk status via the mqtt connection.
|
||||
*/
|
||||
const updateKiosk = () => {
|
||||
const kiosk = WEBVIEW.status;
|
||||
const updateKiosk = async () => {
|
||||
const kiosk = WEBVIEW.tracker.status;
|
||||
publishState("kiosk", kiosk);
|
||||
};
|
||||
|
||||
@@ -340,13 +406,23 @@ const initDisplay = () => {
|
||||
if (topic === config.command_topic) {
|
||||
const status = message.toString();
|
||||
console.log("Set Display Status:", status);
|
||||
hardware.setDisplayStatus(status);
|
||||
hardware.update();
|
||||
hardware.setDisplayStatus(status, (reply, error) => {
|
||||
if (!error) {
|
||||
hardware.update();
|
||||
} else {
|
||||
console.warn("Failed:", error);
|
||||
}
|
||||
});
|
||||
} else if (topic === config.brightness_command_topic) {
|
||||
const brightness = parseInt(message, 10);
|
||||
console.log("Set Display Brightness:", brightness);
|
||||
hardware.setDisplayBrightness(brightness);
|
||||
hardware.update();
|
||||
hardware.setDisplayBrightness(brightness, (reply, error) => {
|
||||
if (!error) {
|
||||
hardware.update();
|
||||
} else {
|
||||
console.warn("Failed:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic)
|
||||
@@ -357,7 +433,7 @@ const initDisplay = () => {
|
||||
/**
|
||||
* Updates the display status, brightness via the mqtt connection.
|
||||
*/
|
||||
const updateDisplay = () => {
|
||||
const updateDisplay = async () => {
|
||||
const status = hardware.getDisplayStatus();
|
||||
const brightness = hardware.getDisplayBrightness();
|
||||
publishState("display/power", status);
|
||||
@@ -385,19 +461,21 @@ const initKeyboard = () => {
|
||||
if (topic === config.command_topic) {
|
||||
const status = message.toString();
|
||||
console.log("Set Keyboard Visibility:", status);
|
||||
hardware.setKeyboardVisibility(status);
|
||||
switch (status) {
|
||||
case "OFF":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "ON":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
}
|
||||
hardware.setDisplayStatus("ON", () => {
|
||||
switch (status) {
|
||||
case "OFF":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "ON":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
}
|
||||
hardware.setKeyboardVisibility(status);
|
||||
});
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
@@ -407,7 +485,7 @@ const initKeyboard = () => {
|
||||
/**
|
||||
* Updates the keyboard visibility via the mqtt connection.
|
||||
*/
|
||||
const updateKeyboard = () => {
|
||||
const updateKeyboard = async () => {
|
||||
const visibility = hardware.getKeyboardVisibility();
|
||||
publishState("keyboard", visibility);
|
||||
};
|
||||
@@ -416,7 +494,7 @@ const updateKeyboard = () => {
|
||||
* Initializes the page number and handles the execute logic.
|
||||
*/
|
||||
const initPageNumber = () => {
|
||||
if (WEBVIEW.viewUrls.length < 3) {
|
||||
if (WEBVIEW.viewUrls.length <= 2) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.root}/page_number`;
|
||||
@@ -438,7 +516,8 @@ const initPageNumber = () => {
|
||||
if (topic === config.command_topic) {
|
||||
const number = parseInt(message, 10);
|
||||
console.log("Set Page Number:", number);
|
||||
WEBVIEW.viewActive = number;
|
||||
WEBVIEW.viewActive = number || 1;
|
||||
EVENTS.emit("updateView");
|
||||
}
|
||||
})
|
||||
.subscribe(config.command_topic);
|
||||
@@ -448,11 +527,50 @@ const initPageNumber = () => {
|
||||
/**
|
||||
* Updates the page number via the mqtt connection.
|
||||
*/
|
||||
const updatePageNumber = () => {
|
||||
const pageNumber = WEBVIEW.viewActive;
|
||||
const updatePageNumber = async () => {
|
||||
const pageNumber = WEBVIEW.viewActive || 1;
|
||||
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);
|
||||
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.
|
||||
*/
|
||||
@@ -473,7 +591,7 @@ const initPageUrl = () => {
|
||||
if (topic === config.command_topic) {
|
||||
const url = message.toString();
|
||||
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);
|
||||
@@ -483,9 +601,9 @@ const initPageUrl = () => {
|
||||
/**
|
||||
* Updates the page url via the mqtt connection.
|
||||
*/
|
||||
const updatePageUrl = () => {
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL();
|
||||
const updatePageUrl = async () => {
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive || 1];
|
||||
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getURL();
|
||||
const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl;
|
||||
publishState("page_url", pageUrl);
|
||||
};
|
||||
@@ -499,6 +617,7 @@ const initModel = () => {
|
||||
name: "Model",
|
||||
unique_id: `${INTEGRATION.node}_model`,
|
||||
state_topic: `${root}/state`,
|
||||
json_attributes_topic: `${root}/attributes`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:raspberry-pi",
|
||||
device: INTEGRATION.device,
|
||||
@@ -510,9 +629,10 @@ const initModel = () => {
|
||||
/**
|
||||
* Updates the model sensor via the mqtt connection.
|
||||
*/
|
||||
const updateModel = () => {
|
||||
const updateModel = async () => {
|
||||
const model = hardware.getModel();
|
||||
publishState("model", model);
|
||||
publishAttributes("model", HARDWARE.support);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -535,11 +655,41 @@ const initSerialNumber = () => {
|
||||
/**
|
||||
* Updates the serial number sensor via the mqtt connection.
|
||||
*/
|
||||
const updateSerialNumber = () => {
|
||||
const updateSerialNumber = async () => {
|
||||
const serialNumber = hardware.getSerialNumber();
|
||||
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.
|
||||
*/
|
||||
@@ -560,7 +710,7 @@ const initHostName = () => {
|
||||
/**
|
||||
* Updates the host name sensor via the mqtt connection.
|
||||
*/
|
||||
const updateHostName = () => {
|
||||
const updateHostName = async () => {
|
||||
const hostName = hardware.getHostName();
|
||||
publishState("host_name", hostName);
|
||||
};
|
||||
@@ -586,7 +736,7 @@ const initUpTime = () => {
|
||||
/**
|
||||
* Updates the up time sensor via the mqtt connection.
|
||||
*/
|
||||
const updateUpTime = () => {
|
||||
const updateUpTime = async () => {
|
||||
const upTime = hardware.getUpTime();
|
||||
publishState("up_time", upTime);
|
||||
};
|
||||
@@ -612,7 +762,7 @@ const initMemorySize = () => {
|
||||
/**
|
||||
* Updates the memory size sensor via the mqtt connection.
|
||||
*/
|
||||
const updateMemorySize = () => {
|
||||
const updateMemorySize = async () => {
|
||||
const memorySize = hardware.getMemorySize();
|
||||
publishState("memory_size", memorySize);
|
||||
};
|
||||
@@ -638,7 +788,7 @@ const initMemoryUsage = () => {
|
||||
/**
|
||||
* Updates the memory usage sensor via the mqtt connection.
|
||||
*/
|
||||
const updateMemoryUsage = () => {
|
||||
const updateMemoryUsage = async () => {
|
||||
const memoryUsage = hardware.getMemoryUsage();
|
||||
publishState("memory_usage", memoryUsage);
|
||||
};
|
||||
@@ -664,7 +814,7 @@ const initProcessorUsage = () => {
|
||||
/**
|
||||
* Updates the processor usage sensor via the mqtt connection.
|
||||
*/
|
||||
const updateProcessorUsage = () => {
|
||||
const updateProcessorUsage = async () => {
|
||||
const processorUsage = hardware.getProcessorUsage();
|
||||
publishState("processor_usage", processorUsage);
|
||||
};
|
||||
@@ -690,7 +840,7 @@ const initProcessorTemperature = () => {
|
||||
/**
|
||||
* Updates the processor temperature sensor via the mqtt connection.
|
||||
*/
|
||||
const updateProcessorTemperature = () => {
|
||||
const updateProcessorTemperature = async () => {
|
||||
const processorTemperature = hardware.getProcessorTemperature();
|
||||
publishState("processor_temperature", processorTemperature);
|
||||
};
|
||||
@@ -719,7 +869,7 @@ const initBatteryLevel = () => {
|
||||
/**
|
||||
* Updates the battery level sensor via the mqtt connection.
|
||||
*/
|
||||
const updateBatteryLevel = () => {
|
||||
const updateBatteryLevel = async () => {
|
||||
const batteryLevel = hardware.getBatteryLevel();
|
||||
publishState("battery_level", batteryLevel);
|
||||
};
|
||||
@@ -745,14 +895,14 @@ const initPackageUpgrades = () => {
|
||||
/**
|
||||
* Updates the package upgrades sensor via the mqtt connection.
|
||||
*/
|
||||
const updatePackageUpgrades = () => {
|
||||
const updatePackageUpgrades = async () => {
|
||||
const packages = hardware.checkPackageUpgrades();
|
||||
const attributes = {
|
||||
total: packages.length,
|
||||
packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()),
|
||||
};
|
||||
publishAttributes("package_upgrades", attributes);
|
||||
publishState("package_upgrades", attributes.total);
|
||||
publishAttributes("package_upgrades", attributes);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -775,7 +925,7 @@ const initHeartbeat = () => {
|
||||
/**
|
||||
* Updates the heartbeat sensor via the mqtt connection.
|
||||
*/
|
||||
const updateHeartbeat = () => {
|
||||
const updateHeartbeat = async () => {
|
||||
const now = new Date();
|
||||
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
|
||||
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
|
||||
@@ -803,10 +953,10 @@ const initLastActive = () => {
|
||||
/**
|
||||
* Updates the last active sensor via the mqtt connection.
|
||||
*/
|
||||
const updateLastActive = () => {
|
||||
const updateLastActive = async () => {
|
||||
const now = new Date();
|
||||
const then = WEBVIEW.pointer.time;
|
||||
const lastActive = Math.abs(now - then) / (1000 * 60);
|
||||
const then = WEBVIEW.tracker.pointer.time;
|
||||
const lastActive = (now - then) / (1000 * 60);
|
||||
publishState("last_active", lastActive);
|
||||
};
|
||||
|
||||
|
||||
+349
-104
@@ -1,15 +1,18 @@
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const hardware = require("./hardware");
|
||||
const integration = require("./integration");
|
||||
const { app, screen, nativeTheme, ipcMain, session, BaseWindow, WebContentsView } = require("electron");
|
||||
const { app, nativeTheme, globalShortcut, ipcMain, session, BaseWindow, WebContentsView } = require("electron");
|
||||
|
||||
global.WEBVIEW = global.WEBVIEW || {
|
||||
initialized: false,
|
||||
status: null,
|
||||
locked: false,
|
||||
pointer: {
|
||||
position: {},
|
||||
time: new Date(),
|
||||
tracker: {
|
||||
pointer: {
|
||||
position: {},
|
||||
time: new Date(),
|
||||
},
|
||||
display: {},
|
||||
status: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -37,28 +40,21 @@ const init = async () => {
|
||||
const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url);
|
||||
|
||||
// Init global properties
|
||||
WEBVIEW.viewActive = 0;
|
||||
WEBVIEW.viewUrls = urls;
|
||||
WEBVIEW.viewZoom = zoom;
|
||||
WEBVIEW.viewTheme = theme;
|
||||
WEBVIEW.pagerEnabled = widget;
|
||||
WEBVIEW.widgetTheme = theme;
|
||||
WEBVIEW.widgetEnabled = widget;
|
||||
WEBVIEW.navigationTheme = theme;
|
||||
WEBVIEW.navigationEnabled = widget;
|
||||
nativeTheme.themeSource = WEBVIEW.viewTheme;
|
||||
Object.defineProperty(WEBVIEW, "viewActive", {
|
||||
get() {
|
||||
return this._viewActive || 0;
|
||||
},
|
||||
set(v) {
|
||||
this._viewActive = Math.max(0, Math.min(v, WEBVIEW.viewUrls.length - 1));
|
||||
updateView();
|
||||
},
|
||||
});
|
||||
|
||||
// Init global root window
|
||||
WEBVIEW.window = new BaseWindow({
|
||||
title: "TouchKio",
|
||||
icon: path.join(__dirname, "..", "img", "icon.png"),
|
||||
title: APP.title,
|
||||
icon: path.join(APP.path, "img", "icon.png"),
|
||||
});
|
||||
WEBVIEW.window.setMenuBarVisibility(false);
|
||||
WEBVIEW.window.setFullScreen(fullscreen);
|
||||
@@ -78,10 +74,24 @@ const init = async () => {
|
||||
view.setVisible(i === 0);
|
||||
view.setBackgroundColor("#FFFFFFFF");
|
||||
WEBVIEW.window.contentView.addChildView(view);
|
||||
view.webContents.loadURL(url);
|
||||
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
|
||||
WEBVIEW.widget = new WebContentsView({
|
||||
transparent: true,
|
||||
@@ -92,7 +102,7 @@ const init = async () => {
|
||||
});
|
||||
WEBVIEW.widget.setBackgroundColor("#00000000");
|
||||
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
|
||||
WEBVIEW.navigation = new WebContentsView({
|
||||
@@ -104,14 +114,24 @@ const init = async () => {
|
||||
});
|
||||
WEBVIEW.navigation.setBackgroundColor("#00000000");
|
||||
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
|
||||
windowEvents();
|
||||
widgetEvents();
|
||||
navigationEvents();
|
||||
domEvents();
|
||||
appEvents();
|
||||
// Register global events
|
||||
EVENTS.on("reloadView", reloadView);
|
||||
EVENTS.on("updateView", updateView);
|
||||
EVENTS.on("updateDisplay", () => {
|
||||
const status = hardware.getDisplayStatus();
|
||||
if (status) {
|
||||
WEBVIEW.tracker.display[status.toLowerCase()] = new Date();
|
||||
}
|
||||
});
|
||||
|
||||
// Register local events
|
||||
await windowEvents();
|
||||
await widgetEvents();
|
||||
await navigationEvents();
|
||||
await viewEvents();
|
||||
await appEvents();
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -124,15 +144,15 @@ const update = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update windows status
|
||||
// Update window status
|
||||
if (WEBVIEW.window.isFullScreen()) {
|
||||
WEBVIEW.status = "Fullscreen";
|
||||
WEBVIEW.tracker.status = "Fullscreen";
|
||||
} else if (WEBVIEW.window.isMinimized()) {
|
||||
WEBVIEW.status = "Minimized";
|
||||
WEBVIEW.tracker.status = "Minimized";
|
||||
} else if (WEBVIEW.window.isMaximized()) {
|
||||
WEBVIEW.status = "Maximized";
|
||||
WEBVIEW.tracker.status = "Maximized";
|
||||
} else {
|
||||
WEBVIEW.status = "Framed";
|
||||
WEBVIEW.tracker.status = "Framed";
|
||||
}
|
||||
|
||||
// Update widget status
|
||||
@@ -142,7 +162,7 @@ const update = async () => {
|
||||
updateNavigation();
|
||||
|
||||
// Update integration sensor
|
||||
console.log("Update Kiosk Status:", WEBVIEW.status);
|
||||
console.log("Update Kiosk Status:", WEBVIEW.tracker.status);
|
||||
integration.update();
|
||||
};
|
||||
|
||||
@@ -150,8 +170,15 @@ const update = async () => {
|
||||
* Updates the active view.
|
||||
*/
|
||||
const updateView = () => {
|
||||
const title = `TouchKio - ${new URL(WEBVIEW.viewUrls[WEBVIEW.viewActive]).host}`;
|
||||
console.log(`Update View: ${title} (${WEBVIEW.viewActive})`);
|
||||
if (!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);
|
||||
|
||||
// Hide all other webviews and show only the active one
|
||||
@@ -186,17 +213,41 @@ const updateNavigation = () => {
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
const currentUrl = view.webContents.getURL();
|
||||
|
||||
// Hide pager button
|
||||
// Hide pager buttons
|
||||
WEBVIEW.navigation.webContents.send("button-hidden", {
|
||||
id: "pager",
|
||||
hidden: WEBVIEW.viewUrls.length < 3,
|
||||
id: "previous",
|
||||
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
|
||||
WEBVIEW.navigation.webContents.send("input-text", {
|
||||
id: "url",
|
||||
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
|
||||
@@ -225,11 +276,34 @@ const toggleNavigation = () => {
|
||||
width: window.width,
|
||||
height: height,
|
||||
});
|
||||
if (height > 0) {
|
||||
WEBVIEW.navigation.webContents.focus();
|
||||
} else {
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
||||
}
|
||||
|
||||
// Resize webview
|
||||
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.
|
||||
*/
|
||||
@@ -251,16 +325,25 @@ const historyForward = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Activates the next webview or cycles back to the first.
|
||||
* Activates the previous webview page.
|
||||
*/
|
||||
const nextView = () => {
|
||||
WEBVIEW.viewActive = (WEBVIEW.viewActive + 1) % WEBVIEW.views.length || 1;
|
||||
const previousView = () => {
|
||||
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 defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
const currentUrl = view.webContents.getURL();
|
||||
@@ -271,6 +354,30 @@ const reloadView = () => {
|
||||
} else {
|
||||
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 +386,7 @@ const reloadView = () => {
|
||||
const resizeView = () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const navigation = WEBVIEW.navigation.getBounds();
|
||||
const pager = { width: 20, height: window.height };
|
||||
const widget = { width: 60, height: 200 };
|
||||
|
||||
// Update view size
|
||||
@@ -289,13 +397,23 @@ const resizeView = () => {
|
||||
width: window.width,
|
||||
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
|
||||
if (WEBVIEW.widgetEnabled) {
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 15,
|
||||
x: window.width - 20,
|
||||
y: parseInt(window.height / 2 - widget.height / 2, 10),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
@@ -318,27 +436,12 @@ const resizeView = () => {
|
||||
/**
|
||||
* Register window events and handler.
|
||||
*/
|
||||
const windowEvents = () => {
|
||||
const windowEvents = async () => {
|
||||
// Handle window resize events
|
||||
WEBVIEW.window.on("ready-to-show", resizeView);
|
||||
WEBVIEW.window.on("resize", resizeView);
|
||||
resizeView();
|
||||
|
||||
// Handle window focus events
|
||||
WEBVIEW.window.on("focus", () => {
|
||||
if (WEBVIEW.locked) {
|
||||
hardware.setDisplayStatus("ON");
|
||||
WEBVIEW.window.blur();
|
||||
}
|
||||
WEBVIEW.locked = false;
|
||||
});
|
||||
HARDWARE.display.notifiers.push(() => {
|
||||
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
||||
if (WEBVIEW.locked) {
|
||||
WEBVIEW.window.blur();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window status updates
|
||||
WEBVIEW.window.on("minimize", update);
|
||||
WEBVIEW.window.on("restore", update);
|
||||
@@ -347,39 +450,46 @@ const windowEvents = () => {
|
||||
WEBVIEW.window.on("enter-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(() => {
|
||||
const now = new Date();
|
||||
const then = WEBVIEW.pointer.time;
|
||||
const delta = Math.abs(now - then) / 1000;
|
||||
const posNew = screen.getCursorScreenPoint();
|
||||
const posOld = WEBVIEW.pointer.position;
|
||||
const then = WEBVIEW.tracker.pointer.time;
|
||||
const delta = (now - then) / 1000;
|
||||
|
||||
// Cursor movement detected
|
||||
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
||||
WEBVIEW.pointer.time = now;
|
||||
|
||||
// Update integration sensor
|
||||
if (delta > 30) {
|
||||
console.log("Update Last Active");
|
||||
integration.update();
|
||||
}
|
||||
} else if (delta > 60) {
|
||||
// Auto-hide navigation
|
||||
if (delta > 60) {
|
||||
const navigation = WEBVIEW.navigation.getBounds();
|
||||
|
||||
// Auto-hide navigation
|
||||
if (navigation.height > 0) {
|
||||
toggleNavigation();
|
||||
}
|
||||
}
|
||||
WEBVIEW.pointer.position = posNew;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register widget events and handler.
|
||||
*/
|
||||
const widgetEvents = () => {
|
||||
const widgetEvents = async () => {
|
||||
if (!WEBVIEW.widgetEnabled) {
|
||||
return;
|
||||
}
|
||||
@@ -406,7 +516,7 @@ const widgetEvents = () => {
|
||||
|
||||
// Hide widget
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 15,
|
||||
x: window.width - 20,
|
||||
y: widget.y,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
@@ -419,7 +529,6 @@ const widgetEvents = () => {
|
||||
switch (button.id) {
|
||||
case "keyboard":
|
||||
const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON";
|
||||
hardware.setKeyboardVisibility(toggle);
|
||||
switch (toggle) {
|
||||
case "OFF":
|
||||
WEBVIEW.window.restore();
|
||||
@@ -432,6 +541,9 @@ const widgetEvents = () => {
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
}
|
||||
hardware.setKeyboardVisibility(toggle, () => {
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
||||
});
|
||||
break;
|
||||
case "fullscreen":
|
||||
if (WEBVIEW.window.isFullScreen()) {
|
||||
@@ -443,7 +555,9 @@ const widgetEvents = () => {
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
}
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
hardware.setKeyboardVisibility("OFF", () => {
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
||||
});
|
||||
break;
|
||||
case "minimize":
|
||||
WEBVIEW.window.restore();
|
||||
@@ -461,7 +575,7 @@ const widgetEvents = () => {
|
||||
/**
|
||||
* Register navigation events and handler.
|
||||
*/
|
||||
const navigationEvents = () => {
|
||||
const navigationEvents = async () => {
|
||||
if (!WEBVIEW.navigationEnabled) {
|
||||
return;
|
||||
}
|
||||
@@ -519,13 +633,30 @@ const navigationEvents = () => {
|
||||
|
||||
// Handle navigation button click events
|
||||
ipcMain.on("button-click", (e, button) => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
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();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "home":
|
||||
reloadView();
|
||||
case "minus":
|
||||
zoomMinus();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "plus":
|
||||
zoomPlus();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "backward":
|
||||
@@ -541,10 +672,11 @@ const navigationEvents = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Register dom events and handler.
|
||||
* Register view events and handler.
|
||||
*/
|
||||
const domEvents = () => {
|
||||
WEBVIEW.views.forEach((view) => {
|
||||
const viewEvents = async () => {
|
||||
const ready = [];
|
||||
WEBVIEW.views.forEach((view, i) => {
|
||||
// Enable webview touch emulation
|
||||
view.webContents.debugger.attach("1.1");
|
||||
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
||||
@@ -558,15 +690,18 @@ const domEvents = () => {
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// Remove webview scrollbar
|
||||
// Update webview layout
|
||||
view.webContents.on("dom-ready", () => {
|
||||
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
||||
if (ready.length < WEBVIEW.views.length) {
|
||||
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
||||
ready.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
// Webview fully loaded
|
||||
view.webContents.on("did-finish-load", () => {
|
||||
WEBVIEW.initialized = true;
|
||||
if (WEBVIEW.viewActive === 0) {
|
||||
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
|
||||
nextView();
|
||||
}
|
||||
if (ARGS.app_debug === "true") {
|
||||
@@ -579,7 +714,7 @@ const domEvents = () => {
|
||||
// Webview not loaded
|
||||
view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => {
|
||||
if (mainframe) {
|
||||
if (WEBVIEW.viewActive === 0) {
|
||||
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
|
||||
nextView();
|
||||
}
|
||||
view.webContents.loadURL(errorHtml(code, text, url));
|
||||
@@ -589,11 +724,57 @@ const domEvents = () => {
|
||||
// Webview url changed
|
||||
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
|
||||
if (mainframe) {
|
||||
update();
|
||||
updateView();
|
||||
}
|
||||
});
|
||||
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.tracker.pointer.time;
|
||||
const delta = (now - then) / 1000;
|
||||
switch (mouse.type) {
|
||||
case "mouseMove":
|
||||
const posNew = { x: mouse.globalX, y: mouse.globalY };
|
||||
if (posNew.x < 0 || posNew.y < 0) {
|
||||
break;
|
||||
}
|
||||
// Update last active on pointer position change
|
||||
const posOld = WEBVIEW.tracker.pointer.position;
|
||||
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
||||
WEBVIEW.tracker.pointer.time = now;
|
||||
WEBVIEW.tracker.pointer.position = posNew;
|
||||
if (delta > 30) {
|
||||
console.log("Update Last Active");
|
||||
integration.update();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "mouseDown":
|
||||
switch (mouse.button) {
|
||||
case "left":
|
||||
// Ignore touch event if display was off
|
||||
if (WEBVIEW.tracker.display.off > WEBVIEW.tracker.display.on) {
|
||||
console.log("Display Touch Event: Ignored");
|
||||
e.preventDefault();
|
||||
}
|
||||
// Turn display on if it was off
|
||||
if (hardware.getDisplayStatus() === "OFF") {
|
||||
hardware.setDisplayStatus("ON");
|
||||
}
|
||||
break;
|
||||
case "back":
|
||||
historyBackward();
|
||||
break;
|
||||
case "forward":
|
||||
historyForward();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -601,14 +782,7 @@ const domEvents = () => {
|
||||
/**
|
||||
* Register app events and handler.
|
||||
*/
|
||||
const appEvents = () => {
|
||||
// Handle signal and exit events
|
||||
process.on("SIGINT", app.quit);
|
||||
app.on("before-quit", () => {
|
||||
WEBVIEW.status = "Terminated";
|
||||
integration.update();
|
||||
});
|
||||
|
||||
const appEvents = async () => {
|
||||
// Handle multiple instances
|
||||
app.on("second-instance", () => {
|
||||
if (WEBVIEW.window.isMinimized()) {
|
||||
@@ -616,6 +790,77 @@ const appEvents = () => {
|
||||
}
|
||||
WEBVIEW.window.focus();
|
||||
});
|
||||
|
||||
// Handle signal and exit events
|
||||
app.on("before-quit", () => {
|
||||
WEBVIEW.tracker.status = "Terminated";
|
||||
console.warn(`${APP.title} Terminated`);
|
||||
integration.update();
|
||||
});
|
||||
process.on("SIGINT", app.quit);
|
||||
|
||||
// Webview initialized
|
||||
WEBVIEW.initialized = true;
|
||||
|
||||
// Check for latest release infos (2h)
|
||||
setInterval(() => {
|
||||
latestRelease();
|
||||
}, 7200 * 1000);
|
||||
await latestRelease();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the latest app release infos from github.
|
||||
*/
|
||||
const latestRelease = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${APP.releases.url}/latest`, { timeout: 10000 });
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+12
-8
@@ -1,27 +1,31 @@
|
||||
{
|
||||
"name": "touchkio",
|
||||
"title": "TouchKio",
|
||||
"description": "Home Assistant Touch Kiosk",
|
||||
"homepage": "https://github.com/leukipp/touchkio",
|
||||
"author": "leukipp",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.1",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"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"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 120
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.8.1",
|
||||
"@electron-forge/maker-deb": "^7.8.1",
|
||||
"@electron-forge/maker-zip": "^7.8.1",
|
||||
"@electron-forge/publisher-github": "^7.8.1",
|
||||
"electron": "^37.2.0",
|
||||
"@electron-forge/cli": "^7.8.3",
|
||||
"@electron-forge/maker-deb": "^7.8.3",
|
||||
"@electron-forge/maker-zip": "^7.8.3",
|
||||
"@electron-forge/publisher-github": "^7.8.3",
|
||||
"electron": "^38.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"prettier": "^3.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"mqtt": "^5.13.2"
|
||||
"axios": "^1.11.0",
|
||||
"electron-log": "^5.4.3",
|
||||
"mqtt": "^5.14.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user