Initial release v0.01 — SeniorNet kiosk browser
- 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
This commit is contained in:
Executable
+593
@@ -0,0 +1,593 @@
|
||||
#!/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_())
|
||||
Reference in New Issue
Block a user