6c756e3adc
- PyQt5 floating toolbar (Back, Forward, HOME, Bookmarks) over Google Chrome - Scrollable bookmarks bar with nested folder dropdown menus - Chrome DevTools Protocol (CDP) for navigation control - Home screen with large senior-friendly buttons (Gmail, Google, Weather, News, Facebook, Messenger, YouTube) - Automatic GPU/ML cache cleanup on every Chrome launch - Kiosk hardening: TTY switching blocked, keyboard shortcuts intercepted, auto-restart on crash - System config files and install script included
594 lines
24 KiB
Python
Executable File
594 lines
24 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
SeniorNet Kiosk — PyQt5 floating toolbar + bookmarks bar over Google Chrome.
|
||
Chrome is controlled via Chrome DevTools Protocol (CDP) over WebSocket.
|
||
"""
|
||
import sys, json, os, subprocess, time, threading, urllib.request
|
||
from PyQt5.QtCore import Qt, QTimer
|
||
from PyQt5.QtGui import QFont, QColor, QPalette
|
||
from PyQt5.QtWidgets import (
|
||
QApplication, QWidget, QHBoxLayout, QVBoxLayout,
|
||
QPushButton, QLabel, QScrollArea, QDialog, QLineEdit,
|
||
QMessageBox, QFrame, QMenu, QAction,
|
||
QTreeWidget, QTreeWidgetItem, QAbstractItemView
|
||
)
|
||
|
||
HOME_URL = "file:///home/jgitta/kiosk-home/index.html"
|
||
BOOKMARKS_FILE = "/home/jgitta/kiosk-browser/bookmarks.json"
|
||
CDP_PORT = 9222
|
||
TOOLBAR_H = 82
|
||
BM_BAR_H = 60
|
||
TOP_H = TOOLBAR_H + BM_BAR_H # 142 px overlay height
|
||
SCREEN_W = 1920
|
||
SCREEN_H = 1080
|
||
|
||
# ── Colour palette ────────────────────────────────────────────────
|
||
TOOLBAR_COLOR = "#1a3a5c"
|
||
BAR_COLOR = "#162f4a"
|
||
BTN_COLOR = "#2e6da4"; BTN_HOVER = "#3a87cc"
|
||
GOLD = "#e8a020"; GOLD_HOVER = "#f0b030"
|
||
GREEN = "#2e7d32"; GREEN_HOVER = "#388e3c"
|
||
RED = "#c62828"; RED_HOVER = "#d32f2f"
|
||
AMBER = "#7a5a10"; AMBER_HOVER = "#9a7a20"
|
||
PURPLE = "#5a5a8a"; PURPLE_HOVER = "#7a7aaa"
|
||
FOLDER_BG = "#3a4a6a"; FOLDER_HOVER = "#4a5a7a"
|
||
FONT_SIZE = 18
|
||
|
||
MENU_STYLE = """
|
||
QMenu {
|
||
background: #1e3a5c; color: white;
|
||
border: 2px solid #3a6da4; border-radius: 8px; padding: 4px;
|
||
}
|
||
QMenu::item { padding: 12px 32px 12px 14px; border-radius: 6px; font-size: 16px; }
|
||
QMenu::item:selected { background: #3a6da4; color: white; }
|
||
QMenu::separator { height: 1px; background: #3a6da4; margin: 4px 8px; }
|
||
"""
|
||
|
||
# ── Helpers ───────────────────────────────────────────────────────
|
||
def mk_btn(label, color, hover, min_w=120, min_h=52, fs=FONT_SIZE):
|
||
b = QPushButton(label)
|
||
b.setFont(QFont("Arial", fs, QFont.Bold))
|
||
b.setMinimumSize(min_w, min_h)
|
||
b.setCursor(Qt.PointingHandCursor)
|
||
b.setStyleSheet(f"""
|
||
QPushButton {{ background:{color}; color:white; border:none;
|
||
border-radius:10px; padding:6px 14px; }}
|
||
QPushButton:hover {{ background:{hover}; }}
|
||
QPushButton:pressed {{ background:#222; }}
|
||
QPushButton:disabled {{ background:#444; color:#888; }}
|
||
""")
|
||
return b
|
||
|
||
def load_bm():
|
||
try:
|
||
with open(BOOKMARKS_FILE) as f: return json.load(f)
|
||
except: return []
|
||
|
||
def save_bm(data):
|
||
with open(BOOKMARKS_FILE, "w") as f: json.dump(data, f, indent=2)
|
||
|
||
def is_folder(item): return "children" in item
|
||
|
||
|
||
# ── CDP controller ────────────────────────────────────────────────
|
||
class CDP:
|
||
"""Thin Chrome DevTools Protocol client — navigate, back, forward."""
|
||
def __init__(self):
|
||
self.ready = False
|
||
self._lock = threading.Lock()
|
||
|
||
def wait_ready(self, timeout=30):
|
||
for _ in range(timeout * 2):
|
||
try:
|
||
urllib.request.urlopen(
|
||
f"http://127.0.0.1:{CDP_PORT}/json", timeout=1)
|
||
self.ready = True
|
||
print("CDP ready")
|
||
return True
|
||
except Exception:
|
||
time.sleep(0.5)
|
||
print("CDP not available after timeout")
|
||
return False
|
||
|
||
def _ws_url(self):
|
||
raw = urllib.request.urlopen(
|
||
f"http://127.0.0.1:{CDP_PORT}/json", timeout=2).read()
|
||
tabs = json.loads(raw)
|
||
page = next((t for t in tabs if t.get("type") == "page"), None)
|
||
return page["webSocketDebuggerUrl"].replace("localhost", "127.0.0.1") if page else None
|
||
|
||
def _send(self, method, params=None):
|
||
if not self.ready:
|
||
return
|
||
import websocket
|
||
with self._lock:
|
||
try:
|
||
ws = websocket.create_connection(self._ws_url(), timeout=3)
|
||
ws.send(json.dumps({"id": 1, "method": method,
|
||
**({"params": params} if params else {})}))
|
||
ws.recv()
|
||
ws.close()
|
||
except Exception as e:
|
||
print(f"CDP error ({method}): {e}")
|
||
|
||
def _async(self, method, params=None):
|
||
threading.Thread(target=self._send, args=(method, params),
|
||
daemon=True).start()
|
||
|
||
def navigate(self, url):
|
||
self._async("Page.navigate", {"url": url})
|
||
|
||
def back(self):
|
||
self._async("Runtime.evaluate",
|
||
{"expression": "window.history.back()"})
|
||
|
||
def forward(self):
|
||
self._async("Runtime.evaluate",
|
||
{"expression": "window.history.forward()"})
|
||
|
||
|
||
# ── Folder name dialog ────────────────────────────────────────────
|
||
def ask_name(parent, title="Name", current=""):
|
||
d = QDialog(parent)
|
||
d.setWindowTitle(title)
|
||
d.setMinimumWidth(480)
|
||
d.setStyleSheet("background:#1e2a3a; color:white;")
|
||
lay = QVBoxLayout(d)
|
||
lay.setContentsMargins(28, 28, 28, 28)
|
||
lay.setSpacing(16)
|
||
lbl = QLabel("Folder name:")
|
||
lbl.setFont(QFont("Arial", 16))
|
||
lbl.setStyleSheet("color:#aac8e8;")
|
||
lay.addWidget(lbl)
|
||
edit = QLineEdit(current)
|
||
edit.setFont(QFont("Arial", 18))
|
||
edit.setMinimumHeight(50)
|
||
edit.setStyleSheet("background:#2a3f55;color:white;border:2px solid #3a6da4;"
|
||
"border-radius:8px;padding:6px;")
|
||
lay.addWidget(edit)
|
||
row = QHBoxLayout()
|
||
ok = mk_btn("💾 Save", GREEN, GREEN_HOVER, 150, 56, 16)
|
||
can = mk_btn("✖ Cancel", RED, RED_HOVER, 150, 56, 16)
|
||
ok.clicked.connect(d.accept); can.clicked.connect(d.reject)
|
||
row.addStretch(); row.addWidget(ok); row.addWidget(can)
|
||
lay.addLayout(row)
|
||
return edit.text().strip() if d.exec_() == QDialog.Accepted else None
|
||
|
||
|
||
# ── Bookmark add / edit dialog ────────────────────────────────────
|
||
class BookmarkDialog(QDialog):
|
||
def __init__(self, parent=None, name="", url=""):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("Bookmark")
|
||
self.setMinimumWidth(560)
|
||
self.setStyleSheet("background:#1e2a3a; color:white;")
|
||
lay = QVBoxLayout(self)
|
||
lay.setSpacing(18); lay.setContentsMargins(30, 30, 30, 30)
|
||
for lbl_text, attr, val in [("Name:", "name_edit", name),
|
||
("URL:", "url_edit", url)]:
|
||
l = QLabel(lbl_text)
|
||
l.setFont(QFont("Arial", 16)); l.setStyleSheet("color:#aac8e8;")
|
||
lay.addWidget(l)
|
||
e = QLineEdit(val)
|
||
e.setFont(QFont("Arial", 18)); e.setMinimumHeight(50)
|
||
e.setStyleSheet("background:#2a3f55;color:white;border:2px solid #3a6da4;"
|
||
"border-radius:8px;padding:6px;")
|
||
lay.addWidget(e); setattr(self, attr, e)
|
||
row = QHBoxLayout()
|
||
ok = mk_btn("💾 Save", GREEN, GREEN_HOVER, 160, 58, 16)
|
||
can = mk_btn("✖ Cancel", RED, RED_HOVER, 160, 58, 16)
|
||
ok.clicked.connect(self.accept); can.clicked.connect(self.reject)
|
||
row.addStretch(); row.addWidget(ok); row.addWidget(can)
|
||
lay.addLayout(row)
|
||
|
||
def values(self):
|
||
return self.name_edit.text().strip(), self.url_edit.text().strip()
|
||
|
||
|
||
# ── Bookmark Manager ──────────────────────────────────────────────
|
||
class ManagerDialog(QDialog):
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("Manage Bookmarks")
|
||
self.setMinimumSize(860, 640)
|
||
self.setStyleSheet("background:#1e2a3a; color:white;")
|
||
lay = QVBoxLayout(self)
|
||
lay.setContentsMargins(20, 20, 20, 20); lay.setSpacing(12)
|
||
|
||
hdr = QLabel("📋 Bookmarks")
|
||
hdr.setFont(QFont("Arial", 22, QFont.Bold))
|
||
hdr.setStyleSheet("color:white;")
|
||
lay.addWidget(hdr)
|
||
|
||
hint = QLabel("Select a folder to add inside it, or nothing to add at top level. "
|
||
"▲ ▼ move items within the same folder.")
|
||
hint.setFont(QFont("Arial", 13))
|
||
hint.setStyleSheet("color:#88aacc;"); hint.setWordWrap(True)
|
||
lay.addWidget(hint)
|
||
|
||
self.tree = QTreeWidget()
|
||
self.tree.setHeaderHidden(True)
|
||
self.tree.setFont(QFont("Arial", 15))
|
||
self.tree.setSelectionMode(QAbstractItemView.SingleSelection)
|
||
self.tree.setStyleSheet("""
|
||
QTreeWidget { background:#2a3f55; color:white;
|
||
border:2px solid #3a6da4; border-radius:8px; }
|
||
QTreeWidget::item { padding:8px 4px; border-bottom:1px solid #1a2f45; }
|
||
QTreeWidget::item:selected { background:#3a6da4; }
|
||
QTreeWidget::branch { background:#2a3f55; }
|
||
""")
|
||
self.tree.setIndentation(30)
|
||
self._reload_tree()
|
||
lay.addWidget(self.tree)
|
||
|
||
row = QHBoxLayout(); row.setSpacing(8)
|
||
btns = [mk_btn("➕ Bookmark", GREEN, GREEN_HOVER, 155, 54, 14),
|
||
mk_btn("📁 Folder", AMBER, AMBER_HOVER, 130, 54, 14),
|
||
mk_btn("✏️ Edit", BTN_COLOR,BTN_HOVER, 120, 54, 14),
|
||
mk_btn("🗑 Delete", RED, RED_HOVER, 120, 54, 14),
|
||
mk_btn("▲ Up", PURPLE, PURPLE_HOVER, 90, 54, 14),
|
||
mk_btn("▼ Down", PURPLE, PURPLE_HOVER, 90, 54, 14),
|
||
mk_btn("✖ Close", "#555", "#777", 120, 54, 14)]
|
||
acts = [self.add_bookmark, self.add_folder, self.edit_item,
|
||
self.delete_item, lambda: self.move_item(-1),
|
||
lambda: self.move_item(1), self.accept]
|
||
for b, a in zip(btns, acts):
|
||
b.clicked.connect(a); row.addWidget(b)
|
||
lay.addLayout(row)
|
||
|
||
def _reload_tree(self):
|
||
self.tree.clear()
|
||
self._build_nodes(load_bm(), self.tree.invisibleRootItem())
|
||
self.tree.expandAll()
|
||
|
||
def _build_nodes(self, items, parent):
|
||
for item in items:
|
||
node = QTreeWidgetItem(parent)
|
||
if is_folder(item):
|
||
node.setText(0, f"[+] {item['name']}")
|
||
node.setData(0, Qt.UserRole, {"name": item["name"], "_folder": True})
|
||
self._build_nodes(item["children"], node)
|
||
else:
|
||
node.setText(0, f" {item['name']} - {item['url']}")
|
||
node.setData(0, Qt.UserRole, {"name": item["name"], "url": item["url"]})
|
||
|
||
def _to_list(self, parent=None):
|
||
if parent is None: parent = self.tree.invisibleRootItem()
|
||
result = []
|
||
for i in range(parent.childCount()):
|
||
n = parent.child(i); d = n.data(0, Qt.UserRole)
|
||
if "_folder" in d:
|
||
result.append({"name": d["name"], "children": self._to_list(n)})
|
||
else:
|
||
result.append({"name": d["name"], "url": d["url"]})
|
||
return result
|
||
|
||
def _sel(self):
|
||
items = self.tree.selectedItems()
|
||
return items[0] if items else None
|
||
|
||
def _commit(self): save_bm(self._to_list())
|
||
|
||
def _insert(self, node):
|
||
sel = self._sel()
|
||
if sel:
|
||
d = sel.data(0, Qt.UserRole)
|
||
if "_folder" in d:
|
||
sel.addChild(node); sel.setExpanded(True); return
|
||
par = sel.parent() or self.tree.invisibleRootItem()
|
||
par.insertChild(par.indexOfChild(sel) + 1, node)
|
||
else:
|
||
self.tree.invisibleRootItem().addChild(node)
|
||
|
||
def add_bookmark(self):
|
||
dlg = BookmarkDialog(self)
|
||
if dlg.exec_() != QDialog.Accepted: return
|
||
name, url = dlg.values()
|
||
if not name or not url: return
|
||
node = QTreeWidgetItem()
|
||
node.setText(0, f" {name} - {url}")
|
||
node.setData(0, Qt.UserRole, {"name": name, "url": url})
|
||
self._insert(node); self._commit()
|
||
|
||
def add_folder(self):
|
||
name = ask_name(self, "New Folder")
|
||
if not name: return
|
||
node = QTreeWidgetItem()
|
||
node.setText(0, f"[+] {name}")
|
||
node.setData(0, Qt.UserRole, {"name": name, "_folder": True})
|
||
self._insert(node); self._commit()
|
||
|
||
def edit_item(self):
|
||
sel = self._sel()
|
||
if not sel: return
|
||
d = sel.data(0, Qt.UserRole)
|
||
if "_folder" in d:
|
||
name = ask_name(self, "Edit Folder", d["name"])
|
||
if not name: return
|
||
sel.setText(0, f"[+] {name}")
|
||
sel.setData(0, Qt.UserRole, {"name": name, "_folder": True})
|
||
else:
|
||
dlg = BookmarkDialog(self, d["name"], d["url"])
|
||
if dlg.exec_() != QDialog.Accepted: return
|
||
name, url = dlg.values()
|
||
if not name or not url: return
|
||
sel.setText(0, f" {name} - {url}")
|
||
sel.setData(0, Qt.UserRole, {"name": name, "url": url})
|
||
self._commit()
|
||
|
||
def delete_item(self):
|
||
sel = self._sel()
|
||
if not sel: return
|
||
d = sel.data(0, Qt.UserRole)
|
||
fld = "_folder" in d
|
||
msg = (f"Delete folder '{d['name']}' and ALL its bookmarks?"
|
||
if fld else f"Delete '{d['name']}'?")
|
||
box = QMessageBox(self)
|
||
box.setWindowTitle("Delete"); box.setText(msg)
|
||
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
||
box.setStyleSheet("background:#1e2a3a; color:white; font-size:17px;")
|
||
if box.exec_() == QMessageBox.Yes:
|
||
par = sel.parent() or self.tree.invisibleRootItem()
|
||
par.removeChild(sel); self._commit()
|
||
|
||
def move_item(self, direction):
|
||
sel = self._sel()
|
||
if not sel: return
|
||
par = sel.parent() or self.tree.invisibleRootItem()
|
||
idx = par.indexOfChild(sel); ni = idx + direction
|
||
if 0 <= ni < par.childCount():
|
||
par.takeChild(idx); par.insertChild(ni, sel)
|
||
self.tree.setCurrentItem(sel); self._commit()
|
||
|
||
|
||
# ── Bookmarks Bar ─────────────────────────────────────────────────
|
||
class BookmarksBar(QWidget):
|
||
def __init__(self, nav_cb):
|
||
super().__init__()
|
||
self.nav_cb = nav_cb
|
||
self.setFixedHeight(BM_BAR_H)
|
||
self.setStyleSheet(f"background:{BAR_COLOR};")
|
||
|
||
outer = QHBoxLayout(self)
|
||
outer.setContentsMargins(6, 5, 6, 5); outer.setSpacing(4)
|
||
|
||
self.left_btn = self._arrow("◀")
|
||
self.left_btn.clicked.connect(lambda: self._scroll(-240))
|
||
outer.addWidget(self.left_btn)
|
||
|
||
self.scroll_area = QScrollArea()
|
||
self.scroll_area.setWidgetResizable(True)
|
||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||
self.scroll_area.setFrameShape(QFrame.NoFrame)
|
||
self.scroll_area.setStyleSheet(f"background:{BAR_COLOR}; border:none;")
|
||
|
||
self.inner = QWidget()
|
||
self.inner.setStyleSheet(f"background:{BAR_COLOR};")
|
||
self.row = QHBoxLayout(self.inner)
|
||
self.row.setContentsMargins(4, 0, 4, 0); self.row.setSpacing(6)
|
||
self.scroll_area.setWidget(self.inner)
|
||
outer.addWidget(self.scroll_area)
|
||
|
||
self.right_btn = self._arrow("▶")
|
||
self.right_btn.clicked.connect(lambda: self._scroll(240))
|
||
outer.addWidget(self.right_btn)
|
||
|
||
self.reload()
|
||
|
||
def _arrow(self, label):
|
||
b = QPushButton(label)
|
||
b.setFixedSize(38, 42); b.setFont(QFont("Arial", 14, QFont.Bold))
|
||
b.setCursor(Qt.PointingHandCursor)
|
||
b.setStyleSheet("QPushButton { background:#2a4a6c; color:white; border:none;"
|
||
" border-radius:8px; } QPushButton:hover { background:#3a6da4; }")
|
||
return b
|
||
|
||
def reload(self):
|
||
while self.row.count():
|
||
w = self.row.takeAt(0).widget()
|
||
if w: w.deleteLater()
|
||
for item in load_bm():
|
||
b = self._folder_btn(item) if is_folder(item) else self._bm_btn(item["name"], item["url"])
|
||
self.row.addWidget(b)
|
||
self.row.addStretch()
|
||
|
||
def _bm_btn(self, name, url):
|
||
b = QPushButton(name)
|
||
b.setFont(QFont("Arial", 15)); b.setMinimumHeight(44); b.setMinimumWidth(100)
|
||
b.setCursor(Qt.PointingHandCursor); b.setToolTip(url)
|
||
b.setStyleSheet("QPushButton { background:#2e5c8a; color:white; border:none;"
|
||
" border-radius:8px; padding:4px 12px; }"
|
||
"QPushButton:hover { background:#3a7abf; }"
|
||
"QPushButton:pressed { background:#1a3a5c; }")
|
||
b.clicked.connect(lambda _, u=url: self.nav_cb(u))
|
||
return b
|
||
|
||
def _folder_btn(self, folder):
|
||
b = QPushButton(f"+ {folder['name']}")
|
||
b.setFont(QFont("Arial", 15)); b.setMinimumHeight(44); b.setMinimumWidth(110)
|
||
b.setCursor(Qt.PointingHandCursor)
|
||
b.setStyleSheet(f"QPushButton {{ background:{FOLDER_BG}; color:white; border:none;"
|
||
f" border-radius:8px; padding:4px 14px; }}"
|
||
f"QPushButton:hover {{ background:{FOLDER_HOVER}; }}"
|
||
f"QPushButton:pressed {{ background:#2a3a5a; }}")
|
||
b.clicked.connect(lambda _, f=folder, bb=b: self._show_menu(f, bb))
|
||
return b
|
||
|
||
def _show_menu(self, folder, button):
|
||
self._build_menu(folder["children"]).exec_(
|
||
button.mapToGlobal(button.rect().bottomLeft()))
|
||
|
||
def _build_menu(self, items):
|
||
menu = QMenu(); menu.setFont(QFont("Arial", 16)); menu.setStyleSheet(MENU_STYLE)
|
||
for item in items:
|
||
if is_folder(item):
|
||
sub = self._build_menu(item["children"])
|
||
sub.setTitle(f" > {item['name']}")
|
||
sub.setFont(QFont("Arial", 16)); sub.setStyleSheet(MENU_STYLE)
|
||
menu.addMenu(sub)
|
||
else:
|
||
act = QAction(f" {item['name']}", menu)
|
||
url = item["url"]
|
||
act.triggered.connect(lambda _, u=url: self.nav_cb(u))
|
||
menu.addAction(act)
|
||
return menu
|
||
|
||
def _scroll(self, dx):
|
||
sb = self.scroll_area.horizontalScrollBar()
|
||
sb.setValue(sb.value() + dx)
|
||
|
||
|
||
# ── Floating toolbar overlay ──────────────────────────────────────
|
||
class KioskOverlay(QWidget):
|
||
def __init__(self, cdp):
|
||
super().__init__()
|
||
self.cdp = cdp
|
||
self.setWindowFlags(
|
||
Qt.FramelessWindowHint |
|
||
Qt.WindowStaysOnTopHint |
|
||
Qt.Tool
|
||
)
|
||
self.setAttribute(Qt.WA_ShowWithoutActivating)
|
||
self.setGeometry(0, 0, SCREEN_W, TOP_H)
|
||
self.setStyleSheet("background:transparent;")
|
||
QApplication.instance().installEventFilter(self)
|
||
|
||
vbox = QVBoxLayout(self)
|
||
vbox.setContentsMargins(0, 0, 0, 0); vbox.setSpacing(0)
|
||
vbox.addWidget(self._build_toolbar())
|
||
self.bm_bar = BookmarksBar(cdp.navigate)
|
||
vbox.addWidget(self.bm_bar)
|
||
self.show()
|
||
|
||
def _build_toolbar(self):
|
||
bar = QWidget(); bar.setFixedHeight(TOOLBAR_H)
|
||
bar.setStyleSheet(f"background:{TOOLBAR_COLOR};")
|
||
tl = QHBoxLayout(bar); tl.setContentsMargins(16, 8, 16, 8); tl.setSpacing(12)
|
||
|
||
brand = QLabel("SeniorNet")
|
||
brand.setFont(QFont("Arial", 24, QFont.Bold))
|
||
brand.setStyleSheet("color:white;")
|
||
tl.addWidget(brand); tl.addStretch()
|
||
|
||
back_btn = mk_btn("< Back", BTN_COLOR, BTN_HOVER, 120, 62)
|
||
fwd_btn = mk_btn("Forward >", BTN_COLOR, BTN_HOVER, 140, 62)
|
||
home_btn = mk_btn("HOME", GOLD, GOLD_HOVER, 120, 62)
|
||
mgr_btn = mk_btn("Bookmarks", PURPLE, PURPLE_HOVER, 150, 62)
|
||
|
||
back_btn.clicked.connect(self.cdp.back)
|
||
fwd_btn.clicked.connect(self.cdp.forward)
|
||
home_btn.clicked.connect(lambda: self.cdp.navigate(HOME_URL))
|
||
mgr_btn.clicked.connect(self.open_manager)
|
||
|
||
for b in [back_btn, fwd_btn, home_btn, mgr_btn]: tl.addWidget(b)
|
||
return bar
|
||
|
||
def open_manager(self):
|
||
d = ManagerDialog(self); d.exec_()
|
||
self.bm_bar.reload()
|
||
|
||
def keyPressEvent(self, e):
|
||
# Home key -> home screen
|
||
if e.key() == Qt.Key_Home:
|
||
self.cdp.navigate(HOME_URL)
|
||
# Block all other dangerous keys silently
|
||
e.accept()
|
||
|
||
def eventFilter(self, obj, event):
|
||
from PyQt5.QtCore import QEvent
|
||
if event.type() == QEvent.KeyPress:
|
||
key = event.key()
|
||
mods = event.modifiers()
|
||
# Block Alt+F4, Ctrl+W, Ctrl+T, Ctrl+N, Ctrl+Tab, F11, Escape
|
||
blocked_keys = {Qt.Key_F4, Qt.Key_W, Qt.Key_T, Qt.Key_N,
|
||
Qt.Key_Tab, Qt.Key_F11, Qt.Key_Escape,
|
||
Qt.Key_F1, Qt.Key_F2, Qt.Key_F3,
|
||
Qt.Key_F5, Qt.Key_F6, Qt.Key_F10}
|
||
if mods & (Qt.AltModifier | Qt.ControlModifier):
|
||
if key in blocked_keys:
|
||
return True # swallow the event
|
||
# Also block bare Escape and F-keys
|
||
if key in {Qt.Key_Escape, Qt.Key_F11}:
|
||
return True
|
||
return super().eventFilter(obj, event)
|
||
|
||
|
||
# ── Chrome launcher ───────────────────────────────────────────────
|
||
def launch_chrome():
|
||
import pathlib, json as _json, shutil
|
||
profile_dir = pathlib.Path("/home/jgitta/.config/google-chrome-kiosk")
|
||
profile_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# Remove stale singleton locks
|
||
for name in ("SingletonLock", "SingletonSocket", "SingletonCookie"):
|
||
try: (profile_dir / name).unlink()
|
||
except FileNotFoundError: pass
|
||
|
||
# Wipe GPU/ML caches that corrupt across restarts
|
||
for rel in ("GraphiteDawnCache", "GrShaderCache", "ShaderCache",
|
||
"OptGuideOnDeviceClassifierModel", "OptGuideOnDeviceModel",
|
||
"optimization_guide_model_store",
|
||
"Default/GPUCache", "Default/DawnWebGPUCache",
|
||
"Default/DawnGraphiteCache", "Default/Segmentation Platform"):
|
||
p = profile_dir / rel
|
||
if p.exists():
|
||
shutil.rmtree(p, ignore_errors=True)
|
||
|
||
# Patch Local State
|
||
ls_path = profile_dir / "Local State"
|
||
if ls_path.exists():
|
||
try:
|
||
d = _json.loads(ls_path.read_text())
|
||
d.setdefault("profile", {})["exit_type"] = "Normal"
|
||
d["hardware_acceleration_mode_previous"] = False
|
||
ls_path.write_text(_json.dumps(d))
|
||
except Exception:
|
||
pass
|
||
|
||
chrome_h = SCREEN_H - TOP_H
|
||
cmd = [
|
||
"google-chrome",
|
||
"--no-first-run",
|
||
"--no-default-browser-check",
|
||
"--disable-translate",
|
||
"--disable-infobars",
|
||
"--disable-session-crashed-bubble",
|
||
"--disable-save-password-bubble",
|
||
"--disable-restore-session-state",
|
||
"--disable-features=TranslateUI,OptimizationHints,OptimizationHintsFetching,OptimizationTargetPrediction,OptimizationGuideModelDownloading",
|
||
"--disable-component-update",
|
||
"--disable-background-networking",
|
||
"--disable-extensions",
|
||
"--disable-gpu",
|
||
"--no-sandbox",
|
||
"--test-type",
|
||
"--disable-dev-shm-usage",
|
||
"--user-data-dir=/home/jgitta/.config/google-chrome-kiosk",
|
||
f"--remote-debugging-port={CDP_PORT}",
|
||
"--remote-allow-origins=*",
|
||
f"--window-position=0,{TOP_H}",
|
||
f"--window-size={SCREEN_W},{chrome_h}",
|
||
f"--app={HOME_URL}",
|
||
]
|
||
return subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
|
||
|
||
# ── Main ──────────────────────────────────────────────────────────
|
||
if __name__ == "__main__":
|
||
app = QApplication(sys.argv)
|
||
app.setApplicationName("SeniorNet")
|
||
p = app.palette(); p.setColor(QPalette.Window, QColor("#1a3a5c")); app.setPalette(p)
|
||
|
||
cdp = CDP()
|
||
overlay = KioskOverlay(cdp)
|
||
|
||
chrome_proc = launch_chrome()
|
||
|
||
def init_cdp():
|
||
if cdp.wait_ready(timeout=30):
|
||
print("CDP ready — kiosk running")
|
||
threading.Thread(target=init_cdp, daemon=True).start()
|
||
|
||
sys.exit(app.exec_())
|