Teachers prepare a lesson across several tabs — the lesson, its video,
the activity — and app mode is the one thing that cannot give them that.
Chrome cannot be asked for "tabs but no omnibox". App mode has no tabs; a
normal window has an address bar and no policy, flag or extension removes
it; tabbed web apps need a manifest on a site that is not ours and push
off-origin links out into a normal browser window, which is the whole of
what this design exists to prevent. So the tab strip is ours, and the
engine is Chromium either way.
QtWebEngine, from Debian, measured before it was chosen rather than
after. The usual and correct objection is codecs: on Debian 13 it reports
H.264, AAC, MP3, VP8 and VP9 all playable, MediaSource supports avc1,
mp4a and vp9, and a 720p H.264 file decoded and played to the end in the
lab. app.wonderink.org loads, and its sign-in is an ordinary email and
password form rather than a Google flow that would mind an embedded
browser. The user agent has the QtWebEngine token stripped so a site
gating on it sees the Chromium this actually is, and Widevine is handed
over from Chrome's own bundle when the machine has Chrome, which a
classroom always does.
[ Lesson ] [ Video ] [ Activity ]
The strip appears only above one tab, so an ordinary lesson looks exactly
like the application it replaces. A tab can only come from the lesson —
target=_blank, window.open, a middle click, a Ctrl+click — and there is
no address bar, no search and no button that makes a blank tab. No
allowlist of domains either, on purpose: a list would have to be right
about every video service a lesson might ever embed.
Three bugs the lab caught and this fixes. A lesson calling window.print()
did nothing at all, which is worse than no print button because a teacher
concludes the printer is broken; printRequested is now bound to the page
that asked. Leaving fullscreen left the window 200 pixels wide in the
corner — a window that has only ever been maximised has no ordinary
geometry to return to — so the fullscreen bit is cleared rather than
maximised being re-requested. And the Present view video failure written
up in docs/wonder-ink.md is acted on at last: that investigation named
the autoplay gesture chain as the likeliest cause and declined to fix it,
because in Chrome the only lever governed the ordinary browser as well.
Here it does not — this profile is Wonder Ink and nothing else — so a
lesson may start its own video and no other page in the room may.
Reproduced both ways in the lab with an unmuted video started by a script
with no gesture: NotAllowedError at Chrome's default, playing as shipped.
That is not proof Present view is fixed; the lab has no Wonder Ink
account, and candidates 2 and 3 in that document are still open.
Measured on a rebooted machine, from the package: tabs open from a link
and from window.open, switch, and close; closing one leaves the others;
closing the last closes Wonder Ink; H.264 video plays and fills the
display in fullscreen and comes back maximised with its tabs; the print
dialog offers the room's own Elmer queue; a download lands in ~/Downloads;
back and forward work from a right-click menu that is built rather than
filtered; pressing the panel button twice raises one window rather than
starting a second on the same profile; KWin reports the window as
ekklesia-wonderink, which is one panel button and not two.
Separate from the browser as before, in its own profile, with cookies
that live only as long as the window — what the Chrome policy did for
wonderink.org, and the reason closing Wonder Ink ends a teacher's
session. Chrome's old profile directory is left untouched beside it, and
the browser's own policy is not edited by any of this.
Wonder Ink is now optional per device, through WONDERINK_ENABLED in
device.conf, because one future device is a laptop that will not have it.
It rides the mechanism the disc player already uses: one marked line in
the packaged layout, edited by classroom-desktop-layout. False takes the
launcher off the panel and makes the command refuse to start; the
browser, Parent Call and the disc player are untouched and the panel
stays balanced. Absent means yes, so every existing room is unchanged.
One scale, still. Chrome has to be handed UI_SCALE because it was
measured not to follow the compositor; Qt does follow it — devicePixelRatio
2.0 at UI_SCALE=2.0, measured — so Wonder Ink is deliberately not told,
or the room would get it twice.
Co-Authored-By: Claude Opus 5 <[email protected]>
234 lines
7.6 KiB
Python
234 lines
7.6 KiB
Python
"""Device configuration for the Ekklesia Classroom shell.
|
|
|
|
Everything that has to agree about /etc/ekklesia-classroom/device.conf
|
|
reads it through here, so the dock, the notification daemon and the DVD
|
|
commands cannot drift apart over what a setting means.
|
|
"""
|
|
|
|
import math
|
|
from pathlib import Path
|
|
|
|
CONFIG_FILE = Path("/etc/ekklesia-classroom/device.conf")
|
|
|
|
TRUTHY = {"1", "true", "yes", "on", "enabled"}
|
|
|
|
# Tuned for a 1080p classroom television at 1.0. A 4K set wants 2.0,
|
|
# which is the same interface at the same physical size on a display with
|
|
# twice the pixels.
|
|
UI_SCALE_DEFAULT = 1.0
|
|
UI_SCALE_MIN = 0.8
|
|
# 2.0 is a 4K television drawn at 1080p sizes, which is the setting the
|
|
# large classroom screens actually need. The old ceiling of 1.5 could not
|
|
# express it, so a room asking for 2.0 silently got 1.5.
|
|
UI_SCALE_MAX = 3.0
|
|
|
|
|
|
DEVICE_CONFIG_TEMPLATE = """# Ekklesia Classroom device configuration
|
|
|
|
UPDATE_URL=https://updates.riverofglorymn.org
|
|
CHANNEL=stable
|
|
|
|
# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
|
|
# implemented yet; this only controls whether the current placeholder appears.
|
|
LIVE_ENABLED=false
|
|
|
|
# Which output the room plays through.
|
|
#
|
|
# auto prefer a connected HDMI or DisplayPort television, and fall
|
|
# back to the analog jack. This is right for almost every room.
|
|
# hdmi always the television, even if it does not report itself.
|
|
# analog always the jack.
|
|
#
|
|
# auto re-decides whenever the hardware changes, so a television switched
|
|
# on after the computer is picked up without anybody touching anything,
|
|
# and sound that is already playing is moved across to it.
|
|
AUDIO_OUTPUT=auto
|
|
|
|
# How large everything in the room is drawn: the panel, the clock, the
|
|
# browser, Wonder Ink, the disc player, notifications and the volume
|
|
# display, all from this one number. 1.0 to 3.0.
|
|
#
|
|
# 1.0 suits a 1080p television at ordinary classroom distance. 2.0 is
|
|
# right for a 4K set, where everything is otherwise half the size it
|
|
# looks. This is an administrator's setting: there is no way to reach it
|
|
# from the room, on purpose.
|
|
UI_SCALE=1.0
|
|
|
|
# Church message broker. Every classroom uses the same one, so the address
|
|
# is a default rather than something to fill in room by room. The room is
|
|
# this machine's hostname unless ROOM is set. A username and password are
|
|
# only needed if the broker asks for them.
|
|
MQTT_ENABLED=true
|
|
MQTT_HOST=10.40.0.2
|
|
MQTT_PORT=1883
|
|
MQTT_USERNAME=
|
|
MQTT_PASSWORD=
|
|
|
|
# Whether this device has Wonder Ink at all.
|
|
#
|
|
# Every classroom does. A laptop that goes out with a volunteer does not,
|
|
# and a room that is offered a lesson application it has no licence for is
|
|
# a button that can only disappoint.
|
|
#
|
|
# false takes the launcher off the panel and stops the command starting.
|
|
# The ordinary browser, Parent Call and everything else are untouched.
|
|
WONDERINK_ENABLED=true
|
|
"""
|
|
|
|
|
|
# Sections added after the first classroom release. Keeping them separate lets
|
|
# an upgrade make a setting discoverable without replacing anything an
|
|
# administrator has already put in device.conf.
|
|
DEVICE_CONFIG_SECTIONS = (
|
|
(
|
|
"LIVE_ENABLED",
|
|
"""# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
|
|
# implemented yet; this only controls whether the current placeholder appears.
|
|
LIVE_ENABLED=false
|
|
""",
|
|
),
|
|
(
|
|
"AUDIO_OUTPUT",
|
|
"""# Which output the room plays through.
|
|
#
|
|
# auto prefer a connected HDMI or DisplayPort television, and fall
|
|
# back to the analog jack. This is right for almost every room.
|
|
# hdmi always the television, even if it does not report itself.
|
|
# analog always the jack.
|
|
#
|
|
# auto re-decides whenever the hardware changes, so a television switched
|
|
# on after the computer is picked up without anybody touching anything,
|
|
# and sound that is already playing is moved across to it.
|
|
AUDIO_OUTPUT=auto
|
|
""",
|
|
),
|
|
(
|
|
"UI_SCALE",
|
|
"""# How large everything in the room is drawn: the panel, the clock, the
|
|
# browser, Wonder Ink, the disc player, notifications and the volume
|
|
# display, all from this one number. 1.0 to 3.0.
|
|
#
|
|
# 1.0 suits a 1080p television at ordinary classroom distance. 2.0 is
|
|
# right for a 4K set, where everything is otherwise half the size it
|
|
# looks. This is an administrator's setting: there is no way to reach it
|
|
# from the room, on purpose.
|
|
UI_SCALE=1.0
|
|
""",
|
|
),
|
|
(
|
|
"WONDERINK_ENABLED",
|
|
'''# Whether this device has Wonder Ink at all.
|
|
#
|
|
# Every classroom does. A laptop that goes out with a volunteer does not,
|
|
# and a room that is offered a lesson application it has no licence for is
|
|
# a button that can only disappoint.
|
|
#
|
|
# false takes the launcher off the panel and stops the command starting.
|
|
# The ordinary browser, Parent Call and everything else are untouched.
|
|
WONDERINK_ENABLED=true
|
|
''',
|
|
),
|
|
(
|
|
"MQTT_ENABLED",
|
|
"""# Church message broker. Every classroom uses the same one, so the address
|
|
# is a default rather than something to fill in room by room. The room is
|
|
# this machine's hostname unless ROOM is set. A username and password are
|
|
# only needed if the broker asks for them.
|
|
MQTT_ENABLED=true
|
|
MQTT_HOST=10.40.0.2
|
|
MQTT_PORT=1883
|
|
MQTT_USERNAME=
|
|
MQTT_PASSWORD=
|
|
""",
|
|
),
|
|
)
|
|
|
|
|
|
def read_device_config(path=None):
|
|
config = {}
|
|
path = Path(path) if path is not None else CONFIG_FILE
|
|
|
|
if not path.exists():
|
|
return config
|
|
|
|
for raw_line in path.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 TRUTHY
|
|
|
|
|
|
def ensure_device_config(path=None):
|
|
"""Create device.conf or append any missing settings exactly once.
|
|
|
|
Existing bytes are otherwise left alone: per-room settings, comments and
|
|
even keys this release does not know about all survive an upgrade.
|
|
"""
|
|
path = Path(path) if path is not None else CONFIG_FILE
|
|
|
|
if not path.exists():
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(DEVICE_CONFIG_TEMPLATE)
|
|
return
|
|
|
|
text = path.read_text()
|
|
config = read_device_config(path)
|
|
additions = []
|
|
|
|
for key, section in DEVICE_CONFIG_SECTIONS:
|
|
if key not in config:
|
|
additions.append(section.rstrip("\n"))
|
|
|
|
if not additions:
|
|
return
|
|
|
|
separator = "" if not text or text.endswith("\n") else "\n"
|
|
path.write_text(text + separator + "\n\n".join(additions) + "\n")
|
|
|
|
|
|
def ui_scale(config):
|
|
"""UI_SCALE, clamped to something a classroom can actually use."""
|
|
raw = config.get("UI_SCALE", "")
|
|
|
|
try:
|
|
value = float(raw)
|
|
except (TypeError, ValueError):
|
|
return UI_SCALE_DEFAULT
|
|
|
|
if not math.isfinite(value):
|
|
return UI_SCALE_DEFAULT
|
|
|
|
return min(max(value, UI_SCALE_MIN), UI_SCALE_MAX)
|
|
|
|
|
|
# The scale the classroom's own GTK surfaces render at.
|
|
#
|
|
# UI_SCALE is applied to the whole output (see classroom-scale),
|
|
# which scales Chrome, VLC and every other application along with the
|
|
# panel. Our surfaces are measured in the same logical pixels the
|
|
# compositor is
|
|
# already scaling, so scaling them here as well would apply UI_SCALE
|
|
# twice and the dock would come out half again too big.
|
|
#
|
|
# This is the seam: if compositor scaling ever has to be abandoned,
|
|
# changing this back to ui_scale(config) restores per-surface scaling
|
|
# without touching the templates.
|
|
SHELL_SCALE = 1.0
|
|
|
|
|
|
def scaled(value, scale):
|
|
"""A pixel measurement at the given scale, never smaller than 1."""
|
|
return max(1, int(round(value * scale)))
|