dc4b129986
Change menu anchor from bottomLeft() to bottomRight() so Qt's built-in overflow logic flips the menu leftward when the button is near the right screen edge. All bookmarks now visible.
566 lines
22 KiB
Python
Executable File
566 lines
22 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
SeniorNet Kiosk — PyQt5 single-row floating toolbar over Google Chrome.
|
||
Navigation: ◀ ▶ ⌂ icon buttons + ☰ hamburger menu for bookmarks.
|
||
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 = 72 # single row — was 142 (82 + 60)
|
||
TOP_H = TOOLBAR_H
|
||
SCREEN_W = 1920
|
||
SCREEN_H = 1080
|
||
|
||
# ── Colour palette ────────────────────────────────────────────────
|
||
TOOLBAR_COLOR = "#1a3a5c"
|
||
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"
|
||
FONT_SIZE = 18
|
||
|
||
MENU_STYLE = """
|
||
QMenu {
|
||
background: #1e3a5c; color: white;
|
||
border: 2px solid #3a6da4; border-radius: 8px; padding: 4px;
|
||
}
|
||
QMenu::item { padding: 14px 36px 14px 16px; border-radius: 6px; font-size: 17px; }
|
||
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 mk_icon_btn(icon, color, hover, size=58, fs=26):
|
||
"""Square icon button for Back / Forward / Home / Hamburger."""
|
||
b = QPushButton(icon)
|
||
b.setFont(QFont("Arial", fs))
|
||
b.setFixedSize(size, size)
|
||
b.setCursor(Qt.PointingHandCursor)
|
||
b.setStyleSheet(f"""
|
||
QPushButton {{ background:{color}; color:white; border:none;
|
||
border-radius:12px; }}
|
||
QPushButton:hover {{ background:{hover}; }}
|
||
QPushButton:pressed {{ background:#222; }}
|
||
""")
|
||
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()
|
||
|
||
|
||
# ── 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.show()
|
||
|
||
def _build_toolbar(self):
|
||
bar = QWidget(); bar.setFixedHeight(TOOLBAR_H)
|
||
bar.setStyleSheet(f"background:{TOOLBAR_COLOR};")
|
||
tl = QHBoxLayout(bar)
|
||
tl.setContentsMargins(24, 7, 16, 7); tl.setSpacing(10)
|
||
|
||
# Brand label
|
||
brand = QLabel("SeniorNet")
|
||
brand.setFont(QFont("Arial", 22, QFont.Bold))
|
||
brand.setStyleSheet("color:white;")
|
||
tl.addWidget(brand)
|
||
tl.addStretch()
|
||
|
||
# ◀ Back
|
||
back_btn = mk_icon_btn("◀", BTN_COLOR, BTN_HOVER)
|
||
back_btn.setToolTip("Go Back")
|
||
back_btn.clicked.connect(self.cdp.back)
|
||
tl.addWidget(back_btn)
|
||
|
||
# ▶ Forward
|
||
fwd_btn = mk_icon_btn("▶", BTN_COLOR, BTN_HOVER)
|
||
fwd_btn.setToolTip("Go Forward")
|
||
fwd_btn.clicked.connect(self.cdp.forward)
|
||
tl.addWidget(fwd_btn)
|
||
|
||
# ⌂ Home
|
||
home_btn = mk_icon_btn("⌂", GOLD, GOLD_HOVER)
|
||
home_btn.setToolTip("Go Home")
|
||
home_btn.clicked.connect(lambda: self.cdp.navigate(HOME_URL))
|
||
tl.addWidget(home_btn)
|
||
|
||
# ☰ Hamburger — bookmarks menu
|
||
self.burger_btn = mk_icon_btn("☰", PURPLE, PURPLE_HOVER)
|
||
self.burger_btn.setToolTip("Bookmarks")
|
||
self.burger_btn.clicked.connect(self._show_bookmarks_menu)
|
||
tl.addWidget(self.burger_btn)
|
||
|
||
return bar
|
||
|
||
def _show_bookmarks_menu(self):
|
||
menu = self._build_menu(load_bm())
|
||
menu.addSeparator()
|
||
mgr_act = QAction(" ⚙ Manage Bookmarks…", menu)
|
||
mgr_act.triggered.connect(self.open_manager)
|
||
menu.addAction(mgr_act)
|
||
# Show menu below the hamburger button, anchored to right edge so
|
||
# Qt's overflow logic flips it leftward (avoids clipping off-screen).
|
||
menu.exec_(self.burger_btn.mapToGlobal(
|
||
self.burger_btn.rect().bottomRight()))
|
||
|
||
def _build_menu(self, items):
|
||
menu = QMenu()
|
||
menu.setFont(QFont("Arial", 17))
|
||
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", 17))
|
||
sub.setStyleSheet(MENU_STYLE)
|
||
menu.addMenu(sub)
|
||
else:
|
||
act = QAction(f" {item['name']}", menu)
|
||
url = item["url"]
|
||
act.triggered.connect(lambda _, u=url: self.cdp.navigate(u))
|
||
menu.addAction(act)
|
||
return menu
|
||
|
||
def open_manager(self):
|
||
d = ManagerDialog(self)
|
||
d.exec_()
|
||
|
||
def keyPressEvent(self, e):
|
||
if e.key() == Qt.Key_Home:
|
||
self.cdp.navigate(HOME_URL)
|
||
e.accept()
|
||
|
||
def eventFilter(self, obj, event):
|
||
from PyQt5.QtCore import QEvent
|
||
if event.type() == QEvent.KeyPress:
|
||
key = event.key()
|
||
mods = event.modifiers()
|
||
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
|
||
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)
|
||
|
||
for name in ("SingletonLock", "SingletonSocket", "SingletonCookie"):
|
||
try: (profile_dir / name).unlink()
|
||
except FileNotFoundError: pass
|
||
|
||
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)
|
||
|
||
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",
|
||
"--suppress-message-center-popups",
|
||
"--disable-default-browser-check",
|
||
"--no-first-run",
|
||
"--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 dismiss_chrome_dialogs():
|
||
"""
|
||
Chrome shows startup dialogs (first-run welcome, profile errors) on a
|
||
fresh or corrupted profile. Dismiss them all automatically so the
|
||
senior never sees them. Runs for ~15 seconds after startup.
|
||
"""
|
||
dialogs = [
|
||
"Welcome to Google Chrome",
|
||
"Profile error occurred",
|
||
]
|
||
for _ in range(15):
|
||
time.sleep(1)
|
||
for title in dialogs:
|
||
subprocess.run(
|
||
["xdotool", "search", "--name", title, "key", "Return"],
|
||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||
)
|
||
|
||
def init_cdp():
|
||
if cdp.wait_ready(timeout=30):
|
||
print("CDP ready — kiosk running")
|
||
dismiss_chrome_dialogs()
|
||
threading.Thread(target=init_cdp, daemon=True).start()
|
||
|
||
sys.exit(app.exec_())
|