Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8f809c50e | ||
|
|
c3e625a8f6 | ||
|
|
7c15707e3b | ||
|
|
dbd3756793 | ||
|
|
85fe5a910a | ||
|
|
d78088e6af | ||
|
|
bf77d9b2a0 | ||
|
|
f4ca7bc412 | ||
|
|
d0ea377ead | ||
|
|
9a829f72e3 | ||
|
|
482a56c9b9 | ||
|
|
32409983b2 | ||
|
|
fe0667919e | ||
|
|
0037a892cd | ||
|
|
ad22dd3215 | ||
|
|
619179c3c8 | ||
|
|
7e5e669b09 | ||
|
|
437c4ab1c5 |
+4
-2
@@ -28,17 +28,19 @@ If you are running Linux with a graphical user interface (Wayland or X11), you s
|
|||||||
|
|
||||||
### HDMI
|
### HDMI
|
||||||
| Device | System | Display | Status |
|
| Device | System | Display | Status |
|
||||||
| ------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------ |
|
| ------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------ |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN 5" Touch Display (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [LAFVIN 5" Touch Display (800x480)](https://www.amazon.de/gp/product/B0BWJ8YP7S) | 🟨 |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 8" CAPLCD (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 8" CAPLCD (768x1024)](https://www.waveshare.com/wiki/8inch_768x1024_LCD) | 🟨 |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 7" 7EP-CAPLCD (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Waveshare 7" 7EP-CAPLCD (1280x800)](https://www.waveshare.com/7ep-caplcd.htm) | 🟨 |
|
||||||
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 |
|
| Raspberry Pi 4 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [Hannspree HT225 21.5" (1920x1080)](https://www.hannspree.eu/product/HT-225-HPB) | 🟨 |
|
||||||
| Raspberry Pi 400 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [UPerfect Vertical Touch 15.6" (1920x1080)](https://uperfect.com/products/uperfect-y-vertical-monitor-15-6) | 🟨 |
|
| Raspberry Pi 400 (arm64) | Raspberry Pi OS (64-bit), Wayland, X11 | [UPerfect Vertical Touch 15.6" (1920x1080)](https://uperfect.com/products/uperfect-y-vertical-monitor-15-6) | 🟨 |
|
||||||
|
| Raspberry Pi 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 | [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 | [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 | [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 | 🟨 |
|
| 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) | 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), X11 | Generic Non-Touch | 🟧 |
|
||||||
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 |
|
| Generic PC (x64) | Ubuntu GNOME (64-bit), Wayland | Generic Non-Touch | 🟥 |
|
||||||
|
|
||||||
|
|||||||
@@ -109,16 +109,16 @@ For example:
|
|||||||
touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0
|
touchkio --web-url=http://192.168.1.42:8123 --web-theme=light --web-zoom=1.0
|
||||||
```
|
```
|
||||||
|
|
||||||
> <a href="#ref1">↩ </a><a id="foot1"></a>**[1]:** Not necessarily a Home Assistant Url.
|
> <a id="foot1"></a><a href="#ref1">[1]</a>: This doesn't necessarily have to be a Home Assistant Url.
|
||||||
> You can load multiple pages by separating them with a comma:
|
> You can configure multiple pages by separating them with a comma:
|
||||||
> `touchkio --web-url=https://demo.home-assistant.io,https://imvbh.github.io/FlipClock`.
|
> `touchkio --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app`.
|
||||||
|
|
||||||
In the `~/.config/touchkio/Arguments.json` file:
|
In the `~/.config/touchkio/Arguments.json` file:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"web_url": [
|
"web_url": [
|
||||||
"https://demo.home-assistant.io",
|
"https://demo.home-assistant.io",
|
||||||
"https://imvbh.github.io/FlipClock"
|
"https://demo.immichkiosk.app"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -140,6 +140,29 @@ For example:
|
|||||||
touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883 --mqtt-user=kiosk --mqtt-password=password
|
touchkio --web-url=http://192.168.1.42:8123 --mqtt-url=mqtt://192.168.1.42:1883 --mqtt-user=kiosk --mqtt-password=password
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## User Interface
|
||||||
|
TouchKio provides a simple webview window designed specifically for Touch Displays. Electron apps are known to be resource intensive due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
|
||||||
|
|
||||||
|
### 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 back in history |
|
||||||
|
| `Alt+Right`/`Mouse+Forward` | Navigate forward in history |
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
To create your own local build, you first need to install [Node.js](https://pimylifeup.com/raspberry-pi-nodejs) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install).
|
To create your own local build, you first need to install [Node.js](https://pimylifeup.com/raspberry-pi-nodejs) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/install).
|
||||||
|
|
||||||
@@ -226,8 +249,6 @@ This way, the first click only turns the screen on and focuses the window, allow
|
|||||||
Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
|
Additionally, to address the problem that scrolling only works with the **web browser scrollbar** on the right, the application is configured to **simulate a touch device** using `Emulation.setEmitTouchEventsForMouse`.
|
||||||
This adjustment provides a user experience similar to that of a proper mobile device.
|
This adjustment provides a user experience similar to that of a proper mobile device.
|
||||||
|
|
||||||
Electron apps are known to be **resource intensive** due to their architecture and the inclusion of a full web browser environment. If you just run the kiosk application without other heavy loads, everything should run smoothly.
|
|
||||||
|
|
||||||
</div></details>
|
</div></details>
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
@@ -238,13 +259,19 @@ Please have a look into the [hardware](https://github.com/leukipp/touchkio/blob/
|
|||||||
|
|
||||||
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
|
**Extended features** become available when the `--mqtt-*` arguments are provided and the hardware is supported:
|
||||||
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
|
- If your hardware is not fully compatible there should be no crashes, but you may miss some sensors.
|
||||||
- On some Debian based systems (e.g. Ubuntu), the display status control may only be available when using X11.
|
- On some Debian based systems (e.g. Ubuntu GNOME), the display status control may only be available when using X11.
|
||||||
|
- The following commands are currently implemented to modify the display status.
|
||||||
|
- `wlopm --[on]/[off] *` (Raspberry Pi OS, Wayland)
|
||||||
|
- `kscreen-doctor --dpms [on]/[off]` (Debian KDE, Wayland)
|
||||||
|
- `xset dpms force [on]/[off]` (Generic, X11)
|
||||||
|
|
||||||
**Known Issues** that are by-design or for which there isn't a solution so far:
|
**Known Issues** that are by-design or for which there isn't a solution so far:
|
||||||
- You can use Raspberry Pi's build-in screen blanking functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
|
- You can use Raspberry Pi's build-in [screen blanking](https://www.raspberrypi.com/documentation/computers/configuration.html#screen-blanking-3) functionality, however, if the screen is turned on through Home Assistant after being automatically turned off, it will remain on indefinitely.
|
||||||
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant automation (e.g. presence detection) to manage the screen status.
|
- It's recommended to either use the built-in screen blanking feature or implement a Home Assistant automation (e.g. presence detection) to manage the screen status.
|
||||||
- When using a Raspberry Pi, the on-screen keyboard doesn't automatically pop-out when entering a text field inside the webview.
|
- When using a Raspberry Pi, the on-screen keyboard doesn't automatically pop-out when entering a text field inside the webview.
|
||||||
- As a current [workaround](https://github.com/leukipp/touchkio/issues/4) you can use the side [widget](https://github.com/leukipp/touchkio/issues/16) to toggle the keyboard visibility.
|
- As a current [workaround](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.
|
||||||
|
- Certain commands from the MQTT integration may require elevated privileges to work correctly.
|
||||||
|
- Ensure that your local user has the [necessary permissions](https://github.com/leukipp/touchkio/issues/39) to run `sudo` without being prompted for a password.
|
||||||
- On the terminal you may see some *ERROR:gbm_wrapper.cc* messages.
|
- On the terminal you may see some *ERROR:gbm_wrapper.cc* messages.
|
||||||
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works.
|
- This appears to be a [known issue](https://github.com/electron/electron/issues/42322) that currently lacks a fix, but the webview still works.
|
||||||
|
|
||||||
|
|||||||
+59
-6
@@ -46,11 +46,19 @@
|
|||||||
li.dynamic {
|
li.dynamic {
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
}
|
}
|
||||||
|
li.separator {
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
li.separator:has(+ li.hidden) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
li.hidden {
|
li.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
li.disabled {
|
li.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
@@ -62,12 +70,13 @@
|
|||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0 10px;
|
margin: 0 5px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
cursor: text;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
background-color: var(--border-color);
|
background-color: var(--border-color);
|
||||||
@@ -91,7 +100,28 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button id="pager">
|
<button id="refresh">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>refresh-circle</title>
|
||||||
|
<path
|
||||||
|
d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2M18 11H13L14.81 9.19A3.94 3.94 0 0 0 12 8A4 4 0 1 0 15.86 13H17.91A6 6 0 1 1 12 6A5.91 5.91 0 0 1 16.22 7.78L18 6Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
|
<li>
|
||||||
|
<button id="previous">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>page-previous</title>
|
||||||
|
<path
|
||||||
|
d="M4,21H19A2,2 0 0,0 21,19V13H17V15L13,12L17,9V11H21V5A2,2 0 0,0 19,3H4A2,2 0 0,0 2,5V19A2,2 0 0,0 4,21M4,15H8V17H4V15M4,11H11V13H4V11M4,7H11V9H4V7M21,11H24V13H21V11Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="next">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<title>page-next</title>
|
<title>page-next</title>
|
||||||
<path
|
<path
|
||||||
@@ -100,15 +130,17 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
<li class="dynamic">
|
<li class="dynamic">
|
||||||
<input id="url" type="text" spellcheck="false" />
|
<input id="url" type="text" spellcheck="false" />
|
||||||
</li>
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
<li>
|
<li>
|
||||||
<button id="backward">
|
<button id="backward">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<title>arrow-left-circle</title>
|
<title>history</title>
|
||||||
<path
|
<path
|
||||||
d="M2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12M18,11H10L13.5,7.5L12.08,6.08L6.16,12L12.08,17.92L13.5,16.5L10,13H18V11Z"
|
d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -116,9 +148,30 @@
|
|||||||
<li>
|
<li>
|
||||||
<button id="forward">
|
<button id="forward">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<title>arrow-right-circle</title>
|
<title>update</title>
|
||||||
<path
|
<path
|
||||||
d="M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M6,13H14L10.5,16.5L11.92,17.92L17.84,12L11.92,6.08L10.5,7.5L14,11H6V13Z"
|
d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="separator"></li>
|
||||||
|
<li>
|
||||||
|
<button id="minus">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>magnify-minus</title>
|
||||||
|
<path
|
||||||
|
d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M5,8V10H13V8H5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="plus">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<title>magnify-plus</title>
|
||||||
|
<path
|
||||||
|
d="M9,2A7,7 0 0,1 16,9C16,10.57 15.5,12 14.61,13.19L15.41,14H16L22,20L20,22L14,16V15.41L13.19,14.61C12,15.5 10.57,16 9,16A7,7 0 0,1 2,9A7,7 0 0,1 9,2M8,5V8H5V10H8V13H10V10H13V8H10V5H8Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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: 71 KiB |
@@ -59,7 +59,7 @@ app.whenReady().then(async () => {
|
|||||||
global.ARGS = args;
|
global.ARGS = args;
|
||||||
|
|
||||||
// Show used arguments
|
// Show used arguments
|
||||||
console.log(`Arguments: ${JSON.stringify(global.ARGS, null, 2)}`);
|
console.log(`Arguments: ${JSON.stringify(global.ARGS, null, 2)}\n`);
|
||||||
|
|
||||||
// Chained init functions
|
// Chained init functions
|
||||||
const chained = [webview.init, hardware.init, integration.init];
|
const chained = [webview.init, hardware.init, integration.init];
|
||||||
|
|||||||
+18
-7
@@ -63,11 +63,10 @@ echo -e "\nCreating systemd user service..."
|
|||||||
|
|
||||||
SERVICE_FILE="$HOME/.config/systemd/user/touchkio.service"
|
SERVICE_FILE="$HOME/.config/systemd/user/touchkio.service"
|
||||||
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE"; exit 1; }
|
mkdir -p "$(dirname "$SERVICE_FILE")" || { echo "Failed to create directory for $SERVICE_FILE"; exit 1; }
|
||||||
|
SERVICE_CONTENT="[Unit]
|
||||||
bash -c "cat << EOF > \"$SERVICE_FILE\"
|
|
||||||
[Unit]
|
|
||||||
Description=TouchKio
|
Description=TouchKio
|
||||||
After=graphical.target
|
After=graphical.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/touchkio
|
ExecStart=/usr/bin/touchkio
|
||||||
@@ -75,11 +74,23 @@ Restart=on-failure
|
|||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target"
|
||||||
EOF"
|
|
||||||
|
|
||||||
|
SERVICE_CREATE=true
|
||||||
|
if [ -f "$SERVICE_FILE" ]; then
|
||||||
|
read -p "Service $SERVICE_FILE exists, overwrite? (y/N) " overwrite
|
||||||
|
if [[ ! ${overwrite:-n} == [Yy]* ]]; then
|
||||||
|
SERVICE_CREATE=false
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SERVICE_CREATE" = true ]; 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; }
|
systemctl --user enable "$(basename "$SERVICE_FILE")" || { echo "Failed to enable service $SERVICE_FILE"; exit 1; }
|
||||||
echo "Service $SERVICE_FILE enabled"
|
echo "Service $SERVICE_FILE enabled"
|
||||||
|
else
|
||||||
|
echo "Service $SERVICE_FILE not created"
|
||||||
|
fi
|
||||||
|
|
||||||
# Export display variables
|
# Export display variables
|
||||||
echo -e "\nExporting display variables..."
|
echo -e "\nExporting display variables..."
|
||||||
@@ -99,8 +110,8 @@ fi
|
|||||||
|
|
||||||
# Start the setup mode
|
# Start the setup mode
|
||||||
echo ""
|
echo ""
|
||||||
read -p "Start touchkio setup? (Y/n) " answer
|
read -p "Start touchkio setup? (Y/n) " setup
|
||||||
if [[ ${answer:-y} == [Yy]* ]]; then
|
if [[ ${setup:-y} == [Yy]* ]]; then
|
||||||
echo "/usr/bin/touchkio --setup"
|
echo "/usr/bin/touchkio --setup"
|
||||||
/usr/bin/touchkio --setup
|
/usr/bin/touchkio --setup
|
||||||
else
|
else
|
||||||
|
|||||||
+131
-51
@@ -3,10 +3,11 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fsp = require("fs/promises");
|
const fsp = require("fs/promises");
|
||||||
const cpr = require("child_process");
|
const cpr = require("child_process");
|
||||||
|
const Events = require("events");
|
||||||
|
|
||||||
global.HARDWARE = global.HARDWARE || {
|
global.HARDWARE = global.HARDWARE || {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
status: null,
|
events: new Events(),
|
||||||
session: {},
|
session: {},
|
||||||
support: {},
|
support: {},
|
||||||
battery: {
|
battery: {
|
||||||
@@ -16,19 +17,18 @@ global.HARDWARE = global.HARDWARE || {
|
|||||||
},
|
},
|
||||||
display: {
|
display: {
|
||||||
status: {
|
status: {
|
||||||
path: null,
|
|
||||||
value: null,
|
value: null,
|
||||||
|
path: null,
|
||||||
|
command: null,
|
||||||
},
|
},
|
||||||
brightness: {
|
brightness: {
|
||||||
path: null,
|
|
||||||
value: null,
|
value: null,
|
||||||
|
path: null,
|
||||||
max: null,
|
max: null,
|
||||||
},
|
},
|
||||||
notifiers: [],
|
|
||||||
},
|
},
|
||||||
keyboard: {
|
keyboard: {
|
||||||
visible: null,
|
visible: null,
|
||||||
notifiers: [],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,24 +38,26 @@ global.HARDWARE = global.HARDWARE || {
|
|||||||
* @returns {bool} Returns true if the initialization was successful.
|
* @returns {bool} Returns true if the initialization was successful.
|
||||||
*/
|
*/
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
if (!compatibleDevice()) {
|
if (!compatibleSystem()) {
|
||||||
console.warn("Device not supported");
|
console.warn("Operating system is not supported\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!sessionRights()) {
|
||||||
|
console.warn(`User "${sessionUser()}" is missing password-less sudo rights\n`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init globals
|
// Init globals
|
||||||
HARDWARE.session.user = sessionUser();
|
HARDWARE.session.user = sessionUser();
|
||||||
HARDWARE.session.type = sessionType();
|
HARDWARE.session.type = sessionType();
|
||||||
|
HARDWARE.session.desktop = sessionDesktop();
|
||||||
HARDWARE.battery.level.path = getBatteryLevelPath();
|
HARDWARE.battery.level.path = getBatteryLevelPath();
|
||||||
HARDWARE.support.batteryLevel = HARDWARE.battery.level.path !== null;
|
|
||||||
HARDWARE.display.status.path = getDisplayStatusPath();
|
HARDWARE.display.status.path = getDisplayStatusPath();
|
||||||
HARDWARE.support.displayStatus = HARDWARE.display.status.path !== null;
|
HARDWARE.display.status.command = getDisplayStatusCommand();
|
||||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
||||||
HARDWARE.support.displayBrightness = HARDWARE.display.brightness.path !== null;
|
|
||||||
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
||||||
HARDWARE.support.keyboardVisibility = processRuns("squeekboard");
|
HARDWARE.support = checkSupport();
|
||||||
HARDWARE.initialized = true;
|
HARDWARE.initialized = true;
|
||||||
HARDWARE.status = "Supported";
|
|
||||||
|
|
||||||
// Show supported features
|
// Show supported features
|
||||||
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
|
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
|
||||||
@@ -63,6 +65,7 @@ const init = async () => {
|
|||||||
// Show session infos
|
// Show session infos
|
||||||
console.log("\nUser:", HARDWARE.session.user);
|
console.log("\nUser:", HARDWARE.session.user);
|
||||||
console.log("Session:", HARDWARE.session.type);
|
console.log("Session:", HARDWARE.session.type);
|
||||||
|
console.log("Desktop:", HARDWARE.session.desktop);
|
||||||
|
|
||||||
// Show device infos
|
// Show device infos
|
||||||
console.log("\nModel:", getModel());
|
console.log("\nModel:", getModel());
|
||||||
@@ -78,9 +81,23 @@ const init = async () => {
|
|||||||
console.log("Processor Temperature:", getProcessorTemperature());
|
console.log("Processor Temperature:", getProcessorTemperature());
|
||||||
|
|
||||||
// Show hardware infos
|
// Show hardware infos
|
||||||
console.log(`\nBattery Level [${HARDWARE.battery.level.path}]:`, getBatteryLevel());
|
const unsupported = "unsupported";
|
||||||
console.log(`Display Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
|
console.log(
|
||||||
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
|
`\nBattery Level [${HARDWARE.battery.level.path || unsupported}]:`,
|
||||||
|
`${getBatteryLevel() || unsupported}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Display Status [${HARDWARE.display.status.path || unsupported}]:`,
|
||||||
|
`${getDisplayStatus() || unsupported} (${HARDWARE.display.status.command || unsupported})`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`,
|
||||||
|
`${getDisplayBrightness() || unsupported}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`,
|
||||||
|
`${getKeyboardVisibility() || unsupported}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
// Check for keyboard visibility
|
// Check for keyboard visibility
|
||||||
setKeyboardVisibility("OFF", (reply, error) => {
|
setKeyboardVisibility("OFF", (reply, error) => {
|
||||||
@@ -92,9 +109,7 @@ const init = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
HARDWARE.keyboard.visibility = property.Visible === "true";
|
HARDWARE.keyboard.visibility = property.Visible === "true";
|
||||||
HARDWARE.keyboard.notifiers.forEach((notifier) => {
|
HARDWARE.events.emit("updateKeyboard");
|
||||||
notifier();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,9 +133,7 @@ const update = async () => {
|
|||||||
if (status !== HARDWARE.display.status.value) {
|
if (status !== HARDWARE.display.status.value) {
|
||||||
console.log("Update Display Status:", getDisplayStatus());
|
console.log("Update Display Status:", getDisplayStatus());
|
||||||
HARDWARE.display.status.value = status;
|
HARDWARE.display.status.value = status;
|
||||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
HARDWARE.events.emit("updateDisplay");
|
||||||
notifier();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,26 +143,37 @@ const update = async () => {
|
|||||||
if (brightness !== HARDWARE.display.brightness.value) {
|
if (brightness !== HARDWARE.display.brightness.value) {
|
||||||
console.log("Update Display Brightness:", getDisplayBrightness());
|
console.log("Update Display Brightness:", getDisplayBrightness());
|
||||||
HARDWARE.display.brightness.value = brightness;
|
HARDWARE.display.brightness.value = brightness;
|
||||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
HARDWARE.events.emit("updateDisplay");
|
||||||
notifier();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies device compatibility by checking the presence of necessary sys paths.
|
* Verifies system compatibility by checking the presence of necessary sys paths.
|
||||||
*
|
*
|
||||||
* @returns {bool} Returns true if all paths exists.
|
* @returns {bool} Returns true if all paths exists.
|
||||||
*/
|
*/
|
||||||
const compatibleDevice = () => {
|
const compatibleSystem = () => {
|
||||||
if (os.platform() !== "linux") {
|
if (os.platform() !== "linux") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/thermal"];
|
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/power_supply", "/sys/class/thermal"];
|
||||||
return paths.every((path) => fs.existsSync(path));
|
return paths.every((path) => fs.existsSync(path));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies user rights to run sudo commands without a password.
|
||||||
|
*
|
||||||
|
* @returns {bool} Returns true if password-less sudo rights exists.
|
||||||
|
*/
|
||||||
|
const sessionRights = () => {
|
||||||
|
if (!commandExists("sudo")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const result = execSyncCommand("sudo", ["-n", "true"]);
|
||||||
|
return result !== null && !result.includes("password is required");
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the session user name using `os.userInfo()`.
|
* Gets the session user name using `os.userInfo()`.
|
||||||
*
|
*
|
||||||
@@ -179,12 +203,32 @@ const sessionType = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the session type for the logged in user.
|
* Gets the desktop environment name by checking environment variables.
|
||||||
*
|
*
|
||||||
* @returns {bool} Returns true if the session matches the type.
|
* @returns {string} Returns desktop environment name or 'unknown' if not detected.
|
||||||
*/
|
*/
|
||||||
const session = (type) => {
|
const sessionDesktop = () => {
|
||||||
return HARDWARE.session.type === type;
|
const envs = ["XDG_CURRENT_DESKTOP", "XDG_DESKTOP_SESSION", "DESKTOP_SESSION"];
|
||||||
|
const names = envs.map((env) => process.env[env]).filter(Boolean);
|
||||||
|
return (names.join(":") || "unknown").toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks supported features based on global properties.
|
||||||
|
*
|
||||||
|
* @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");
|
||||||
|
return {
|
||||||
|
batteryLevel: battery.path !== null,
|
||||||
|
displayStatus: status.path !== null && status.command !== null,
|
||||||
|
displayBrightness: brightness.path !== null && brightness.max !== null,
|
||||||
|
keyboardVisibility: keyboard === true,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -371,7 +415,31 @@ const getDisplayStatusPath = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current display power status using `wlopm` or `xset`.
|
* Gets the available display status command checking for `wlopm`, `kscreen-doctor` and `xset`.
|
||||||
|
*
|
||||||
|
* @returns {string|null} The display status command or null if nothing was found.
|
||||||
|
*/
|
||||||
|
const getDisplayStatusCommand = () => {
|
||||||
|
const type = HARDWARE.session.type;
|
||||||
|
const desktop = HARDWARE.session.desktop;
|
||||||
|
const mapping = {
|
||||||
|
wayland: [
|
||||||
|
{ command: "wlopm", desktops: ["labwc", "wayfire", "unknown"] },
|
||||||
|
{ command: "kscreen-doctor", desktops: ["kde", "plasma", "unknown"] },
|
||||||
|
],
|
||||||
|
x11: [{ command: "xset", desktops: ["*"] }],
|
||||||
|
}[type];
|
||||||
|
for (const map of mapping || []) {
|
||||||
|
if (commandExists(map.command) && map.desktops.some((d) => d === "*" || desktop.includes(d))) {
|
||||||
|
return map.command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.warn("No display command available");
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current display power status using the available command.
|
||||||
*
|
*
|
||||||
* @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
|
* @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
@@ -379,24 +447,34 @@ const getDisplayStatus = () => {
|
|||||||
if (!HARDWARE.support.displayStatus) {
|
if (!HARDWARE.support.displayStatus) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (session("wayland") && commandExists("wlopm")) {
|
switch (HARDWARE.display.status.command) {
|
||||||
const status = execSyncCommand("wlopm", []);
|
case "wlopm":
|
||||||
if (status !== null) {
|
const wlopm = execSyncCommand("wlopm", []);
|
||||||
const out = status.split("\n")[0].split(" ");
|
if (wlopm !== null) {
|
||||||
|
const out = wlopm.split("\n")[0].split(" ");
|
||||||
return out.pop().toUpperCase();
|
return out.pop().toUpperCase();
|
||||||
}
|
}
|
||||||
} else if (session("x11") && commandExists("xset")) {
|
break;
|
||||||
const status = execSyncCommand("xset", ["-q"]);
|
case "kscreen-doctor":
|
||||||
if (status !== null) {
|
const kdoc = execSyncCommand("kscreen-doctor", ["--dpms", "show"]);
|
||||||
const on = /Monitor is On/.test(status);
|
if (kdoc !== null) {
|
||||||
|
const out = kdoc.split("\n")[0].split(" ");
|
||||||
|
return out.pop().toUpperCase();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "xset":
|
||||||
|
const xset = execSyncCommand("xset", ["-q"]);
|
||||||
|
if (xset !== null) {
|
||||||
|
const on = /Monitor is On/.test(xset);
|
||||||
return on ? "ON" : "OFF";
|
return on ? "ON" : "OFF";
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the display power status using `wlopm` or `xset`.
|
* Sets the display power status using the available command.
|
||||||
*
|
*
|
||||||
* This function takes a desired status ('ON' or 'OFF') and executes
|
* This function takes a desired status ('ON' or 'OFF') and executes
|
||||||
* the appropriate command to set the display status.
|
* the appropriate command to set the display status.
|
||||||
@@ -414,10 +492,16 @@ const setDisplayStatus = (status, callback = null) => {
|
|||||||
if (typeof callback === "function") callback(null, "Invalid status");
|
if (typeof callback === "function") callback(null, "Invalid status");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (session("wayland") && commandExists("wlopm")) {
|
switch (HARDWARE.display.status.command) {
|
||||||
|
case "wlopm":
|
||||||
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
|
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
|
||||||
} else if (session("x11") && commandExists("xset")) {
|
break;
|
||||||
|
case "kscreen-doctor":
|
||||||
|
execAsyncCommand("kscreen-doctor", ["--dpms", status.toLowerCase()], callback);
|
||||||
|
break;
|
||||||
|
case "xset":
|
||||||
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
|
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -444,7 +528,7 @@ const getDisplayBrightnessPath = () => {
|
|||||||
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
const getDisplayBrightnessMax = () => {
|
const getDisplayBrightnessMax = () => {
|
||||||
if (!HARDWARE.support.displayBrightness) {
|
if (!HARDWARE.display.brightness.path) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const max = fs.readFileSync(`${HARDWARE.display.brightness.path}/max_brightness`, "utf8").trim();
|
const max = fs.readFileSync(`${HARDWARE.display.brightness.path}/max_brightness`, "utf8").trim();
|
||||||
@@ -490,7 +574,7 @@ const setDisplayBrightness = (brightness, callback = null) => {
|
|||||||
if (typeof callback === "function") callback(null, "Invalid brightness");
|
if (typeof callback === "function") callback(null, "Invalid brightness");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const max = HARDWARE.display.brightness.max;
|
const max = HARDWARE.display.brightness.max || 1;
|
||||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
||||||
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
||||||
proc.stdin.write(value.toString());
|
proc.stdin.write(value.toString());
|
||||||
@@ -670,9 +754,9 @@ const dbusCall = (path, method, values, callback = null) => {
|
|||||||
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
||||||
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
||||||
try {
|
try {
|
||||||
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
const output = cpr.execSync([cmd, ...args].join(" ").trim(), { encoding: "utf8" });
|
||||||
if (typeof callback === "function") {
|
if (typeof callback === "function") {
|
||||||
callback(output.trim().replace(/\0/g, ""), null);
|
callback(output.trim().replace(/\0/g, "").split("\n").slice(1).join("\n").trim(), null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Call D-Bus:", error.message);
|
console.error("Call D-Bus:", error.message);
|
||||||
@@ -761,13 +845,9 @@ module.exports = {
|
|||||||
getMemoryUsage,
|
getMemoryUsage,
|
||||||
getProcessorUsage,
|
getProcessorUsage,
|
||||||
getProcessorTemperature,
|
getProcessorTemperature,
|
||||||
getBatteryLevelPath,
|
|
||||||
getBatteryLevel,
|
getBatteryLevel,
|
||||||
getDisplayStatusPath,
|
|
||||||
getDisplayStatus,
|
getDisplayStatus,
|
||||||
setDisplayStatus,
|
setDisplayStatus,
|
||||||
getDisplayBrightnessPath,
|
|
||||||
getDisplayBrightnessMax,
|
|
||||||
getDisplayBrightness,
|
getDisplayBrightness,
|
||||||
setDisplayBrightness,
|
setDisplayBrightness,
|
||||||
getKeyboardVisibility,
|
getKeyboardVisibility,
|
||||||
|
|||||||
+67
-20
@@ -1,10 +1,11 @@
|
|||||||
const mqtt = require("mqtt");
|
const mqtt = require("mqtt");
|
||||||
const hardware = require("./hardware");
|
const hardware = require("./hardware");
|
||||||
const { app } = require("electron");
|
const { app } = require("electron");
|
||||||
|
const Events = require("events");
|
||||||
|
|
||||||
global.INTEGRATION = global.INTEGRATION || {
|
global.INTEGRATION = global.INTEGRATION || {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
status: null,
|
events: new Events(),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,7 +57,7 @@ const init = async () => {
|
|||||||
|
|
||||||
// Client connected
|
// Client connected
|
||||||
INTEGRATION.client
|
INTEGRATION.client
|
||||||
.on("connect", () => {
|
.once("connect", () => {
|
||||||
console.log(`MQTT Connected: ${url.toString()}\n`);
|
console.log(`MQTT Connected: ${url.toString()}\n`);
|
||||||
|
|
||||||
// Init client controls
|
// Init client controls
|
||||||
@@ -67,6 +68,7 @@ const init = async () => {
|
|||||||
initDisplay();
|
initDisplay();
|
||||||
initKeyboard();
|
initKeyboard();
|
||||||
initPageNumber();
|
initPageNumber();
|
||||||
|
initPageZoom();
|
||||||
initPageUrl();
|
initPageUrl();
|
||||||
|
|
||||||
// Init client sensors
|
// Init client sensors
|
||||||
@@ -85,20 +87,14 @@ const init = async () => {
|
|||||||
|
|
||||||
// Integration initialized
|
// Integration initialized
|
||||||
INTEGRATION.initialized = true;
|
INTEGRATION.initialized = true;
|
||||||
INTEGRATION.status = "Online";
|
|
||||||
})
|
})
|
||||||
.on("error", (error) => {
|
.on("error", (error) => {
|
||||||
console.error("MQTT:", error);
|
console.error("MQTT", error.message);
|
||||||
INTEGRATION.status = "Offline";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update sensor states from notifiers
|
// Update sensor states from events
|
||||||
HARDWARE.display.notifiers.push(() => {
|
HARDWARE.events.on("updateDisplay", updateDisplay);
|
||||||
updateDisplay();
|
HARDWARE.events.on("updateKeyboard", updateKeyboard);
|
||||||
});
|
|
||||||
HARDWARE.keyboard.notifiers.push(() => {
|
|
||||||
updateKeyboard();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update time sensors periodically (30s)
|
// Update time sensors periodically (30s)
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@@ -130,6 +126,7 @@ const update = async () => {
|
|||||||
// Update client sensors
|
// Update client sensors
|
||||||
updateKiosk();
|
updateKiosk();
|
||||||
updatePageNumber();
|
updatePageNumber();
|
||||||
|
updatePageZoom();
|
||||||
updatePageUrl();
|
updatePageUrl();
|
||||||
updateUpTime();
|
updateUpTime();
|
||||||
updateLastActive();
|
updateLastActive();
|
||||||
@@ -248,7 +245,7 @@ const initRefresh = () => {
|
|||||||
if (topic === config.command_topic) {
|
if (topic === config.command_topic) {
|
||||||
console.log("Refreshing webview...");
|
console.log("Refreshing webview...");
|
||||||
hardware.setDisplayStatus("ON");
|
hardware.setDisplayStatus("ON");
|
||||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.reloadIgnoringCache();
|
WEBVIEW.events.emit("reloadView");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribe(config.command_topic);
|
.subscribe(config.command_topic);
|
||||||
@@ -340,14 +337,24 @@ const initDisplay = () => {
|
|||||||
if (topic === config.command_topic) {
|
if (topic === config.command_topic) {
|
||||||
const status = message.toString();
|
const status = message.toString();
|
||||||
console.log("Set Display Status:", status);
|
console.log("Set Display Status:", status);
|
||||||
hardware.setDisplayStatus(status);
|
hardware.setDisplayStatus(status, (reply, error) => {
|
||||||
|
if (error) {
|
||||||
|
console.warn("Failed:", error);
|
||||||
|
} else {
|
||||||
hardware.update();
|
hardware.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (topic === config.brightness_command_topic) {
|
} else if (topic === config.brightness_command_topic) {
|
||||||
const brightness = parseInt(message, 10);
|
const brightness = parseInt(message, 10);
|
||||||
console.log("Set Display Brightness:", brightness);
|
console.log("Set Display Brightness:", brightness);
|
||||||
hardware.setDisplayBrightness(brightness);
|
hardware.setDisplayBrightness(brightness, (reply, error) => {
|
||||||
|
if (error) {
|
||||||
|
console.warn("Failed:", error);
|
||||||
|
} else {
|
||||||
hardware.update();
|
hardware.update();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.subscribe(config.command_topic)
|
.subscribe(config.command_topic)
|
||||||
.subscribe(config.brightness_command_topic);
|
.subscribe(config.brightness_command_topic);
|
||||||
@@ -438,7 +445,8 @@ const initPageNumber = () => {
|
|||||||
if (topic === config.command_topic) {
|
if (topic === config.command_topic) {
|
||||||
const number = parseInt(message, 10);
|
const number = parseInt(message, 10);
|
||||||
console.log("Set Page Number:", number);
|
console.log("Set Page Number:", number);
|
||||||
WEBVIEW.viewActive = number;
|
WEBVIEW.viewActive = number || 1;
|
||||||
|
WEBVIEW.events.emit("updateView");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribe(config.command_topic);
|
.subscribe(config.command_topic);
|
||||||
@@ -449,10 +457,49 @@ const initPageNumber = () => {
|
|||||||
* Updates the page number via the mqtt connection.
|
* Updates the page number via the mqtt connection.
|
||||||
*/
|
*/
|
||||||
const updatePageNumber = () => {
|
const updatePageNumber = () => {
|
||||||
const pageNumber = WEBVIEW.viewActive;
|
const pageNumber = WEBVIEW.viewActive || 1;
|
||||||
publishState("page_number", pageNumber);
|
publishState("page_number", pageNumber);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the page zoom and handles the execute logic.
|
||||||
|
*/
|
||||||
|
const initPageZoom = () => {
|
||||||
|
const root = `${INTEGRATION.root}/page_zoom`;
|
||||||
|
const config = {
|
||||||
|
name: "Page Zoom",
|
||||||
|
unique_id: `${INTEGRATION.node}_page_zoom`,
|
||||||
|
command_topic: `${root}/set`,
|
||||||
|
state_topic: `${root}/state`,
|
||||||
|
value_template: "{{ value | int }}",
|
||||||
|
mode: "slider",
|
||||||
|
min: 25,
|
||||||
|
max: 400,
|
||||||
|
unit_of_measurement: "%",
|
||||||
|
icon: "mdi:magnify-plus",
|
||||||
|
device: INTEGRATION.device,
|
||||||
|
};
|
||||||
|
publishConfig("number", config)
|
||||||
|
.on("message", (topic, message) => {
|
||||||
|
if (topic === config.command_topic) {
|
||||||
|
const zoom = parseInt(message, 10);
|
||||||
|
console.log("Set Page Zoom:", zoom);
|
||||||
|
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.setZoomFactor(zoom / 100.0);
|
||||||
|
WEBVIEW.events.emit("updateView");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.subscribe(config.command_topic);
|
||||||
|
updatePageZoom();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the page zoom via the mqtt connection.
|
||||||
|
*/
|
||||||
|
const updatePageZoom = () => {
|
||||||
|
const pageZoom = Math.round(WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getZoomFactor() * 100.0);
|
||||||
|
publishState("page_zoom", pageZoom);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the page url and handles the execute logic.
|
* Initializes the page url and handles the execute logic.
|
||||||
*/
|
*/
|
||||||
@@ -473,7 +520,7 @@ const initPageUrl = () => {
|
|||||||
if (topic === config.command_topic) {
|
if (topic === config.command_topic) {
|
||||||
const url = message.toString();
|
const url = message.toString();
|
||||||
console.log("Set Page Url:", url);
|
console.log("Set Page Url:", url);
|
||||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.loadURL(url);
|
WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.loadURL(url);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.subscribe(config.command_topic);
|
.subscribe(config.command_topic);
|
||||||
@@ -484,8 +531,8 @@ const initPageUrl = () => {
|
|||||||
* Updates the page url via the mqtt connection.
|
* Updates the page url via the mqtt connection.
|
||||||
*/
|
*/
|
||||||
const updatePageUrl = () => {
|
const updatePageUrl = () => {
|
||||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive || 1];
|
||||||
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL();
|
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.getURL();
|
||||||
const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl;
|
const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl;
|
||||||
publishState("page_url", pageUrl);
|
publishState("page_url", pageUrl);
|
||||||
};
|
};
|
||||||
|
|||||||
+261
-58
@@ -1,14 +1,16 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const axios = require("axios");
|
||||||
const hardware = require("./hardware");
|
const hardware = require("./hardware");
|
||||||
const integration = require("./integration");
|
const integration = require("./integration");
|
||||||
const { app, screen, nativeTheme, ipcMain, session, BaseWindow, WebContentsView } = require("electron");
|
const { app, nativeTheme, globalShortcut, ipcMain, session, BaseWindow, WebContentsView } = require("electron");
|
||||||
|
const Events = require("events");
|
||||||
|
|
||||||
global.WEBVIEW = global.WEBVIEW || {
|
global.WEBVIEW = global.WEBVIEW || {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
|
events: new Events(),
|
||||||
status: null,
|
status: null,
|
||||||
locked: false,
|
locked: false,
|
||||||
pointer: {
|
pointer: {
|
||||||
position: {},
|
|
||||||
time: new Date(),
|
time: new Date(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -37,23 +39,18 @@ const init = async () => {
|
|||||||
const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url);
|
const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url);
|
||||||
|
|
||||||
// Init global properties
|
// Init global properties
|
||||||
|
WEBVIEW.viewActive = 0;
|
||||||
WEBVIEW.viewUrls = urls;
|
WEBVIEW.viewUrls = urls;
|
||||||
WEBVIEW.viewZoom = zoom;
|
WEBVIEW.viewZoom = zoom;
|
||||||
WEBVIEW.viewTheme = theme;
|
WEBVIEW.viewTheme = theme;
|
||||||
|
WEBVIEW.pagerEnabled = widget;
|
||||||
WEBVIEW.widgetTheme = theme;
|
WEBVIEW.widgetTheme = theme;
|
||||||
WEBVIEW.widgetEnabled = widget;
|
WEBVIEW.widgetEnabled = widget;
|
||||||
WEBVIEW.navigationTheme = theme;
|
WEBVIEW.navigationTheme = theme;
|
||||||
WEBVIEW.navigationEnabled = widget;
|
WEBVIEW.navigationEnabled = widget;
|
||||||
nativeTheme.themeSource = WEBVIEW.viewTheme;
|
nativeTheme.themeSource = WEBVIEW.viewTheme;
|
||||||
Object.defineProperty(WEBVIEW, "viewActive", {
|
WEBVIEW.events.on("reloadView", reloadView);
|
||||||
get() {
|
WEBVIEW.events.on("updateView", updateView);
|
||||||
return this._viewActive || 0;
|
|
||||||
},
|
|
||||||
set(v) {
|
|
||||||
this._viewActive = Math.max(0, Math.min(v, WEBVIEW.viewUrls.length - 1));
|
|
||||||
updateView();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Init global root window
|
// Init global root window
|
||||||
WEBVIEW.window = new BaseWindow({
|
WEBVIEW.window = new BaseWindow({
|
||||||
@@ -78,9 +75,23 @@ const init = async () => {
|
|||||||
view.setVisible(i === 0);
|
view.setVisible(i === 0);
|
||||||
view.setBackgroundColor("#FFFFFFFF");
|
view.setBackgroundColor("#FFFFFFFF");
|
||||||
WEBVIEW.window.contentView.addChildView(view);
|
WEBVIEW.window.contentView.addChildView(view);
|
||||||
view.webContents.loadURL(url);
|
|
||||||
WEBVIEW.views.push(view);
|
WEBVIEW.views.push(view);
|
||||||
|
onlineStatus(url).then(() => {
|
||||||
|
view.webContents.loadURL(url);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Init global pager
|
||||||
|
WEBVIEW.pager = new WebContentsView({
|
||||||
|
transparent: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
WEBVIEW.pager.setBackgroundColor("#00000000");
|
||||||
|
WEBVIEW.window.contentView.addChildView(WEBVIEW.pager);
|
||||||
|
WEBVIEW.pager.webContents.loadFile(path.join(app.getAppPath(), "html", "pager.html"));
|
||||||
|
|
||||||
// Init global widget
|
// Init global widget
|
||||||
WEBVIEW.widget = new WebContentsView({
|
WEBVIEW.widget = new WebContentsView({
|
||||||
@@ -110,7 +121,7 @@ const init = async () => {
|
|||||||
windowEvents();
|
windowEvents();
|
||||||
widgetEvents();
|
widgetEvents();
|
||||||
navigationEvents();
|
navigationEvents();
|
||||||
domEvents();
|
viewEvents();
|
||||||
appEvents();
|
appEvents();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -150,8 +161,15 @@ const update = async () => {
|
|||||||
* Updates the active view.
|
* Updates the active view.
|
||||||
*/
|
*/
|
||||||
const updateView = () => {
|
const updateView = () => {
|
||||||
const title = `TouchKio - ${new URL(WEBVIEW.viewUrls[WEBVIEW.viewActive]).host}`;
|
if (!WEBVIEW.viewActive) {
|
||||||
console.log(`Update View: ${title} (${WEBVIEW.viewActive})`);
|
return;
|
||||||
|
}
|
||||||
|
const url = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL();
|
||||||
|
const host = url.startsWith("data:") ? "whoopsie" : new URL(url).host;
|
||||||
|
const title = `TouchKio - ${host} (${WEBVIEW.viewActive})`;
|
||||||
|
|
||||||
|
// Update window title
|
||||||
|
console.log(`Update View: ${title}`);
|
||||||
WEBVIEW.window.setTitle(title);
|
WEBVIEW.window.setTitle(title);
|
||||||
|
|
||||||
// Hide all other webviews and show only the active one
|
// Hide all other webviews and show only the active one
|
||||||
@@ -186,17 +204,41 @@ const updateNavigation = () => {
|
|||||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||||
const currentUrl = view.webContents.getURL();
|
const currentUrl = view.webContents.getURL();
|
||||||
|
|
||||||
// Hide pager button
|
// Hide pager buttons
|
||||||
WEBVIEW.navigation.webContents.send("button-hidden", {
|
WEBVIEW.navigation.webContents.send("button-hidden", {
|
||||||
id: "pager",
|
id: "previous",
|
||||||
hidden: WEBVIEW.viewUrls.length < 3,
|
hidden: WEBVIEW.viewUrls.length <= 2,
|
||||||
|
});
|
||||||
|
WEBVIEW.navigation.webContents.send("button-hidden", {
|
||||||
|
id: "next",
|
||||||
|
hidden: WEBVIEW.viewUrls.length <= 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable pager buttons
|
||||||
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||||
|
id: "previous",
|
||||||
|
disabled: WEBVIEW.viewActive <= 1,
|
||||||
|
});
|
||||||
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||||
|
id: "next",
|
||||||
|
disabled: WEBVIEW.viewActive >= WEBVIEW.viewUrls.length - 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update url text
|
// Update url text
|
||||||
WEBVIEW.navigation.webContents.send("input-text", {
|
WEBVIEW.navigation.webContents.send("input-text", {
|
||||||
id: "url",
|
id: "url",
|
||||||
text: currentUrl.startsWith("data:") ? "" : currentUrl,
|
text: currentUrl.startsWith("data:") ? "" : currentUrl,
|
||||||
placeholder: defaultUrl,
|
placeholder: defaultUrl.startsWith("data:") ? "" : defaultUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable zoom buttons
|
||||||
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||||
|
id: "minus",
|
||||||
|
disabled: view.webContents.getZoomFactor().toFixed(2) <= 0.25,
|
||||||
|
});
|
||||||
|
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||||
|
id: "plus",
|
||||||
|
disabled: view.webContents.getZoomFactor().toFixed(2) >= 4.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable history buttons
|
// Disable history buttons
|
||||||
@@ -225,11 +267,34 @@ const toggleNavigation = () => {
|
|||||||
width: window.width,
|
width: window.width,
|
||||||
height: height,
|
height: height,
|
||||||
});
|
});
|
||||||
|
if (height > 0) {
|
||||||
|
WEBVIEW.navigation.webContents.focus();
|
||||||
|
} else {
|
||||||
|
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
||||||
|
}
|
||||||
|
|
||||||
// Resize webview
|
// Resize webview
|
||||||
resizeView();
|
resizeView();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decreases page zoom on the active webview.
|
||||||
|
*/
|
||||||
|
const zoomMinus = () => {
|
||||||
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
|
view.webContents.setZoomFactor(Math.max(0.25, view.webContents.getZoomFactor() - 0.1));
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increases page zoom on the active webview.
|
||||||
|
*/
|
||||||
|
const zoomPlus = () => {
|
||||||
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
|
view.webContents.setZoomFactor(Math.min(4.0, view.webContents.getZoomFactor() + 0.1));
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates backward in the history of the active webview.
|
* Navigates backward in the history of the active webview.
|
||||||
*/
|
*/
|
||||||
@@ -251,16 +316,25 @@ const historyForward = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates the next webview or cycles back to the first.
|
* Activates the previous webview page.
|
||||||
*/
|
*/
|
||||||
const nextView = () => {
|
const previousView = () => {
|
||||||
WEBVIEW.viewActive = (WEBVIEW.viewActive + 1) % WEBVIEW.views.length || 1;
|
if (WEBVIEW.viewActive > 1) WEBVIEW.viewActive--;
|
||||||
|
updateView();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reloads the current or default url on the active webview.
|
* Activates the next webview page.
|
||||||
*/
|
*/
|
||||||
const reloadView = () => {
|
const nextView = () => {
|
||||||
|
if (WEBVIEW.viewActive < WEBVIEW.views.length - 1) WEBVIEW.viewActive++;
|
||||||
|
updateView();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the default url and settings on the active webview.
|
||||||
|
*/
|
||||||
|
const homeView = () => {
|
||||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||||
const currentUrl = view.webContents.getURL();
|
const currentUrl = view.webContents.getURL();
|
||||||
@@ -271,6 +345,30 @@ const reloadView = () => {
|
|||||||
} else {
|
} else {
|
||||||
view.webContents.reloadIgnoringCache();
|
view.webContents.reloadIgnoringCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset page zoom and history
|
||||||
|
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
||||||
|
setTimeout(() => {
|
||||||
|
view.webContents.navigationHistory.clear();
|
||||||
|
update();
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the current url on the active webview.
|
||||||
|
*/
|
||||||
|
const reloadView = () => {
|
||||||
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
|
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||||
|
const currentUrl = view.webContents.getURL();
|
||||||
|
|
||||||
|
// Reload the default url or refresh the page
|
||||||
|
if (currentUrl.startsWith("data:")) {
|
||||||
|
view.webContents.loadURL(defaultUrl);
|
||||||
|
} else {
|
||||||
|
view.webContents.reloadIgnoringCache();
|
||||||
|
}
|
||||||
|
update();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -279,6 +377,7 @@ const reloadView = () => {
|
|||||||
const resizeView = () => {
|
const resizeView = () => {
|
||||||
const window = WEBVIEW.window.getBounds();
|
const window = WEBVIEW.window.getBounds();
|
||||||
const navigation = WEBVIEW.navigation.getBounds();
|
const navigation = WEBVIEW.navigation.getBounds();
|
||||||
|
const pager = { width: 20, height: window.height };
|
||||||
const widget = { width: 60, height: 200 };
|
const widget = { width: 60, height: 200 };
|
||||||
|
|
||||||
// Update view size
|
// Update view size
|
||||||
@@ -289,13 +388,23 @@ const resizeView = () => {
|
|||||||
width: window.width,
|
width: window.width,
|
||||||
height: window.height - navigation.height,
|
height: window.height - navigation.height,
|
||||||
});
|
});
|
||||||
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update pager size
|
||||||
|
if (WEBVIEW.pagerEnabled) {
|
||||||
|
WEBVIEW.pager.setBounds({
|
||||||
|
x: window.width - pager.width,
|
||||||
|
y: 0,
|
||||||
|
width: pager.width,
|
||||||
|
height: pager.height,
|
||||||
|
});
|
||||||
|
WEBVIEW.pager.webContents.send("data-theme", { theme: "hidden" });
|
||||||
|
}
|
||||||
|
|
||||||
// Update widget size
|
// Update widget size
|
||||||
if (WEBVIEW.widgetEnabled) {
|
if (WEBVIEW.widgetEnabled) {
|
||||||
WEBVIEW.widget.setBounds({
|
WEBVIEW.widget.setBounds({
|
||||||
x: window.width - 15,
|
x: window.width - 20,
|
||||||
y: parseInt(window.height / 2 - widget.height / 2, 10),
|
y: parseInt(window.height / 2 - widget.height / 2, 10),
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
@@ -332,7 +441,7 @@ const windowEvents = () => {
|
|||||||
}
|
}
|
||||||
WEBVIEW.locked = false;
|
WEBVIEW.locked = false;
|
||||||
});
|
});
|
||||||
HARDWARE.display.notifiers.push(() => {
|
HARDWARE.events.on("updateDisplay", () => {
|
||||||
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
||||||
if (WEBVIEW.locked) {
|
if (WEBVIEW.locked) {
|
||||||
WEBVIEW.window.blur();
|
WEBVIEW.window.blur();
|
||||||
@@ -347,32 +456,39 @@ const windowEvents = () => {
|
|||||||
WEBVIEW.window.on("enter-full-screen", update);
|
WEBVIEW.window.on("enter-full-screen", update);
|
||||||
WEBVIEW.window.on("leave-full-screen", update);
|
WEBVIEW.window.on("leave-full-screen", update);
|
||||||
|
|
||||||
// Handle window touch events for activity tracking
|
// Handle global shortcut events
|
||||||
|
globalShortcut.register("Control+Left", () => {
|
||||||
|
previousView();
|
||||||
|
});
|
||||||
|
globalShortcut.register("Control+Right", () => {
|
||||||
|
nextView();
|
||||||
|
});
|
||||||
|
globalShortcut.register("Control+numsub", () => {
|
||||||
|
zoomMinus();
|
||||||
|
});
|
||||||
|
globalShortcut.register("Control+numadd", () => {
|
||||||
|
zoomPlus();
|
||||||
|
});
|
||||||
|
globalShortcut.register("Alt+Left", () => {
|
||||||
|
historyBackward();
|
||||||
|
});
|
||||||
|
globalShortcut.register("Alt+Right", () => {
|
||||||
|
historyForward();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle window touch events
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const then = WEBVIEW.pointer.time;
|
const then = WEBVIEW.pointer.time;
|
||||||
const delta = Math.abs(now - then) / 1000;
|
const delta = Math.abs(now - then) / 1000;
|
||||||
const posNew = screen.getCursorScreenPoint();
|
|
||||||
const posOld = WEBVIEW.pointer.position;
|
|
||||||
|
|
||||||
// Cursor movement detected
|
|
||||||
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) {
|
|
||||||
const navigation = WEBVIEW.navigation.getBounds();
|
|
||||||
|
|
||||||
// Auto-hide navigation
|
// Auto-hide navigation
|
||||||
|
if (delta > 60) {
|
||||||
|
const navigation = WEBVIEW.navigation.getBounds();
|
||||||
if (navigation.height > 0) {
|
if (navigation.height > 0) {
|
||||||
toggleNavigation();
|
toggleNavigation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WEBVIEW.pointer.position = posNew;
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -406,7 +522,7 @@ const widgetEvents = () => {
|
|||||||
|
|
||||||
// Hide widget
|
// Hide widget
|
||||||
WEBVIEW.widget.setBounds({
|
WEBVIEW.widget.setBounds({
|
||||||
x: window.width - 15,
|
x: window.width - 20,
|
||||||
y: widget.y,
|
y: widget.y,
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
@@ -419,7 +535,6 @@ const widgetEvents = () => {
|
|||||||
switch (button.id) {
|
switch (button.id) {
|
||||||
case "keyboard":
|
case "keyboard":
|
||||||
const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON";
|
const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON";
|
||||||
hardware.setKeyboardVisibility(toggle);
|
|
||||||
switch (toggle) {
|
switch (toggle) {
|
||||||
case "OFF":
|
case "OFF":
|
||||||
WEBVIEW.window.restore();
|
WEBVIEW.window.restore();
|
||||||
@@ -432,6 +547,9 @@ const widgetEvents = () => {
|
|||||||
WEBVIEW.window.maximize();
|
WEBVIEW.window.maximize();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
hardware.setKeyboardVisibility(toggle, () => {
|
||||||
|
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "fullscreen":
|
case "fullscreen":
|
||||||
if (WEBVIEW.window.isFullScreen()) {
|
if (WEBVIEW.window.isFullScreen()) {
|
||||||
@@ -443,7 +561,9 @@ const widgetEvents = () => {
|
|||||||
WEBVIEW.window.unmaximize();
|
WEBVIEW.window.unmaximize();
|
||||||
WEBVIEW.window.setFullScreen(true);
|
WEBVIEW.window.setFullScreen(true);
|
||||||
}
|
}
|
||||||
hardware.setKeyboardVisibility("OFF");
|
hardware.setKeyboardVisibility("OFF", () => {
|
||||||
|
WEBVIEW.views[WEBVIEW.viewActive].webContents.focus();
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "minimize":
|
case "minimize":
|
||||||
WEBVIEW.window.restore();
|
WEBVIEW.window.restore();
|
||||||
@@ -519,13 +639,30 @@ const navigationEvents = () => {
|
|||||||
|
|
||||||
// Handle navigation button click events
|
// Handle navigation button click events
|
||||||
ipcMain.on("button-click", (e, button) => {
|
ipcMain.on("button-click", (e, button) => {
|
||||||
|
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||||
switch (button.id) {
|
switch (button.id) {
|
||||||
case "pager":
|
case "home":
|
||||||
|
homeView();
|
||||||
|
hardware.setKeyboardVisibility("OFF");
|
||||||
|
break;
|
||||||
|
case "refresh":
|
||||||
|
reloadView();
|
||||||
|
hardware.setKeyboardVisibility("OFF");
|
||||||
|
break;
|
||||||
|
case "previous":
|
||||||
|
previousView();
|
||||||
|
hardware.setKeyboardVisibility("OFF");
|
||||||
|
break;
|
||||||
|
case "next":
|
||||||
nextView();
|
nextView();
|
||||||
hardware.setKeyboardVisibility("OFF");
|
hardware.setKeyboardVisibility("OFF");
|
||||||
break;
|
break;
|
||||||
case "home":
|
case "minus":
|
||||||
reloadView();
|
zoomMinus();
|
||||||
|
hardware.setKeyboardVisibility("OFF");
|
||||||
|
break;
|
||||||
|
case "plus":
|
||||||
|
zoomPlus();
|
||||||
hardware.setKeyboardVisibility("OFF");
|
hardware.setKeyboardVisibility("OFF");
|
||||||
break;
|
break;
|
||||||
case "backward":
|
case "backward":
|
||||||
@@ -541,10 +678,11 @@ const navigationEvents = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register dom events and handler.
|
* Register view events and handler.
|
||||||
*/
|
*/
|
||||||
const domEvents = () => {
|
const viewEvents = () => {
|
||||||
WEBVIEW.views.forEach((view) => {
|
const ready = [];
|
||||||
|
WEBVIEW.views.forEach((view, i) => {
|
||||||
// Enable webview touch emulation
|
// Enable webview touch emulation
|
||||||
view.webContents.debugger.attach("1.1");
|
view.webContents.debugger.attach("1.1");
|
||||||
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
||||||
@@ -558,15 +696,18 @@ const domEvents = () => {
|
|||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove webview scrollbar
|
// Update webview layout
|
||||||
view.webContents.on("dom-ready", () => {
|
view.webContents.on("dom-ready", () => {
|
||||||
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
||||||
|
if (ready.length < WEBVIEW.views.length) {
|
||||||
|
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
||||||
|
ready.push(i);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Webview fully loaded
|
// Webview fully loaded
|
||||||
view.webContents.on("did-finish-load", () => {
|
view.webContents.on("did-finish-load", () => {
|
||||||
WEBVIEW.initialized = true;
|
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
|
||||||
if (WEBVIEW.viewActive === 0) {
|
|
||||||
nextView();
|
nextView();
|
||||||
}
|
}
|
||||||
if (ARGS.app_debug === "true") {
|
if (ARGS.app_debug === "true") {
|
||||||
@@ -579,7 +720,7 @@ const domEvents = () => {
|
|||||||
// Webview not loaded
|
// Webview not loaded
|
||||||
view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => {
|
view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => {
|
||||||
if (mainframe) {
|
if (mainframe) {
|
||||||
if (WEBVIEW.viewActive === 0) {
|
if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) {
|
||||||
nextView();
|
nextView();
|
||||||
}
|
}
|
||||||
view.webContents.loadURL(errorHtml(code, text, url));
|
view.webContents.loadURL(errorHtml(code, text, url));
|
||||||
@@ -589,11 +730,37 @@ const domEvents = () => {
|
|||||||
// Webview url changed
|
// Webview url changed
|
||||||
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
|
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
|
||||||
if (mainframe) {
|
if (mainframe) {
|
||||||
update();
|
updateView();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
view.webContents.on("did-navigate", () => {
|
view.webContents.on("did-navigate", () => {
|
||||||
update();
|
updateView();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle webview mouse events
|
||||||
|
view.webContents.on("before-mouse-event", (e, mouse) => {
|
||||||
|
const now = new Date();
|
||||||
|
const then = WEBVIEW.pointer.time;
|
||||||
|
const delta = Math.abs(now - then) / 1000;
|
||||||
|
WEBVIEW.pointer.time = now;
|
||||||
|
switch (mouse.type) {
|
||||||
|
case "mouseMove":
|
||||||
|
if (delta > 30) {
|
||||||
|
console.log("Update Last Active");
|
||||||
|
integration.update();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "mouseDown":
|
||||||
|
switch (mouse.button) {
|
||||||
|
case "back":
|
||||||
|
historyBackward();
|
||||||
|
break;
|
||||||
|
case "forward":
|
||||||
|
historyForward();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -616,6 +783,42 @@ const appEvents = () => {
|
|||||||
}
|
}
|
||||||
WEBVIEW.window.focus();
|
WEBVIEW.window.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Webview initialized
|
||||||
|
WEBVIEW.initialized = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+9
-8
@@ -3,25 +3,26 @@
|
|||||||
"description": "Home Assistant Touch Kiosk",
|
"description": "Home Assistant Touch Kiosk",
|
||||||
"homepage": "https://github.com/leukipp/touchkio",
|
"homepage": "https://github.com/leukipp/touchkio",
|
||||||
"author": "leukipp",
|
"author": "leukipp",
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron . --web-url=https://demo.home-assistant.io --trace-warnings",
|
"start": "electron . --web-url=https://demo.home-assistant.io,https://demo.immichkiosk.app --app-debug=false --trace-warnings",
|
||||||
"build": "electron-forge make"
|
"build": "electron-forge make"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-forge/cli": "^7.8.1",
|
"@electron-forge/cli": "^7.8.3",
|
||||||
"@electron-forge/maker-deb": "^7.8.1",
|
"@electron-forge/maker-deb": "^7.8.3",
|
||||||
"@electron-forge/maker-zip": "^7.8.1",
|
"@electron-forge/maker-zip": "^7.8.3",
|
||||||
"@electron-forge/publisher-github": "^7.8.1",
|
"@electron-forge/publisher-github": "^7.8.3",
|
||||||
"electron": "^37.2.0",
|
"electron": "^37.4.0",
|
||||||
"prettier": "^3.6.2"
|
"prettier": "^3.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mqtt": "^5.13.2"
|
"axios": "^1.11.0",
|
||||||
|
"mqtt": "^5.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user