feat: polish classroom dock and visual shell

This commit is contained in:
ekklesia
2026-09-02 16:53:40 -05:00
parent 12d77a62a7
commit ddcf77b6a9
10 changed files with 507 additions and 4 deletions
+318
View File
@@ -0,0 +1,318 @@
#!/usr/bin/env python3
import subprocess
from pathlib import Path
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("GtkLayerShell", "0.1")
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, GtkLayerShell
CONFIG_FILE = Path("/etc/ekklesia-classroom/device.conf")
CSS_FILE = Path("/etc/ekklesia-classroom/dock/style.css")
ICON_DIR = Path("/etc/ekklesia-classroom/icons")
def read_device_config():
config = {}
if not CONFIG_FILE.exists():
return config
for raw_line in CONFIG_FILE.read_text().splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
config[key.strip()] = value.strip()
return config
def enabled(config, key, default=False):
if key not in config:
return default
return config[key].lower() in {
"1",
"true",
"yes",
"on",
"enabled",
}
def launch(command):
subprocess.Popen(
command,
start_new_session=True,
)
def notify(title, message):
launch([
"notify-send",
"--app-name=Ekklesia Classroom",
title,
message,
])
def themed_icon(*names):
theme = Gtk.IconTheme.get_default()
for name in names:
if theme.has_icon(name):
return Gtk.Image.new_from_icon_name(name, Gtk.IconSize.DIALOG)
return Gtk.Image.new_from_icon_name(
"application-x-executable",
Gtk.IconSize.DIALOG,
)
def custom_icon(filename, size):
path = ICON_DIR / filename
if path.exists():
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
str(path),
size,
size,
True,
)
return Gtk.Image.new_from_pixbuf(pixbuf)
return None
class ClassroomDock(Gtk.Window):
def __init__(self):
super().__init__()
screen = self.get_screen()
visual = screen.get_rgba_visual()
if visual is not None:
self.set_visual(visual)
self.set_app_paintable(True)
self.config = read_device_config()
self.set_name("classroom-dock")
self.set_decorated(False)
self.set_resizable(False)
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
GtkLayerShell.set_anchor(
self,
GtkLayerShell.Edge.BOTTOM,
True,
)
GtkLayerShell.set_margin(
self,
GtkLayerShell.Edge.BOTTOM,
12,
)
GtkLayerShell.set_keyboard_interactivity(self, False)
dock = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=10,
)
dock.get_style_context().add_class("dock")
dock.pack_start(
self.make_button(
self.browser_icon(),
"Browser",
lambda *_: launch(
["/usr/local/bin/classroom-browser"]
),
),
False,
False,
0,
)
if enabled(self.config, "DVD_ENABLED", False):
dock.pack_start(
self.make_button(
self.dvd_icon(),
"DVD",
lambda *_: notify(
"DVD",
"DVD support is being prepared.",
),
),
False,
False,
0,
)
dock.pack_start(
self.make_button(
self.sanctuary_icon(),
"Sanctuary",
lambda *_: notify(
"Sanctuary",
"Sanctuary live view is being prepared.",
),
),
False,
False,
0,
)
self.add(dock)
def browser_icon(self):
image = custom_icon("browser.svg", 28)
if image is not None:
return image
image = themed_icon("web-browser-symbolic", "internet-web-browser-symbolic")
image.set_pixel_size(28)
return image
def dvd_icon(self):
image = custom_icon("dvd.svg", 40)
if image is not None:
return image
image = themed_icon(
"media-optical-symbolic",
"media-optical",
)
image.set_pixel_size(28)
return image
def sanctuary_icon(self):
is_live = enabled(
self.config,
"SANCTUARY_LIVE",
False,
)
filename = "live.svg" if is_live else "live-off.svg"
image = custom_icon(filename, 38)
if image is not None:
return image
image = themed_icon(
"video-display-symbolic",
"video-display",
)
image.set_pixel_size(28)
return image
def make_button(self, image, tooltip, callback):
button = Gtk.Button()
button.set_relief(Gtk.ReliefStyle.NONE)
button.get_style_context().add_class("dock-button")
button.add(image)
button.connect("clicked", callback)
return button
class ClassroomClock(Gtk.Window):
def __init__(self):
super().__init__()
screen = self.get_screen()
visual = screen.get_rgba_visual()
if visual is not None:
self.set_visual(visual)
self.set_app_paintable(True)
self.set_name("classroom-clock")
self.set_decorated(False)
self.set_resizable(False)
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
GtkLayerShell.set_anchor(
self,
GtkLayerShell.Edge.BOTTOM,
True,
)
GtkLayerShell.set_anchor(
self,
GtkLayerShell.Edge.RIGHT,
True,
)
GtkLayerShell.set_margin(
self,
GtkLayerShell.Edge.BOTTOM,
16,
)
GtkLayerShell.set_margin(
self,
GtkLayerShell.Edge.RIGHT,
20,
)
GtkLayerShell.set_keyboard_interactivity(self, False)
self.label = Gtk.Label()
self.label.get_style_context().add_class("clock")
self.add(self.label)
self.update_time()
GLib.timeout_add_seconds(15, self.update_time)
def update_time(self):
from datetime import datetime
self.label.set_text(
datetime.now().strftime("%-I:%M %p")
)
return True
def load_css():
provider = Gtk.CssProvider()
provider.load_from_path(str(CSS_FILE))
screen = Gdk.Screen.get_default()
Gtk.StyleContext.add_provider_for_screen(
screen,
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
def main():
load_css()
dock = ClassroomDock()
dock.connect("destroy", Gtk.main_quit)
dock.show_all()
clock = ClassroomClock()
clock.show_all()
Gtk.main()
if __name__ == "__main__":
main()