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
+85
View File
@@ -0,0 +1,85 @@
window#classroom-dock,
window#classroom-clock {
background-color: transparent;
background-image: none;
border: none;
box-shadow: none;
}
/* Main floating dock */
.dock {
background-image: none;
background-color: rgba(34, 38, 50, 0.62);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
padding: 8px 10px;
/* Important: no shadow here.
GTK was clipping it against the rectangular window edge. */
box-shadow: none;
}
button.dock-button {
color: white;
background-image: none;
background-color: rgba(255, 255, 255, 0.05);
border: none;
border-radius: 16px;
padding: 6px;
margin: 0;
min-width: 44px;
min-height: 44px;
box-shadow: none;
outline: none;
}
button.dock-button:hover {
background-image: none;
background-color: rgba(70, 75, 90, 0.85);
box-shadow: none;
}
button.dock-button:active {
background-image: none;
background-color: rgba(85, 90, 108, 0.90);
box-shadow: none;
}
button.dock-button:focus {
background-image: none;
background-color: rgba(255, 255, 255, 0.08);
box-shadow: none;
outline: none;
}
button.dock-button image {
color: white;
-gtk-icon-effect: none;
}
/* Clock */
.clock {
color: white;
background-image: none;
background-color: rgba(34, 38, 50, 0.62);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 7px 12px;
font-size: 17px;
font-weight: 600;
box-shadow: none;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 640 640"><path fill="#ffffff" d="M161.423 279.11L69.816 120.474C128.458 47.068 218.708-.012 319.976-.012c117.168 0 219.57 62.965 275.33 156.888H334.175c-4.69-.39-9.438-.638-14.245-.638-76.158 0-140.352 52.288-158.553 122.848l.048.024zm273.121-75.958H617.96c14.197 36.202 22.04 75.64 22.04 116.872C640 495.55 498.668 638.04 323.626 640l130.88-226.692c18.449-26.492 29.244-58.642 29.244-93.284 0-45.71-18.839-87.12-49.205-116.872zM203.742 320.024c0-64.099 52.158-116.234 116.234-116.234 64.087 0 116.246 52.159 116.246 116.234 0 64.088-52.158 116.246-116.246 116.246-64.076 0-116.234-52.158-116.234-116.246zm160.112 157.762l-91.596 158.695C118.17 613.438 0 480.515 0 320.024c0-56.988 14.918-110.505 41.044-156.876l130.643 226.278c26.162 55.678 82.808 94.372 148.277 94.372 15.19 0 29.87-2.091 43.879-5.965l.012-.047z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+67
View File
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 187.09 82.68"
version="1.1"
id="svg4"
sodipodi:docname="dvdlogo-02.svg"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview4"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="3.0875941"
inkscape:cx="62.022401"
inkscape:cy="35.950321"
inkscape:window-width="1646"
inkscape:window-height="955"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<defs
id="defs1">
<style
id="style1">.cls-1{fill:#00feff;}</style>
</defs>
<title
id="title1">08dvdlogo-</title>
<g
id="Calque_2"
data-name="Calque 2"
style="fill:#ffffff;fill-opacity:1">
<g
id="Layer_1"
data-name="Layer 1"
style="fill:#ffffff;fill-opacity:1">
<path
class="cls-1"
d="M128.81,10.16H147S169,9,168.45,20.32c-.87,17.47-27.65,16.22-27.65,16.22L146,13.83h-18.2L120.2,46.7h18.06s18,.8,32.88-6.35c15.8-7.62,15.94-21,15.94-21a15.3,15.3,0,0,0-7.76-13.4C170,.42,157.87,0,157.87,0H118.09L94.53,30.62,84.65,0H16.08L13.54,10.16h18.2S53.75,9,53.19,20.32c-.87,17.47-27.65,16.22-27.65,16.22l5.22-22.71H12.56L4.94,46.7H23s18,.8,32.87-6.35c15.8-7.62,15.94-21,15.94-21a35,35,0,0,0-.7-5.5c-.43-1.41-1-3.67-1-3.67H71L87.76,57.28l41.05-47.12Z"
id="path1"
style="fill:#ffffff;fill-opacity:1" />
<path
class="cls-1"
d="M88.32,57.28C39.54,57.28,0,63,0,70s39.54,12.7,88.32,12.7S176.64,77,176.64,70,137.1,57.28,88.32,57.28ZM45.54,76.92H41.82L34.06,63.73h5.21l4.46,8,4.48-8h5.22Zm20.93,0h-4.8V63.73h4.8Zm17,0h-6.8V63.73h6.8c5.15,0,9.38,2.89,9.38,6.59S88.58,76.92,83.46,76.92Zm29.16-10.28h-5.7v2.2h5.41v2.9h-5.41V74h5.7v2.9h-10.5V63.73h10.5Zm19.29,10.72c-5.93,0-10.21-3-10.21-7.28,0-4,4.89-6.78,10.21-6.78s10.21,2.79,10.21,6.78C142.12,74.35,137.83,77.36,131.91,77.36Z"
id="path2"
style="fill:#ffffff;fill-opacity:1" />
<path
class="cls-1"
d="M131.91,66.62c2.86,0,5.21,1.66,5.21,3.48,0,2.27-2.35,3.93-5.21,3.93s-5.22-1.66-5.22-3.93c0-1.82,2.35-3.48,5.22-3.48Z"
id="path3"
style="fill:#ffffff;fill-opacity:1" />
<path
class="cls-1"
d="M82.58,66.64H81.45V74h1.08c2.87,0,5.32-1.12,5.32-3.69C87.85,68,85.67,66.64,82.58,66.64Z"
id="path4"
style="fill:#ffffff;fill-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+2
View File
@@ -0,0 +1,2 @@
<?xml version='1.0' encoding='utf-8'?>
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 441 512.49"><defs><mask id="live-text-cutout" maskUnits="userSpaceOnUse" x="0" y="0" width="441" height="512.49"><rect x="0" y="0" width="441" height="512.49" fill="#ffffff" /><path d="M147.21 459.19H92.39v-85.66h27.41v63.73h27.41v21.93zm10.63 0v-85.66h27.4v85.66h-27.4zm90.93-85.66h28.92l-21.25 85.66h-40.02l-21.24-85.66h28.92l11.65 54.41h1.23l11.79-54.41zm93.67 53.31h-27.41v10.42h33.58v21.93h-60.99v-85.66h60.31l-3.43 21.93h-29.47v11.51h27.41v19.87z" fill="#000000" /></mask></defs><path fill="#7f8491" fill-rule="nonzero" d="M220.5 0c60.89 0 116.02 24.69 155.92 64.58 39.9 39.9 64.58 95.04 64.58 155.92 0 33.84-7.63 65.9-21.25 94.55-.46-.5-.94-.99-1.42-1.48a50.983 50.983 0 00-18.43-11.8c11.24-24.78 17.5-52.29 17.5-81.27 0-54.37-22.04-103.6-57.67-139.23C324.1 45.64 274.87 23.6 220.5 23.6c-54.37 0-103.6 22.04-139.23 57.67C45.64 116.9 23.6 166.13 23.6 220.5c0 28.98 6.26 56.49 17.5 81.27a50.983 50.983 0 00-18.43 11.8c-.48.49-.95.98-1.42 1.48C7.63 286.4 0 254.34 0 220.5c0-60.89 24.69-116.02 64.58-155.92C104.48 24.69 159.61 0 220.5 0zm0 64.27c86.28 0 156.23 69.95 156.23 156.23 0 28.48-7.62 55.18-20.94 78.17H85.21c-13.32-22.99-20.94-49.69-20.94-78.17 0-86.28 69.95-156.23 156.23-156.23z" /><path fill="#ffffff" d="M270.57 213.92c8.58-5.28 8.53-11.22 0-15.92l-70.1-48.77c-7-4.21-14.39-1.72-14.1 7l.24 98.63c.62 9.59 6.28 12.09 14.38 7.67l69.58-48.61z" /><path fill="#ffffff" d="M58.55 320.23h323.9c16.08 0 29.22 13.17 29.22 29.23v133.8c0 16.06-13.16 29.23-29.22 29.23H58.55c-16.06 0-29.22-13.15-29.22-29.23v-133.8c0-16.08 13.15-29.23 29.22-29.23z" mask="url(#live-text-cutout)" /></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+2
View File
@@ -0,0 +1,2 @@
<?xml version='1.0' encoding='utf-8'?>
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 441 512.49"><defs><mask id="live-text-cutout" maskUnits="userSpaceOnUse" x="0" y="0" width="441" height="512.49"><rect x="0" y="0" width="441" height="512.49" fill="#ffffff" /><path d="M147.21 459.19H92.39v-85.66h27.41v63.73h27.41v21.93zm10.63 0v-85.66h27.4v85.66h-27.4zm90.93-85.66h28.92l-21.25 85.66h-40.02l-21.24-85.66h28.92l11.65 54.41h1.23l11.79-54.41zm93.67 53.31h-27.41v10.42h33.58v21.93h-60.99v-85.66h60.31l-3.43 21.93h-29.47v11.51h27.41v19.87z" fill="#000000" /></mask></defs><path fill="#FE0000" fill-rule="nonzero" d="M220.5 0c60.89 0 116.02 24.69 155.92 64.58 39.9 39.9 64.58 95.04 64.58 155.92 0 33.84-7.63 65.9-21.25 94.55-.46-.5-.94-.99-1.42-1.48a50.983 50.983 0 00-18.43-11.8c11.24-24.78 17.5-52.29 17.5-81.27 0-54.37-22.04-103.6-57.67-139.23C324.1 45.64 274.87 23.6 220.5 23.6c-54.37 0-103.6 22.04-139.23 57.67C45.64 116.9 23.6 166.13 23.6 220.5c0 28.98 6.26 56.49 17.5 81.27a50.983 50.983 0 00-18.43 11.8c-.48.49-.95.98-1.42 1.48C7.63 286.4 0 254.34 0 220.5c0-60.89 24.69-116.02 64.58-155.92C104.48 24.69 159.61 0 220.5 0zm0 64.27c86.28 0 156.23 69.95 156.23 156.23 0 28.48-7.62 55.18-20.94 78.17H85.21c-13.32-22.99-20.94-49.69-20.94-78.17 0-86.28 69.95-156.23 156.23-156.23z" /><path fill="#ffffff" d="M270.57 213.92c8.58-5.28 8.53-11.22 0-15.92l-70.1-48.77c-7-4.21-14.39-1.72-14.1 7l.24 98.63c.62 9.59 6.28 12.09 14.38 7.67l69.58-48.61z" /><path fill="#ffffff" d="M58.55 320.23h323.9c16.08 0 29.22 13.17 29.22 29.23v133.8c0 16.06-13.16 29.23-29.22 29.23H58.55c-16.06 0-29.22-13.15-29.22-29.23v-133.8c0-16.08 13.15-29.23 29.22-29.23z" mask="url(#live-text-cutout)" /></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+3 -3
View File
@@ -4,10 +4,10 @@
set $mod Mod4 set $mod Mod4
# Wallpaper # Wallpaper
output * bg #202020 solid_color output * bg /usr/share/backgrounds/ekklesia/classroom-default.png fill
# Waybar # Ekklesia Classroom dock
exec --no-startup-id waybar -c /etc/ekklesia-classroom/waybar/config.jsonc -s /etc/ekklesia-classroom/waybar/style.css exec --no-startup-id /usr/local/bin/classroom-dock
# Keep the interface intentionally minimal. # Keep the interface intentionally minimal.
# Additional classroom controls are added explicitly by Ekklesia Classroom. # Additional classroom controls are added explicitly by Ekklesia Classroom.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+1 -1
View File
@@ -4,6 +4,6 @@ Section: misc
Priority: optional Priority: optional
Architecture: amd64 Architecture: amd64
Maintainer: Ekklesia Maintainer: Ekklesia
Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups, python3-gi, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, adwaita-icon-theme, libnotify-bin
Description: Ekklesia managed classroom environment Description: Ekklesia managed classroom environment
A minimal Wayland classroom environment for Ekklesia-managed church computers. A minimal Wayland classroom environment for Ekklesia-managed church computers.
+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()
+28
View File
@@ -54,6 +54,18 @@ install -Dm755 \
"$ROOT/scripts/classroom-browser" \ "$ROOT/scripts/classroom-browser" \
"$STAGE/usr/local/bin/classroom-browser" "$STAGE/usr/local/bin/classroom-browser"
install -Dm644 \
"$ROOT/config/wallpapers/classroom-default.png" \
"$STAGE/usr/share/backgrounds/ekklesia/classroom-default.png"
install -Dm755 \
"$ROOT/scripts/classroom-dock" \
"$STAGE/usr/local/bin/classroom-dock"
install -Dm644 \
"$ROOT/config/dock/style.css" \
"$STAGE/etc/ekklesia-classroom/dock/style.css"
install -Dm755 \ install -Dm755 \
"$ROOT/scripts/ekklesia-classroom-update" \ "$ROOT/scripts/ekklesia-classroom-update" \
"$STAGE/usr/local/bin/ekklesia-classroom-update" "$STAGE/usr/local/bin/ekklesia-classroom-update"
@@ -71,6 +83,22 @@ install -Dm644 \
"$ROOT/systemd/updater/ekklesia-classroom-update.timer" \ "$ROOT/systemd/updater/ekklesia-classroom-update.timer" \
"$STAGE/etc/systemd/system/ekklesia-classroom-update.timer" "$STAGE/etc/systemd/system/ekklesia-classroom-update.timer"
install -Dm644 \
"$ROOT/config/icons/dvd.svg" \
"$STAGE/etc/ekklesia-classroom/icons/dvd.svg"
install -Dm644 \
"$ROOT/config/icons/live.svg" \
"$STAGE/etc/ekklesia-classroom/icons/live.svg"
install -Dm644 \
"$ROOT/config/icons/live-off.svg" \
"$STAGE/etc/ekklesia-classroom/icons/live-off.svg"
install -Dm644 \
"$ROOT/config/icons/browser.svg" \
"$STAGE/etc/ekklesia-classroom/icons/browser.svg"
printf '%s\n' "$VERSION" > "$ROOT/VERSION" printf '%s\n' "$VERSION" > "$ROOT/VERSION"
dpkg-deb \ dpkg-deb \