Add real first-time provisioning to install.sh (v2.14.0)
Until now ./install.sh only managed an already-installed kiosk; ubuntu-based-kiosk.sh was still the only path from a bare Ubuntu Server box to a running one. install.sh now provisions from scratch too: packages, kiosk user, Node.js/Electron, LightDM+Openbox autologin, audio/video/HDMI/power-button hardware setup, and the firewall, then hands off to the already-migrated Core Settings/Advanced menus for initial configuration instead of reimplementing that logic again. - lib/provision.sh: the new provisioning flow, built mostly by calling existing menus (core_settings_menu, emergency hotspot, virtual consoles) - cuts it to ~300 lines against the legacy script's ~4,000-line first_time_install(). - lib/electron.sh: electron_install_binary() extracted out of menus/advanced_electron.sh so provisioning and the existing "Fix blank screen" action share one implementation. - kiosk-app/ and provision/files/: the Electron app source and every system template file, extracted byte-for-byte out of ubuntu-based-kiosk.sh's heredocs into real files. - Found and fixed a bash set -e gotcha along the way: testing a multi-statement function as an if-condition (`if ! some_func; then`) silently exempts everything inside that function from set -e for the duration of the call. Fixed in the new provisioning code and in menus/advanced_electron.sh's pre-existing repair action, which had the same shape. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: #ecf0f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
padding: 40px;
|
||||
}
|
||||
.container { text-align: center; max-width: 600px; }
|
||||
h2 { font-size: 32px; margin-bottom: 20px; }
|
||||
.message { font-size: 20px; margin-bottom: 20px; line-height: 1.5; }
|
||||
.countdown { font-size: 72px; font-weight: bold; color: #e74c3c; margin: 20px 0; }
|
||||
.options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
.btn {
|
||||
padding: 20px 40px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #27ae60;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.btn-primary:hover { background: #229954; }
|
||||
.btn-primary:active { background: #1e8449; }
|
||||
|
||||
.btn-extend { background: #3498db; }
|
||||
.btn-extend:hover { background: #2980b9; }
|
||||
.btn-extend:active { background: #21618c; }
|
||||
|
||||
.btn-home {
|
||||
background: #95a5a6;
|
||||
grid-column: 1 / -1;
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
}
|
||||
.btn-home:hover { background: #7f8c8d; }
|
||||
|
||||
.info {
|
||||
font-size: 14px;
|
||||
color: #95a5a6;
|
||||
margin-top: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>👋 Are you still here?</h2>
|
||||
<div class="message">
|
||||
No activity detected. Choose an option:
|
||||
</div>
|
||||
<div class="countdown" id="countdown">15</div>
|
||||
|
||||
<div class="options">
|
||||
<button class="btn btn-primary" onclick="imHere(0)">
|
||||
✓ Yes, I'm here!
|
||||
</button>
|
||||
|
||||
<button class="btn btn-extend" onclick="imHere(15)">
|
||||
🍿 15 more minutes
|
||||
</button>
|
||||
|
||||
<button class="btn btn-extend" onclick="imHere(30)">
|
||||
⏱️ 30 more minutes
|
||||
</button>
|
||||
|
||||
<button class="btn btn-extend" onclick="imHere(60)">
|
||||
🎬 1 hour
|
||||
</button>
|
||||
|
||||
<button class="btn btn-extend" onclick="imHere(120)">
|
||||
📺 2 hours
|
||||
</button>
|
||||
|
||||
<button class="btn btn-home" onclick="goHome()">
|
||||
🏠 Return to home now
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
ℹ️ Extensions pause the inactivity timer<br>
|
||||
Media playback (video/audio) automatically pauses the timer<br>
|
||||
Maximum extension: 4 hours (safety timeout)
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const {ipcRenderer} = require('electron');
|
||||
let count = 15;
|
||||
const interval = setInterval(() => {
|
||||
count--;
|
||||
document.getElementById('countdown').textContent = count;
|
||||
if (count <= 0) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
function imHere(minutes) {
|
||||
clearInterval(interval);
|
||||
console.log('[PROMPT] User selected: '+(minutes===0?'Continue':minutes+' minutes'));
|
||||
ipcRenderer.send('user-still-here', minutes);
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
clearInterval(interval);
|
||||
console.log('[PROMPT] User requested immediate home return');
|
||||
ipcRenderer.send('user-still-here', -1);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
imHere(0);
|
||||
} else if (e.key === 'Escape') {
|
||||
goHome();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,261 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
color: #ecf0f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.keyboard { width: 100%; max-width: 1200px; margin: 0 auto; }
|
||||
.row { display: flex; justify-content: center; margin-bottom: 8px; gap: 6px; }
|
||||
.key {
|
||||
min-width: 60px; height: 60px;
|
||||
background: linear-gradient(135deg, #34495e 0%, #2c3e50 100%);
|
||||
border: 2px solid #4a5f7f; border-radius: 8px; color: white;
|
||||
font-size: 24px; font-weight: 600; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: all 0.1s; user-select: none; box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
.key:active {
|
||||
transform: scale(0.95);
|
||||
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||
border-color: #5dade2;
|
||||
}
|
||||
.key.space { flex: 3; }
|
||||
.key.wide { min-width: 90px; }
|
||||
.key.extra-wide { min-width: 120px; }
|
||||
.key.special {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
|
||||
font-size: 16px;
|
||||
}
|
||||
.key.enter {
|
||||
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
|
||||
border-color: #52be80;
|
||||
}
|
||||
.key.backspace {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
border-color: #ec7063;
|
||||
}
|
||||
.key.shift, .key.caps {
|
||||
background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
|
||||
border-color: #f8c471;
|
||||
}
|
||||
.key.shift.active, .key.caps.active {
|
||||
background: linear-gradient(135deg, #16a085 0%, #138d75 100%);
|
||||
border-color: #48c9b0;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
color: #bdc3c7;
|
||||
}
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(231, 76, 60, 0.9);
|
||||
border: 2px solid #e74c3c;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
z-index: 9999;
|
||||
}
|
||||
.close-btn:active {
|
||||
transform: scale(0.9);
|
||||
background: rgba(192, 57, 43, 0.9);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="close-btn" onclick="closeKeyboard()">×</div>
|
||||
|
||||
<div class="keyboard">
|
||||
<div class="header">⌨️ Keyboard - Click icon or swipe to reopen</div>
|
||||
|
||||
<!-- Number Row -->
|
||||
<div class="row">
|
||||
<div class="key" data-key="1" data-shift="!" onclick="typeKey(this)">1</div>
|
||||
<div class="key" data-key="2" data-shift="@" onclick="typeKey(this)">2</div>
|
||||
<div class="key" data-key="3" data-shift="#" onclick="typeKey(this)">3</div>
|
||||
<div class="key" data-key="4" data-shift="$" onclick="typeKey(this)">4</div>
|
||||
<div class="key" data-key="5" data-shift="%" onclick="typeKey(this)">5</div>
|
||||
<div class="key" data-key="6" data-shift="^" onclick="typeKey(this)">6</div>
|
||||
<div class="key" data-key="7" data-shift="&" onclick="typeKey(this)">7</div>
|
||||
<div class="key" data-key="8" data-shift="*" onclick="typeKey(this)">8</div>
|
||||
<div class="key" data-key="9" data-shift="(" onclick="typeKey(this)">9</div>
|
||||
<div class="key" data-key="0" data-shift=")" onclick="typeKey(this)">0</div>
|
||||
<div class="key" data-key="-" data-shift="_" onclick="typeKey(this)">-</div>
|
||||
<div class="key" data-key="=" data-shift="+" onclick="typeKey(this)">=</div>
|
||||
<div class="key backspace wide" onclick="typeKey(this)" data-special="Backspace">⌫</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Row -->
|
||||
<div class="row">
|
||||
<div class="key special wide" onclick="typeKey(this)" data-special="Tab">Tab</div>
|
||||
<div class="key" data-key="q" onclick="typeKey(this)">q</div>
|
||||
<div class="key" data-key="w" onclick="typeKey(this)">w</div>
|
||||
<div class="key" data-key="e" onclick="typeKey(this)">e</div>
|
||||
<div class="key" data-key="r" onclick="typeKey(this)">r</div>
|
||||
<div class="key" data-key="t" onclick="typeKey(this)">t</div>
|
||||
<div class="key" data-key="y" onclick="typeKey(this)">y</div>
|
||||
<div class="key" data-key="u" onclick="typeKey(this)">u</div>
|
||||
<div class="key" data-key="i" onclick="typeKey(this)">i</div>
|
||||
<div class="key" data-key="o" onclick="typeKey(this)">o</div>
|
||||
<div class="key" data-key="p" onclick="typeKey(this)">p</div>
|
||||
<div class="key" data-key="[" data-shift="{" onclick="typeKey(this)">[</div>
|
||||
<div class="key" data-key="]" data-shift="}" onclick="typeKey(this)">]</div>
|
||||
<div class="key" data-key="\\" data-shift="|" onclick="typeKey(this)">\</div>
|
||||
</div>
|
||||
|
||||
<!-- Home Row -->
|
||||
<div class="row">
|
||||
<div class="key caps special extra-wide" onclick="toggleCaps()" id="caps-key">Caps</div>
|
||||
<div class="key" data-key="a" onclick="typeKey(this)">a</div>
|
||||
<div class="key" data-key="s" onclick="typeKey(this)">s</div>
|
||||
<div class="key" data-key="d" onclick="typeKey(this)">d</div>
|
||||
<div class="key" data-key="f" onclick="typeKey(this)">f</div>
|
||||
<div class="key" data-key="g" onclick="typeKey(this)">g</div>
|
||||
<div class="key" data-key="h" onclick="typeKey(this)">h</div>
|
||||
<div class="key" data-key="j" onclick="typeKey(this)">j</div>
|
||||
<div class="key" data-key="k" onclick="typeKey(this)">k</div>
|
||||
<div class="key" data-key="l" onclick="typeKey(this)">l</div>
|
||||
<div class="key" data-key=";" data-shift=":" onclick="typeKey(this)">;</div>
|
||||
<div class="key" data-key="'" data-shift='"' onclick="typeKey(this)">'</div>
|
||||
<div class="key enter extra-wide" onclick="typeKey(this)" data-special="Enter">↵</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Row -->
|
||||
<div class="row">
|
||||
<div class="key shift extra-wide" onclick="toggleShift()" id="shift-left">⇧</div>
|
||||
<div class="key" data-key="z" onclick="typeKey(this)">z</div>
|
||||
<div class="key" data-key="x" onclick="typeKey(this)">x</div>
|
||||
<div class="key" data-key="c" onclick="typeKey(this)">c</div>
|
||||
<div class="key" data-key="v" onclick="typeKey(this)">v</div>
|
||||
<div class="key" data-key="b" onclick="typeKey(this)">b</div>
|
||||
<div class="key" data-key="n" onclick="typeKey(this)">n</div>
|
||||
<div class="key" data-key="m" onclick="typeKey(this)">m</div>
|
||||
<div class="key" data-key="," data-shift="<" onclick="typeKey(this)">,</div>
|
||||
<div class="key" data-key="." data-shift=">" onclick="typeKey(this)">.</div>
|
||||
<div class="key" data-key="/" data-shift="?" onclick="typeKey(this)">/</div>
|
||||
<div class="key shift extra-wide" onclick="toggleShift()" id="shift-right">⇧</div>
|
||||
</div>
|
||||
|
||||
<!-- Space Row -->
|
||||
<div class="row">
|
||||
<div class="key special" onclick="typeKey(this)" data-special="Control">Ctrl</div>
|
||||
<div class="key special" onclick="typeKey(this)" data-special="Alt">Alt</div>
|
||||
<div class="key space" onclick="typeKey(this)" data-special=" ">Space</div>
|
||||
<div class="key special" onclick="typeKey(this)" data-special="Alt">Alt</div>
|
||||
<div class="key special" onclick="typeKey(this)" data-special="Control">Ctrl</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer } = require('electron');
|
||||
let shiftPressed = false;
|
||||
let capsLock = false;
|
||||
|
||||
// CRITICAL: Do NOT preventDefault on keyboard events
|
||||
// This allows physical keyboard to work alongside OSK
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// Don't interfere with physical keyboard
|
||||
// Just update our visual state if needed
|
||||
if (e.key === 'CapsLock') {
|
||||
capsLock = !capsLock;
|
||||
updateCapsDisplay();
|
||||
}
|
||||
}, {passive: true});
|
||||
|
||||
function updateKeyDisplay() {
|
||||
const keys = document.querySelectorAll('.key[data-key]');
|
||||
keys.forEach(key => {
|
||||
const baseKey = key.getAttribute('data-key');
|
||||
const shiftKey = key.getAttribute('data-shift');
|
||||
if (/^[a-z]$/.test(baseKey)) {
|
||||
const shouldBeUpper = (shiftPressed && !capsLock) || (!shiftPressed && capsLock);
|
||||
key.textContent = shouldBeUpper ? baseKey.toUpperCase() : baseKey.toLowerCase();
|
||||
} else if (shiftKey) {
|
||||
key.textContent = shiftPressed ? shiftKey : baseKey;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function typeKey(element) {
|
||||
const special = element.getAttribute('data-special');
|
||||
if (special) {
|
||||
ipcRenderer.send('keyboard-type', special);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseKey = element.getAttribute('data-key');
|
||||
const shiftKey = element.getAttribute('data-shift');
|
||||
let finalKey = baseKey;
|
||||
|
||||
if (/^[a-z]$/.test(baseKey)) {
|
||||
const shouldBeUpper = (shiftPressed && !capsLock) || (!shiftPressed && capsLock);
|
||||
finalKey = shouldBeUpper ? baseKey.toUpperCase() : baseKey.toLowerCase();
|
||||
} else if (shiftPressed && shiftKey) {
|
||||
finalKey = shiftKey;
|
||||
}
|
||||
|
||||
ipcRenderer.send('keyboard-type', finalKey);
|
||||
|
||||
if (shiftPressed) {
|
||||
shiftPressed = false;
|
||||
updateShiftDisplay();
|
||||
updateKeyDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleShift() {
|
||||
shiftPressed = !shiftPressed;
|
||||
updateShiftDisplay();
|
||||
updateKeyDisplay();
|
||||
}
|
||||
|
||||
function toggleCaps() {
|
||||
capsLock = !capsLock;
|
||||
updateCapsDisplay();
|
||||
updateKeyDisplay();
|
||||
}
|
||||
|
||||
function updateShiftDisplay() {
|
||||
document.querySelectorAll('.shift').forEach(key => {
|
||||
if (shiftPressed) key.classList.add('active');
|
||||
else key.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
function updateCapsDisplay() {
|
||||
const capsKey = document.getElementById('caps-key');
|
||||
if (capsLock) capsKey.classList.add('active');
|
||||
else capsKey.classList.remove('active');
|
||||
}
|
||||
|
||||
function closeKeyboard() {
|
||||
ipcRenderer.send('close-keyboard');
|
||||
}
|
||||
|
||||
updateKeyDisplay();
|
||||
|
||||
// Tell main process we're ready
|
||||
ipcRenderer.send('keyboard-ready');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1634
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "kiosk-app",
|
||||
"version": "1.0.0",
|
||||
"main": "main.js",
|
||||
"dependencies": {
|
||||
"electron": "^42.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: #ecf0f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
padding: 40px;
|
||||
}
|
||||
.container { text-align: center; max-width: 600px; }
|
||||
h2 { font-size: 32px; margin-bottom: 20px; }
|
||||
.message { font-size: 20px; margin-bottom: 30px; line-height: 1.5; }
|
||||
.options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
.btn {
|
||||
padding: 20px 40px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
color: white;
|
||||
}
|
||||
.btn-extend { background: #e67e22; }
|
||||
.btn-extend:hover { background: #d35400; }
|
||||
.btn-extend:active { background: #ba4a00; }
|
||||
|
||||
.btn-cancel {
|
||||
background: #95a5a6;
|
||||
grid-column: 1 / -1;
|
||||
font-size: 16px;
|
||||
padding: 15px;
|
||||
}
|
||||
.btn-cancel:hover { background: #7f8c8d; }
|
||||
|
||||
.info {
|
||||
font-size: 14px;
|
||||
color: #95a5a6;
|
||||
margin-top: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.countdown {
|
||||
font-size: 16px;
|
||||
color: #e74c3c;
|
||||
margin-top: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>⏸️ Pause Timers</h2>
|
||||
<div class="message">
|
||||
Select how long to pause rotation and inactivity timers:
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
<button class="btn btn-extend" onclick="selectTime(15)">
|
||||
🍿 15 minutes
|
||||
</button>
|
||||
|
||||
<button class="btn btn-extend" onclick="selectTime(30)">
|
||||
⏱️ 30 minutes
|
||||
</button>
|
||||
|
||||
<button class="btn btn-extend" onclick="selectTime(60)">
|
||||
🎬 1 hour
|
||||
</button>
|
||||
|
||||
<button class="btn btn-extend" onclick="selectTime(120)">
|
||||
📺 2 hours
|
||||
</button>
|
||||
|
||||
<button class="btn btn-cancel" onclick="selectTime(0)">
|
||||
✗ Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
After the time expires, normal rotation and return-to-home logic will resume.
|
||||
</div>
|
||||
<div class="countdown" id="countdown">Auto-closing in 30 seconds...</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const {ipcRenderer} = require('electron');
|
||||
let timeLeft = 30;
|
||||
let countdownInterval;
|
||||
|
||||
function selectTime(minutes) {
|
||||
clearInterval(countdownInterval);
|
||||
ipcRenderer.send('pause-time-selected', minutes);
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
timeLeft--;
|
||||
document.getElementById('countdown').textContent = 'Auto-closing in ' + timeLeft + ' seconds...';
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
selectTime(0);
|
||||
}
|
||||
}
|
||||
|
||||
countdownInterval = setInterval(updateCountdown, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.container { width: 100%; max-width: 400px; }
|
||||
h2 { text-align: center; margin-bottom: 30px; font-size: 24px; }
|
||||
#pin-display {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
font-size: 48px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
border: 3px solid #34495e;
|
||||
border-radius: 12px;
|
||||
background: #34495e;
|
||||
color: #ecf0f1;
|
||||
letter-spacing: 20px;
|
||||
min-height: 90px;
|
||||
line-height: 50px;
|
||||
font-family: monospace;
|
||||
}
|
||||
#error { color: #e74c3c; text-align: center; display: none; margin: 10px 0; font-weight: bold; font-size: 18px; }
|
||||
.numpad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin: 20px 0; }
|
||||
.numpad button {
|
||||
padding: 30px;
|
||||
font-size: 32px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: #34495e;
|
||||
color: #ecf0f1;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.numpad button:active { background: #3498db; }
|
||||
.numpad button:hover { background: #475d6d; }
|
||||
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 20px; }
|
||||
.btn { padding: 20px; font-size: 20px; cursor: pointer; border: none; border-radius: 12px; font-weight: bold; }
|
||||
#clear { background: #f39c12; color: white; }
|
||||
#backspace { background: #e67e22; color: white; }
|
||||
#submit { background: #27ae60; color: white; }
|
||||
#cancel { background: #e74c3c; color: white; }
|
||||
.info { text-align: center; font-size: 14px; color: #95a5a6; margin-top: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>🔒 Enter PIN</h2>
|
||||
<div id="pin-display">••••</div>
|
||||
<div id="error">❌ Incorrect PIN</div>
|
||||
<div class="numpad">
|
||||
<button onclick="addDigit('1')">1</button>
|
||||
<button onclick="addDigit('2')">2</button>
|
||||
<button onclick="addDigit('3')">3</button>
|
||||
<button onclick="addDigit('4')">4</button>
|
||||
<button onclick="addDigit('5')">5</button>
|
||||
<button onclick="addDigit('6')">6</button>
|
||||
<button onclick="addDigit('7')">7</button>
|
||||
<button onclick="addDigit('8')">8</button>
|
||||
<button onclick="addDigit('9')">9</button>
|
||||
<button id="clear" onclick="clearPin()">Clear</button>
|
||||
<button onclick="addDigit('0')">0</button>
|
||||
<button id="backspace" onclick="backspace()">⌫</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" id="submit" onclick="submitPin()">✓ Submit</button>
|
||||
<button class="btn" id="cancel" onclick="cancel()">✗ Cancel</button>
|
||||
</div>
|
||||
<div class="info">Default PIN: 1234 (4-8 digits)</div>
|
||||
</div>
|
||||
<script>
|
||||
const {ipcRenderer} = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pinFile = path.join(__dirname, '.jitsi-pin');
|
||||
let correctPin = '1234';
|
||||
let enteredPin = '';
|
||||
try {
|
||||
const stored = fs.readFileSync(pinFile, 'utf8').trim();
|
||||
if (stored !== 'NOPIN') correctPin = stored;
|
||||
else correctPin = null;
|
||||
} catch(e) {}
|
||||
function updateDisplay() {
|
||||
const display = document.getElementById('pin-display');
|
||||
if (enteredPin.length === 0) {
|
||||
display.textContent = '••••';
|
||||
display.style.color = '#7f8c8d';
|
||||
} else {
|
||||
display.textContent = '•'.repeat(enteredPin.length);
|
||||
display.style.color = '#ecf0f1';
|
||||
}
|
||||
}
|
||||
function addDigit(digit) {
|
||||
if (enteredPin.length < 8) {
|
||||
enteredPin += digit;
|
||||
updateDisplay();
|
||||
document.getElementById('error').style.display = 'none';
|
||||
}
|
||||
}
|
||||
function backspace() { enteredPin = enteredPin.slice(0, -1); updateDisplay(); }
|
||||
function clearPin() { enteredPin = ''; updateDisplay(); document.getElementById('error').style.display = 'none'; }
|
||||
function submitPin() {
|
||||
if (enteredPin.length < 4) {
|
||||
document.getElementById('error').textContent = '❌ PIN must be 4-8 digits';
|
||||
document.getElementById('error').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
if (correctPin === null || enteredPin === correctPin) {
|
||||
ipcRenderer.send('pin-correct');
|
||||
} else {
|
||||
document.getElementById('error').textContent = '❌ Incorrect PIN';
|
||||
document.getElementById('error').style.display = 'block';
|
||||
enteredPin = '';
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
function cancel() { ipcRenderer.send('pin-cancelled'); }
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key >= '0' && e.key <= '9') addDigit(e.key);
|
||||
else if (e.key === 'Backspace') backspace();
|
||||
else if (e.key === 'Enter') submitPin();
|
||||
else if (e.key === 'Escape') cancel();
|
||||
});
|
||||
updateDisplay();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,931 @@
|
||||
const {contextBridge,ipcRenderer}=require('electron');
|
||||
|
||||
console.log('════════════════════════════════════════════════════════════');
|
||||
console.log(' Gestures:');
|
||||
console.log(' 3-finger DOWN: Toggle hidden tabs (PIN required)');
|
||||
console.log(' 2-finger HORIZONTAL: Switch between sites');
|
||||
console.log(' 1-finger HORIZONTAL: Navigate within page');
|
||||
console.log(' Navigation: Top-left key icon for site menu');
|
||||
console.log('════════════════════════════════════════════════════════════');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
notifyActivity: () => ipcRenderer.send('user-activity'),
|
||||
showKeyboard: () => ipcRenderer.send('show-keyboard'),
|
||||
closeKeyboard: () => ipcRenderer.send('close-keyboard'),
|
||||
keyboardActivity: () => ipcRenderer.send('keyboard-activity'),
|
||||
showPauseDialog: () => ipcRenderer.send('show-pause-dialog')
|
||||
});
|
||||
|
||||
// Pause button state (MUST be outside DOMContentLoaded to persist across page loads)
|
||||
let pauseButton=null;
|
||||
let pauseButtonShouldShow=false;
|
||||
let pauseButtonShown=false;
|
||||
let pauseButtonHideTimer=null;
|
||||
const PAUSE_BUTTON_HIDE_DELAY=5000; // Hide after 5 seconds of inactivity
|
||||
|
||||
// Pause button functions (must be outside DOMContentLoaded for IPC listener)
|
||||
function createPauseButton(){
|
||||
if(pauseButton)return;
|
||||
|
||||
pauseButton=document.createElement('div');
|
||||
pauseButton.id='electron-pause-button';
|
||||
pauseButton.innerHTML='<div style="display:flex;gap:4px;"><div style="width:6px;height:24px;background:white;border-radius:2px;"></div><div style="width:6px;height:24px;background:white;border-radius:2px;"></div></div>';
|
||||
pauseButton.title='Pause rotation';
|
||||
pauseButton.style.cssText=`
|
||||
position:fixed;bottom:20px;left:20px;width:60px;height:60px;
|
||||
background:rgba(230,126,34,0.95);border:3px solid rgba(255,255,255,0.9);
|
||||
border-radius:50%;display:none;align-items:center;justify-content:center;
|
||||
font-size:32px;cursor:pointer;z-index:999999;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.4);user-select:none;
|
||||
`;
|
||||
|
||||
pauseButton.addEventListener('click',(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ipcRenderer.send('show-pause-dialog');
|
||||
});
|
||||
|
||||
document.body.appendChild(pauseButton);
|
||||
}
|
||||
|
||||
function showPauseButton(){
|
||||
if(!pauseButton)createPauseButton();
|
||||
pauseButton.style.display='flex';
|
||||
pauseButtonShown=true;
|
||||
|
||||
// Clear existing hide timer
|
||||
if(pauseButtonHideTimer){
|
||||
clearTimeout(pauseButtonHideTimer);
|
||||
pauseButtonHideTimer=null;
|
||||
}
|
||||
|
||||
// Set new hide timer - button will auto-hide after inactivity
|
||||
pauseButtonHideTimer=setTimeout(()=>{
|
||||
console.log('[PAUSE-BTN] Auto-hiding after '+PAUSE_BUTTON_HIDE_DELAY+'ms inactivity');
|
||||
hidePauseButton();
|
||||
},PAUSE_BUTTON_HIDE_DELAY);
|
||||
}
|
||||
|
||||
function hidePauseButton(){
|
||||
if(pauseButtonHideTimer){
|
||||
clearTimeout(pauseButtonHideTimer);
|
||||
pauseButtonHideTimer=null;
|
||||
}
|
||||
if(pauseButton){
|
||||
pauseButton.style.display='none';
|
||||
pauseButtonShown=false;
|
||||
}
|
||||
}
|
||||
|
||||
// Declare variables at top level so IPC handlers and DOMContentLoaded can share them
|
||||
let keyboardButtonEnabled=true;
|
||||
let keyboardVisible=false;
|
||||
let keyboardIcon=null;
|
||||
let navButtonEnabled=true;
|
||||
let navButton=null;
|
||||
let navButtonShown=false;
|
||||
let navButtonHideTimer=null;
|
||||
let navMenu=null;
|
||||
let navMenuVisible=false;
|
||||
let navMenuTimer=null;
|
||||
const NAV_MENU_TIMEOUT=30000; // 30 seconds
|
||||
const NAV_BUTTON_HIDE_DELAY=5000; // Hide after 5 seconds of inactivity
|
||||
|
||||
// Listen for pause button visibility control from main process
|
||||
// CRITICAL: This must be outside DOMContentLoaded so it doesn't reset on page load
|
||||
ipcRenderer.on('pause-button-visibility',(event,shouldShow)=>{
|
||||
console.log('[PAUSE-BTN] Visibility update: shouldShow='+shouldShow);
|
||||
pauseButtonShouldShow=shouldShow;
|
||||
if(!shouldShow){
|
||||
// If button should not show on this site, hide it immediately
|
||||
console.log('[PAUSE-BTN] Hiding button (manual site)');
|
||||
hidePauseButton();
|
||||
}else{
|
||||
console.log('[PAUSE-BTN] Button enabled - will show on user interaction');
|
||||
}
|
||||
// If shouldShow is true, button will appear on user interaction
|
||||
});
|
||||
|
||||
ipcRenderer.on('keyboard-button-enabled',(event,enabled)=>{
|
||||
keyboardButtonEnabled=enabled;
|
||||
console.log('[KEYBOARD-BTN] Keyboard button enabled: '+enabled);
|
||||
// Note: keyboardIcon may not exist yet if page hasn't loaded
|
||||
if(keyboardIcon&&!enabled){
|
||||
keyboardIcon.style.display='none';
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('nav-button-enabled',(event,enabled)=>{
|
||||
navButtonEnabled=enabled;
|
||||
console.log('[NAV-BTN] Navigation button enabled: '+enabled);
|
||||
if(navButton&&!enabled){
|
||||
navButton.style.display='none';
|
||||
}
|
||||
if(navMenu&&!enabled){
|
||||
navMenu.style.display='none';
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('DOMContentLoaded',()=>{
|
||||
document.addEventListener('contextmenu',e=>e.preventDefault());
|
||||
|
||||
const SWIPE_THRESHOLD=120;
|
||||
const SWIPE_MAX_TIME=500;
|
||||
const SWIPE_TOLERANCE=50;
|
||||
|
||||
let touchStartX=0;
|
||||
let touchStartY=0;
|
||||
let touchStartTime=0;
|
||||
let fingerCount=0;
|
||||
let lastKeyboardRequest=0;
|
||||
let keyboardAutoClosedThisSession=false;
|
||||
const KEYBOARD_REQUEST_THROTTLE=1000;
|
||||
|
||||
const activityEvents=[
|
||||
'mousedown','mouseup','mousemove','click','dblclick',
|
||||
'wheel','scroll',
|
||||
'keydown','keyup','keypress',
|
||||
'touchstart','touchmove','touchend',
|
||||
'pointerdown','pointerup','pointermove',
|
||||
'input','change'
|
||||
];
|
||||
|
||||
let lastActivityNotification=0;
|
||||
const ACTIVITY_THROTTLE=1000;
|
||||
|
||||
function notifyActivity(){
|
||||
const now=Date.now();
|
||||
if(now-lastActivityNotification>ACTIVITY_THROTTLE){
|
||||
if(window.electronAPI?.notifyActivity){
|
||||
window.electronAPI.notifyActivity();
|
||||
lastActivityNotification=now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activityEvents.forEach(eventType=>{
|
||||
document.addEventListener(eventType,notifyActivity,{
|
||||
passive:true,
|
||||
capture:true
|
||||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on('keyboard-state-changed',(event,visible)=>{
|
||||
keyboardVisible=visible;
|
||||
if(visible){
|
||||
showKeyboardIcon();
|
||||
keyboardAutoClosedThisSession=false;
|
||||
}else{
|
||||
hideKeyboardIcon();
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('keyboard-auto-closed',()=>{
|
||||
keyboardAutoClosedThisSession=true;
|
||||
});
|
||||
|
||||
function createKeyboardIcon(){
|
||||
if(keyboardIcon||!keyboardButtonEnabled)return;
|
||||
|
||||
|
||||
keyboardIcon=document.createElement('div');
|
||||
keyboardIcon.id='electron-keyboard-icon';
|
||||
keyboardIcon.innerHTML='⌨️';
|
||||
keyboardIcon.style.cssText=`
|
||||
position:fixed;bottom:20px;right:20px;width:60px;height:60px;
|
||||
background:rgba(52,152,219,0.95);border:3px solid rgba(255,255,255,0.9);
|
||||
border-radius:50%;display:none;align-items:center;justify-content:center;
|
||||
font-size:32px;cursor:pointer;z-index:999999;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.4);user-select:none;
|
||||
`;
|
||||
|
||||
keyboardIcon.addEventListener('click',(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
keyboardAutoClosedThisSession=false;
|
||||
if(keyboardVisible){
|
||||
ipcRenderer.send('close-keyboard');
|
||||
}else{
|
||||
ipcRenderer.send('show-keyboard');
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(keyboardIcon);
|
||||
}
|
||||
|
||||
function showKeyboardIcon(){
|
||||
if(!keyboardButtonEnabled)return;
|
||||
if(!keyboardIcon)createKeyboardIcon();
|
||||
if(keyboardIcon)keyboardIcon.style.display='flex';
|
||||
}
|
||||
|
||||
function hideKeyboardIcon(){
|
||||
if(keyboardIcon)keyboardIcon.style.display='none';
|
||||
}
|
||||
|
||||
function createNavButton(){
|
||||
if(navButton||!navButtonEnabled)return;
|
||||
|
||||
navButton=document.createElement('div');
|
||||
navButton.id='electron-nav-button';
|
||||
// Use SVG key icon instead of emoji for better compatibility
|
||||
navButton.innerHTML='<svg width="32" height="32" viewBox="0 0 24 24" fill="white"><path d="M12.65 10C11.7 7.31 8.9 5.5 5.77 6.12c-2.29.46-4.15 2.29-4.63 4.58C.32 14.57 3.26 18 7 18c2.61 0 4.83-1.67 5.65-4H17v2c0 1.1.9 2 2 2s2-.9 2-2v-2c1.1 0 2-.9 2-2s-.9-2-2-2h-8.35zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>';
|
||||
navButton.title='Navigation Menu';
|
||||
navButton.style.cssText=`
|
||||
position:fixed;top:20px;left:20px;width:60px;height:60px;
|
||||
background:rgba(155,89,182,0.95);border:3px solid rgba(255,255,255,0.9);
|
||||
border-radius:50%;display:none;align-items:center;justify-content:center;
|
||||
cursor:pointer;z-index:999999;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.4);user-select:none;
|
||||
`;
|
||||
|
||||
navButton.addEventListener('click',(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('[NAV] Button clicked');
|
||||
try{
|
||||
toggleNavMenu();
|
||||
}catch(err){
|
||||
console.error('[NAV] Error toggling menu:',err);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(navButton);
|
||||
}
|
||||
|
||||
// Power button in top-right corner (follows same show/hide logic as nav button)
|
||||
let powerButton=null;
|
||||
let powerButtonHideTimer=null;
|
||||
const POWER_BUTTON_HIDE_DELAY=5000; // Same as nav button
|
||||
function createPowerButton(){
|
||||
if(powerButton)return;
|
||||
powerButton=document.createElement('div');
|
||||
powerButton.id='electron-power-button';
|
||||
// Power icon SVG
|
||||
powerButton.innerHTML='<svg width="28" height="28" viewBox="0 0 24 24" fill="white"><path d="M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z"/></svg>';
|
||||
powerButton.title='Power Menu';
|
||||
powerButton.style.cssText=`
|
||||
position:fixed;top:20px;right:20px;width:60px;height:60px;
|
||||
background:rgba(231,76,60,0.95);border:3px solid rgba(255,255,255,0.9);
|
||||
border-radius:50%;display:none;align-items:center;justify-content:center;
|
||||
cursor:pointer;z-index:999999;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.4);user-select:none;
|
||||
`;
|
||||
powerButton.addEventListener('click',(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('[POWER] Button clicked');
|
||||
ipcRenderer.send('show-power-menu');
|
||||
});
|
||||
document.body.appendChild(powerButton);
|
||||
}
|
||||
|
||||
function showPowerButton(){
|
||||
if(!powerButton)createPowerButton();
|
||||
if(powerButton){
|
||||
powerButton.style.display='flex';
|
||||
}
|
||||
|
||||
// Clear existing hide timer
|
||||
if(powerButtonHideTimer){
|
||||
clearTimeout(powerButtonHideTimer);
|
||||
powerButtonHideTimer=null;
|
||||
}
|
||||
|
||||
// Set new hide timer - button will auto-hide after inactivity
|
||||
powerButtonHideTimer=setTimeout(()=>{
|
||||
console.log('[POWER-BTN] Auto-hiding after '+POWER_BUTTON_HIDE_DELAY+'ms inactivity');
|
||||
hidePowerButton();
|
||||
},POWER_BUTTON_HIDE_DELAY);
|
||||
}
|
||||
|
||||
function hidePowerButton(){
|
||||
if(powerButtonHideTimer){
|
||||
clearTimeout(powerButtonHideTimer);
|
||||
powerButtonHideTimer=null;
|
||||
}
|
||||
if(powerButton){
|
||||
powerButton.style.display='none';
|
||||
}
|
||||
}
|
||||
|
||||
function showNavButton(){
|
||||
if(!navButtonEnabled)return;
|
||||
if(!navButton)createNavButton();
|
||||
if(navButton){
|
||||
navButton.style.display='flex';
|
||||
navButtonShown=true;
|
||||
}
|
||||
|
||||
// Clear existing hide timer
|
||||
if(navButtonHideTimer){
|
||||
clearTimeout(navButtonHideTimer);
|
||||
navButtonHideTimer=null;
|
||||
}
|
||||
|
||||
// Set new hide timer - button will auto-hide after inactivity
|
||||
navButtonHideTimer=setTimeout(()=>{
|
||||
console.log('[NAV-BTN] Auto-hiding after '+NAV_BUTTON_HIDE_DELAY+'ms inactivity');
|
||||
hideNavButton();
|
||||
},NAV_BUTTON_HIDE_DELAY);
|
||||
}
|
||||
|
||||
function hideNavButton(){
|
||||
if(navButtonHideTimer){
|
||||
clearTimeout(navButtonHideTimer);
|
||||
navButtonHideTimer=null;
|
||||
}
|
||||
if(navButton){
|
||||
navButton.style.display='none';
|
||||
navButtonShown=false;
|
||||
}
|
||||
}
|
||||
|
||||
function createNavMenu(){
|
||||
if(navMenu)return;
|
||||
console.log('[NAV] Creating navigation menu');
|
||||
|
||||
navMenu=document.createElement('div');
|
||||
navMenu.id='electron-nav-menu';
|
||||
navMenu.style.cssText=`
|
||||
position:fixed;top:0;left:0;width:100%;height:100%;
|
||||
background:rgba(0,0,0,0.9);display:none;align-items:center;justify-content:center;
|
||||
z-index:999998;pointer-events:auto;
|
||||
`;
|
||||
|
||||
const content=document.createElement('div');
|
||||
content.style.cssText=`
|
||||
position:relative;background:rgba(44,62,80,0.98);border-radius:20px;padding:40px;
|
||||
max-width:90%;max-height:90%;overflow:hidden;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,0.5);
|
||||
`;
|
||||
|
||||
const closeBtn=document.createElement('div');
|
||||
closeBtn.innerHTML='✕';
|
||||
closeBtn.style.cssText=`
|
||||
position:absolute;top:10px;right:10px;font-size:32px;color:white;
|
||||
cursor:pointer;width:40px;height:40px;display:flex;align-items:center;
|
||||
justify-content:center;border-radius:50%;background:rgba(231,76,60,0.8);
|
||||
user-select:none;
|
||||
`;
|
||||
closeBtn.addEventListener('click',(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('[NAV] Close button clicked');
|
||||
hideNavMenu();
|
||||
});
|
||||
content.appendChild(closeBtn);
|
||||
|
||||
const columns=document.createElement('div');
|
||||
columns.style.cssText='display:flex;gap:40px;margin-top:20px;max-height:70vh;';
|
||||
|
||||
// Column 1: Sites (scrollable)
|
||||
const sitesCol=document.createElement('div');
|
||||
sitesCol.style.cssText='flex:1;min-width:300px;display:flex;flex-direction:column;';
|
||||
sitesCol.innerHTML='<h2 style="color:white;margin-bottom:20px;">Sites</h2>';
|
||||
const sitesList=document.createElement('div');
|
||||
sitesList.id='nav-sites-list';
|
||||
sitesList.style.cssText='display:flex;flex-direction:column;gap:10px;overflow-y:auto;padding-right:10px;';
|
||||
sitesCol.appendChild(sitesList);
|
||||
|
||||
// Column 2: Gesture Cheat Sheet (fixed, no scroll)
|
||||
const cheatCol=document.createElement('div');
|
||||
cheatCol.style.cssText='flex:1;min-width:300px;overflow-y:hidden;';
|
||||
cheatCol.innerHTML=`
|
||||
<h2 style="color:white;margin-bottom:20px;">Touch Gestures</h2>
|
||||
<div style="color:#ecf0f1;line-height:1.8;font-size:16px;">
|
||||
<div style="margin-bottom:15px;">
|
||||
<div style="font-weight:bold;color:#3498db;">2-Finger Horizontal Swipe</div>
|
||||
<div style="padding-left:15px;">Switch between sites</div>
|
||||
</div>
|
||||
<div style="margin-bottom:15px;">
|
||||
<div style="font-weight:bold;color:#3498db;">1-Finger Horizontal Swipe</div>
|
||||
<div style="padding-left:15px;">Navigate within page (arrow keys)</div>
|
||||
</div>
|
||||
<div style="margin-bottom:15px;">
|
||||
<div style="font-weight:bold;color:#9b59b6;">3-Finger Down Swipe</div>
|
||||
<div style="padding-left:15px;">Toggle hidden tabs (PIN required)</div>
|
||||
</div>
|
||||
<div style="margin-bottom:25px;padding-top:15px;border-top:1px solid rgba(255,255,255,0.2);">
|
||||
<div style="font-weight:bold;color:#e74c3c;">Keyboard Shortcuts</div>
|
||||
</div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="padding-left:15px;"><kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">Ctrl+Tab</kbd> or <kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">Ctrl+]</kbd> Next tab</div>
|
||||
</div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="padding-left:15px;"><kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">Ctrl+Shift+Tab</kbd> or <kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">Ctrl+[</kbd> Previous tab</div>
|
||||
</div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="padding-left:15px;"><kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">F10</kbd> or <kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">Ctrl+H</kbd> Toggle hidden tabs</div>
|
||||
</div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="padding-left:15px;"><kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">Escape</kbd> Return to normal tabs</div>
|
||||
</div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="padding-left:15px;"><kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">Ctrl+Alt+Delete</kbd> or <kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">Ctrl+Alt+P</kbd> Power menu</div>
|
||||
</div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="padding-left:15px;"><kbd style="background:rgba(255,255,255,0.2);padding:2px 8px;border-radius:3px;">Ctrl+K</kbd> Toggle keyboard</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
columns.appendChild(sitesCol);
|
||||
columns.appendChild(cheatCol);
|
||||
content.appendChild(columns);
|
||||
navMenu.appendChild(content);
|
||||
|
||||
navMenu.addEventListener('click',(e)=>{
|
||||
if(e.target===navMenu){
|
||||
console.log('[NAV] Background clicked, closing menu');
|
||||
hideNavMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent clicks inside content from closing menu
|
||||
content.addEventListener('click',(e)=>{
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
document.body.appendChild(navMenu);
|
||||
console.log('[NAV] Navigation menu created and appended to body');
|
||||
}
|
||||
|
||||
function toggleNavMenu(){
|
||||
console.log('[NAV] Toggle menu, current state:',navMenuVisible);
|
||||
if(navMenuVisible){
|
||||
hideNavMenu();
|
||||
}else{
|
||||
showNavMenu();
|
||||
}
|
||||
}
|
||||
|
||||
function showNavMenu(){
|
||||
console.log('[NAV] Showing navigation menu');
|
||||
try{
|
||||
if(!navMenu){
|
||||
createNavMenu();
|
||||
}
|
||||
|
||||
// Request sites data
|
||||
loadSitesIntoNav();
|
||||
|
||||
navMenu.style.display='flex';
|
||||
navMenuVisible=true;
|
||||
|
||||
// Force reflow and repaint to ensure proper rendering
|
||||
navMenu.offsetHeight;
|
||||
navMenu.style.opacity='0';
|
||||
setTimeout(()=>{
|
||||
navMenu.style.transition='opacity 0.15s ease-in';
|
||||
navMenu.style.opacity='1';
|
||||
},10);
|
||||
|
||||
// Set 30-second auto-dismiss timer
|
||||
if(navMenuTimer){
|
||||
clearTimeout(navMenuTimer);
|
||||
}
|
||||
navMenuTimer=setTimeout(()=>{
|
||||
console.log('[NAV] Auto-dismissing menu after 30 seconds');
|
||||
hideNavMenu();
|
||||
},NAV_MENU_TIMEOUT);
|
||||
|
||||
console.log('[NAV] Menu displayed, 30-second timer started');
|
||||
}catch(err){
|
||||
console.error('[NAV] Error showing menu:',err);
|
||||
}
|
||||
}
|
||||
|
||||
function hideNavMenu(){
|
||||
console.log('[NAV] Hiding navigation menu');
|
||||
try{
|
||||
if(navMenuTimer){
|
||||
clearTimeout(navMenuTimer);
|
||||
navMenuTimer=null;
|
||||
}
|
||||
if(navMenu){
|
||||
navMenu.style.display='none';
|
||||
navMenu.style.opacity='1';
|
||||
navMenu.style.transition='';
|
||||
}
|
||||
navMenuVisible=false;
|
||||
console.log('[NAV] Menu hidden');
|
||||
}catch(err){
|
||||
console.error('[NAV] Error hiding menu:',err);
|
||||
}
|
||||
}
|
||||
|
||||
// Power menu overlay (with 30-second auto-dismiss)
|
||||
let powerMenu=null;
|
||||
let powerMenuVisible=false;
|
||||
let powerMenuTimer=null;
|
||||
const POWER_MENU_TIMEOUT=30000;
|
||||
let powerMenuInfo={version:'',localIP:'',vpnIP:''};
|
||||
|
||||
function createPowerMenu(){
|
||||
if(powerMenu)return;
|
||||
console.log('[POWER-MENU] Creating power menu');
|
||||
|
||||
powerMenu=document.createElement('div');
|
||||
powerMenu.id='electron-power-menu';
|
||||
powerMenu.style.cssText=`
|
||||
position:fixed;top:0;left:0;width:100%;height:100%;
|
||||
background:rgba(0,0,0,0.9);display:none;align-items:center;justify-content:center;
|
||||
z-index:999998;pointer-events:auto;
|
||||
`;
|
||||
|
||||
const content=document.createElement('div');
|
||||
content.style.cssText=`
|
||||
position:relative;background:rgba(44,62,80,0.98);border-radius:20px;padding:40px;
|
||||
min-width:400px;max-width:90%;box-shadow:0 10px 40px rgba(0,0,0,0.5);text-align:center;
|
||||
`;
|
||||
|
||||
const closeBtn=document.createElement('div');
|
||||
closeBtn.innerHTML='✕';
|
||||
closeBtn.style.cssText=`
|
||||
position:absolute;top:10px;right:10px;font-size:32px;color:white;
|
||||
cursor:pointer;width:40px;height:40px;display:flex;align-items:center;
|
||||
justify-content:center;border-radius:50%;background:rgba(231,76,60,0.8);
|
||||
user-select:none;
|
||||
`;
|
||||
closeBtn.addEventListener('click',(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hidePowerMenu();
|
||||
});
|
||||
content.appendChild(closeBtn);
|
||||
|
||||
const title=document.createElement('h2');
|
||||
title.textContent='Power Options';
|
||||
title.style.cssText='color:white;margin-bottom:20px;font-size:28px;';
|
||||
content.appendChild(title);
|
||||
|
||||
const infoDiv=document.createElement('div');
|
||||
infoDiv.id='power-menu-info';
|
||||
infoDiv.style.cssText='color:#bdc3c7;margin-bottom:30px;font-size:14px;line-height:1.6;';
|
||||
content.appendChild(infoDiv);
|
||||
|
||||
const buttonsDiv=document.createElement('div');
|
||||
buttonsDiv.style.cssText='display:flex;flex-direction:column;gap:15px;';
|
||||
|
||||
const btnStyle=`
|
||||
padding:20px 40px;font-size:20px;border:none;border-radius:10px;
|
||||
cursor:pointer;font-weight:bold;transition:transform 0.2s,opacity 0.2s;
|
||||
`;
|
||||
|
||||
const shutdownBtn=document.createElement('button');
|
||||
shutdownBtn.textContent='⏻ Shutdown';
|
||||
shutdownBtn.style.cssText=btnStyle+'background:#e74c3c;color:white;';
|
||||
shutdownBtn.addEventListener('click',()=>{
|
||||
hidePowerMenu();
|
||||
ipcRenderer.send('power-action','shutdown');
|
||||
});
|
||||
|
||||
const restartBtn=document.createElement('button');
|
||||
restartBtn.textContent='↻ Restart';
|
||||
restartBtn.style.cssText=btnStyle+'background:#f39c12;color:white;';
|
||||
restartBtn.addEventListener('click',()=>{
|
||||
hidePowerMenu();
|
||||
ipcRenderer.send('power-action','restart');
|
||||
});
|
||||
|
||||
const reloadBtn=document.createElement('button');
|
||||
reloadBtn.textContent='⟳ Reload App';
|
||||
reloadBtn.style.cssText=btnStyle+'background:#3498db;color:white;';
|
||||
reloadBtn.addEventListener('click',()=>{
|
||||
hidePowerMenu();
|
||||
ipcRenderer.send('power-action','reload');
|
||||
});
|
||||
|
||||
const cancelBtn=document.createElement('button');
|
||||
cancelBtn.textContent='Cancel';
|
||||
cancelBtn.style.cssText=btnStyle+'background:#7f8c8d;color:white;';
|
||||
cancelBtn.addEventListener('click',()=>{
|
||||
hidePowerMenu();
|
||||
});
|
||||
|
||||
buttonsDiv.appendChild(shutdownBtn);
|
||||
buttonsDiv.appendChild(restartBtn);
|
||||
buttonsDiv.appendChild(reloadBtn);
|
||||
buttonsDiv.appendChild(cancelBtn);
|
||||
content.appendChild(buttonsDiv);
|
||||
|
||||
powerMenu.appendChild(content);
|
||||
|
||||
powerMenu.addEventListener('click',(e)=>{
|
||||
if(e.target===powerMenu){
|
||||
hidePowerMenu();
|
||||
}
|
||||
});
|
||||
|
||||
content.addEventListener('click',(e)=>{
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
document.body.appendChild(powerMenu);
|
||||
}
|
||||
|
||||
function showPowerMenu(info){
|
||||
console.log('[POWER-MENU] Showing power menu');
|
||||
try{
|
||||
if(!powerMenu)createPowerMenu();
|
||||
|
||||
// Update info display
|
||||
const infoDiv=document.getElementById('power-menu-info');
|
||||
if(infoDiv&&info){
|
||||
let infoText='Version: '+info.version+'<br>Local: '+info.localIP;
|
||||
if(info.vpnIP){
|
||||
infoText+='<br>VPN: '+info.vpnIP;
|
||||
}
|
||||
infoDiv.innerHTML=infoText;
|
||||
}
|
||||
|
||||
powerMenu.style.display='flex';
|
||||
powerMenuVisible=true;
|
||||
|
||||
// Set 30-second auto-dismiss timer
|
||||
if(powerMenuTimer){
|
||||
clearTimeout(powerMenuTimer);
|
||||
}
|
||||
powerMenuTimer=setTimeout(()=>{
|
||||
console.log('[POWER-MENU] Auto-dismissing after 30 seconds');
|
||||
hidePowerMenu();
|
||||
},POWER_MENU_TIMEOUT);
|
||||
|
||||
}catch(err){
|
||||
console.error('[POWER-MENU] Error showing menu:',err);
|
||||
}
|
||||
}
|
||||
|
||||
function hidePowerMenu(){
|
||||
console.log('[POWER-MENU] Hiding power menu');
|
||||
try{
|
||||
if(powerMenuTimer){
|
||||
clearTimeout(powerMenuTimer);
|
||||
powerMenuTimer=null;
|
||||
}
|
||||
if(powerMenu){
|
||||
powerMenu.style.display='none';
|
||||
}
|
||||
powerMenuVisible=false;
|
||||
}catch(err){
|
||||
console.error('[POWER-MENU] Error hiding menu:',err);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for power menu display request from main process
|
||||
ipcRenderer.on('display-power-menu',(event,info)=>{
|
||||
showPowerMenu(info);
|
||||
});
|
||||
|
||||
function loadSitesIntoNav(){
|
||||
console.log('[NAV] Requesting config from main process');
|
||||
try{
|
||||
ipcRenderer.send('get-config');
|
||||
}catch(err){
|
||||
console.error('[NAV] Error requesting config:',err);
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.on('config-data',(event,config)=>{
|
||||
console.log('[NAV] Received config data:',config);
|
||||
try{
|
||||
const sitesList=document.getElementById('nav-sites-list');
|
||||
if(!sitesList){
|
||||
console.error('[NAV] Sites list element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!config||!config.tabs){
|
||||
console.error('[NAV] Invalid config data');
|
||||
sitesList.innerHTML='<div style="color:white;padding:10px;">No sites configured</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
sitesList.innerHTML='';
|
||||
let siteCount=0;
|
||||
|
||||
config.tabs.forEach((tab,index)=>{
|
||||
// Skip hidden tabs (duration === -1)
|
||||
if(tab.duration===-1){
|
||||
console.log('[NAV] Skipping hidden tab at index',index);
|
||||
return;
|
||||
}
|
||||
|
||||
const siteBtn=document.createElement('div');
|
||||
const displayName=tab.name||tab.url;
|
||||
siteBtn.textContent=displayName;
|
||||
siteBtn.style.cssText=`
|
||||
padding:15px 20px;background:rgba(52,152,219,0.7);color:white;
|
||||
border-radius:10px;cursor:pointer;font-size:18px;
|
||||
transition:all 0.3s;border:3px solid rgba(52,152,219,0.9);
|
||||
user-select:none;font-weight:normal;
|
||||
box-shadow:0 2px 8px rgba(0,0,0,0.2);
|
||||
`;
|
||||
siteBtn.addEventListener('mouseenter',()=>{
|
||||
siteBtn.style.background='rgba(41,128,185,1)';
|
||||
siteBtn.style.borderColor='rgba(255,255,255,0.9)';
|
||||
siteBtn.style.fontWeight='bold';
|
||||
siteBtn.style.transform='translateY(-2px)';
|
||||
siteBtn.style.boxShadow='0 4px 12px rgba(0,0,0,0.4)';
|
||||
});
|
||||
siteBtn.addEventListener('mouseleave',()=>{
|
||||
siteBtn.style.background='rgba(52,152,219,0.7)';
|
||||
siteBtn.style.borderColor='rgba(52,152,219,0.9)';
|
||||
siteBtn.style.fontWeight='normal';
|
||||
siteBtn.style.transform='translateY(0)';
|
||||
siteBtn.style.boxShadow='0 2px 8px rgba(0,0,0,0.2)';
|
||||
});
|
||||
siteBtn.addEventListener('mousedown',()=>{
|
||||
siteBtn.style.background='rgba(31,97,141,1)';
|
||||
siteBtn.style.transform='translateY(0)';
|
||||
siteBtn.style.boxShadow='0 1px 4px rgba(0,0,0,0.3)';
|
||||
});
|
||||
siteBtn.addEventListener('click',(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('[NAV] Navigating to tab',index);
|
||||
try{
|
||||
ipcRenderer.send('navigate-to-tab',index);
|
||||
hideNavMenu();
|
||||
}catch(err){
|
||||
console.error('[NAV] Error navigating:',err);
|
||||
}
|
||||
});
|
||||
|
||||
sitesList.appendChild(siteBtn);
|
||||
siteCount++;
|
||||
});
|
||||
|
||||
console.log('[NAV] Loaded',siteCount,'sites into menu');
|
||||
}catch(err){
|
||||
console.error('[NAV] Error processing config data:',err);
|
||||
}
|
||||
});
|
||||
|
||||
function isTextInput(el){
|
||||
if(!el)return false;
|
||||
const tag=(el.tagName||'').toLowerCase();
|
||||
const type=(el.type||'').toLowerCase();
|
||||
const editable=el.isContentEditable||el.contentEditable==='true';
|
||||
return(tag==='input'&&['text','email','password','search','tel','url','number'].includes(type))||tag==='textarea'||editable;
|
||||
}
|
||||
|
||||
document.addEventListener('focusin',(e)=>{
|
||||
if(keyboardButtonEnabled&&isTextInput(e.target)){
|
||||
showKeyboardIcon();
|
||||
}
|
||||
},true);
|
||||
|
||||
document.addEventListener('focusout',(e)=>{
|
||||
if(keyboardButtonEnabled&&isTextInput(e.target)){
|
||||
setTimeout(()=>{
|
||||
if(!isTextInput(document.activeElement)){
|
||||
hideKeyboardIcon();
|
||||
}
|
||||
},100);
|
||||
}
|
||||
},true);
|
||||
|
||||
document.addEventListener('mousedown',(e)=>{
|
||||
if(keyboardButtonEnabled&&isTextInput(e.target)){
|
||||
if(keyboardVisible){
|
||||
if(window.electronAPI?.keyboardActivity){
|
||||
window.electronAPI.keyboardActivity();
|
||||
}
|
||||
}else{
|
||||
keyboardAutoClosedThisSession=false;
|
||||
const now=Date.now();
|
||||
if(now-lastKeyboardRequest>KEYBOARD_REQUEST_THROTTLE){
|
||||
lastKeyboardRequest=now;
|
||||
setTimeout(()=>ipcRenderer.send('show-keyboard'),50);
|
||||
}
|
||||
}
|
||||
}
|
||||
},true);
|
||||
|
||||
// Shared debounce prevents double-firing when both touch and pointer events fire
|
||||
let lastSwipeSent=0;
|
||||
function sendSwipeIPC(direction){
|
||||
const now=Date.now();
|
||||
if(now-lastSwipeSent<500)return;
|
||||
lastSwipeSent=now;
|
||||
ipcRenderer.send(direction);
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart',e=>{
|
||||
if(e.touches.length>=1){
|
||||
touchStartX=e.touches[0].clientX;
|
||||
touchStartY=e.touches[0].clientY;
|
||||
touchStartTime=Date.now();
|
||||
fingerCount=e.touches.length;
|
||||
}
|
||||
},{passive:true});
|
||||
|
||||
document.addEventListener('touchend',e=>{
|
||||
if(e.changedTouches.length>=1){
|
||||
const touchEndX=e.changedTouches[0].clientX;
|
||||
const touchEndY=e.changedTouches[0].clientY;
|
||||
const deltaX=touchEndX-touchStartX;
|
||||
const deltaY=touchEndY-touchStartY;
|
||||
const deltaTime=Date.now()-touchStartTime;
|
||||
|
||||
if(deltaTime>SWIPE_MAX_TIME)return;
|
||||
|
||||
const absX=Math.abs(deltaX);
|
||||
const absY=Math.abs(deltaY);
|
||||
|
||||
if(fingerCount===3&&absY>SWIPE_THRESHOLD&&absX<SWIPE_TOLERANCE&&deltaY>0){
|
||||
console.log('[TOUCH] 3-finger DOWN - toggle hidden tabs');
|
||||
ipcRenderer.send('toggle-hidden');
|
||||
}else if(fingerCount===2&&absX>SWIPE_THRESHOLD&&absY<SWIPE_TOLERANCE){
|
||||
console.log('[TOUCH] 2-finger HORIZONTAL - change tab');
|
||||
sendSwipeIPC(deltaX>0?'swipe-right':'swipe-left');
|
||||
}else if(fingerCount===1&&absX>SWIPE_THRESHOLD&&absY<SWIPE_TOLERANCE){
|
||||
const key=deltaX>0?'ArrowRight':'ArrowLeft';
|
||||
const keyCode=deltaX>0?39:37;
|
||||
['keydown','keyup'].forEach(eventType=>{
|
||||
document.dispatchEvent(new KeyboardEvent(eventType,{
|
||||
key:key,code:key,keyCode:keyCode,which:keyCode,bubbles:true,cancelable:true
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
},{passive:true});
|
||||
|
||||
// Pointer event fallback — handles devices/drivers where touchstart/touchend don't fire
|
||||
// (e.g. Electron 42 on some Linux touchscreen drivers that only generate PointerEvents)
|
||||
let ptrIds=new Set();
|
||||
let ptrPeak=0;
|
||||
let ptrStartX=0,ptrStartY=0,ptrStartTime=0;
|
||||
|
||||
document.addEventListener('pointerdown',e=>{
|
||||
if(e.pointerType!=='touch')return;
|
||||
ptrIds.add(e.pointerId);
|
||||
if(ptrIds.size===1){ptrStartX=e.clientX;ptrStartY=e.clientY;ptrStartTime=Date.now();ptrPeak=1;}
|
||||
else{ptrPeak=Math.max(ptrPeak,ptrIds.size);}
|
||||
},{passive:true});
|
||||
|
||||
document.addEventListener('pointerup',e=>{
|
||||
if(e.pointerType!=='touch')return;
|
||||
ptrIds.delete(e.pointerId);
|
||||
if(ptrIds.size!==0)return;
|
||||
const deltaTime=Date.now()-ptrStartTime;
|
||||
if(deltaTime>SWIPE_MAX_TIME){ptrPeak=0;return;}
|
||||
const deltaX=e.clientX-ptrStartX;
|
||||
const deltaY=e.clientY-ptrStartY;
|
||||
const absX=Math.abs(deltaX);
|
||||
const absY=Math.abs(deltaY);
|
||||
if(ptrPeak===3&&absY>SWIPE_THRESHOLD&&absX<SWIPE_TOLERANCE&&deltaY>0){
|
||||
console.log('[TOUCH] 3-finger DOWN (ptr) - toggle hidden tabs');
|
||||
ipcRenderer.send('toggle-hidden');
|
||||
}else if(ptrPeak===2&&absX>SWIPE_THRESHOLD&&absY<SWIPE_TOLERANCE){
|
||||
console.log('[TOUCH] 2-finger HORIZONTAL (ptr) - change tab');
|
||||
sendSwipeIPC(deltaX>0?'swipe-right':'swipe-left');
|
||||
}else if(ptrPeak===1&&absX>SWIPE_THRESHOLD&&absY<SWIPE_TOLERANCE){
|
||||
const key=deltaX>0?'ArrowRight':'ArrowLeft';
|
||||
const keyCode=deltaX>0?39:37;
|
||||
['keydown','keyup'].forEach(eventType=>{
|
||||
document.dispatchEvent(new KeyboardEvent(eventType,{
|
||||
key:key,code:key,keyCode:keyCode,which:keyCode,bubbles:true,cancelable:true
|
||||
}));
|
||||
});
|
||||
}
|
||||
ptrPeak=0;
|
||||
},{passive:true});
|
||||
|
||||
// Show pause button on user interaction (for rotation sites only)
|
||||
let lastUserInteraction=0;
|
||||
const USER_INTERACTION_THROTTLE=500;
|
||||
|
||||
function handleUserInteraction(eventType){
|
||||
const now=Date.now();
|
||||
if(now-lastUserInteraction<USER_INTERACTION_THROTTLE)return;
|
||||
lastUserInteraction=now;
|
||||
|
||||
console.log('[PAUSE-BTN] User interaction ('+eventType+') - shouldShow='+pauseButtonShouldShow+', shown='+pauseButtonShown);
|
||||
// Show/refresh pause button if allowed on this site
|
||||
if(pauseButtonShouldShow){
|
||||
if(!pauseButtonShown){
|
||||
console.log('[PAUSE-BTN] Showing pause button now');
|
||||
}else{
|
||||
console.log('[PAUSE-BTN] Resetting auto-hide timer');
|
||||
}
|
||||
showPauseButton(); // This will reset the hide timer
|
||||
}
|
||||
|
||||
// Always show navigation button on user interaction (if enabled)
|
||||
if(navButtonEnabled){
|
||||
showNavButton();
|
||||
}
|
||||
|
||||
// Always show power button on user interaction
|
||||
showPowerButton();
|
||||
}
|
||||
|
||||
// Show pause button on any user interaction
|
||||
const pauseButtonTriggers=['mousedown','touchstart','keydown'];
|
||||
pauseButtonTriggers.forEach(eventType=>{
|
||||
document.addEventListener(eventType,()=>handleUserInteraction(eventType),{passive:true,capture:true});
|
||||
});
|
||||
});
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
cd /home/kiosk/kiosk-app
|
||||
|
||||
# Wait for network
|
||||
for i in {1..30}; do
|
||||
ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1 && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
export DISPLAY=:0
|
||||
export XAUTHORITY=/home/kiosk/.Xauthority
|
||||
export ELECTRON_ENABLE_LOGGING=1
|
||||
|
||||
# Ensure PipeWire is running
|
||||
systemctl --user is-active --quiet pipewire || systemctl --user start pipewire
|
||||
systemctl --user is-active --quiet pipewire-pulse || systemctl --user start pipewire-pulse
|
||||
systemctl --user is-active --quiet wireplumber || systemctl --user start wireplumber
|
||||
|
||||
# Wait for PipeWire
|
||||
for i in {1..10}; do
|
||||
pactl info >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
exec node_modules/electron/dist/electron . \
|
||||
--no-sandbox --disable-gpu-sandbox --disable-dev-shm-usage \
|
||||
--enable-features=UseOzonePlatform --ozone-platform=x11 \
|
||||
--enable-audio-service-sandbox=false --autoplay-policy=no-user-gesture-required \
|
||||
--password-store=basic \
|
||||
2>&1 | tee -a /home/kiosk/electron.log
|
||||
Reference in New Issue
Block a user