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]>
169 lines
7.7 KiB
JavaScript
169 lines
7.7 KiB
JavaScript
// The Ekklesia Classroom desktop.
|
|
//
|
|
// Plasma runs this once, when a session has no panel yet, and again on
|
|
// demand — `plasma-apply-lookandfeel -a org.ekklesia.classroom.desktop
|
|
// --resetLayout` rebuilds the whole desktop from this file. That is the
|
|
// classroom's recovery path for a damaged shell, and it is Plasma's own
|
|
// mechanism rather than something written here.
|
|
//
|
|
// One full-width bar along the bottom. Left is auxiliary, the middle is
|
|
// applications, the right is the small amount of status a teacher needs.
|
|
// Nothing else is on the screen.
|
|
|
|
var WALLPAPER = "/usr/share/backgrounds/ekklesia/classroom-calm.png";
|
|
|
|
// Matches the 76px strip the classroom has always used, so muscle memory
|
|
// and the room's viewing distance both carry over.
|
|
var PANEL_HEIGHT = 76;
|
|
|
|
// The idle desktop: the wallpaper, and nothing on it.
|
|
var screens = desktops();
|
|
for (var i = 0; i < screens.length; i++) {
|
|
var desktop = screens[i];
|
|
desktop.wallpaperPlugin = "org.kde.image";
|
|
desktop.currentConfigGroup = ["Wallpaper", "org.kde.image", "General"];
|
|
desktop.writeConfig("Image", "file://" + WALLPAPER);
|
|
desktop.writeConfig("SlidePaths", "");
|
|
// 2 is "scaled and cropped": the landscape fills the display at any
|
|
// aspect ratio without being stretched out of shape.
|
|
desktop.writeConfig("FillMode", 2);
|
|
desktop.reloadConfig();
|
|
}
|
|
|
|
var panel = new Panel();
|
|
panel.location = "bottom";
|
|
panel.height = PANEL_HEIGHT;
|
|
panel.hiding = "none";
|
|
panel.floating = false;
|
|
panel.lengthMode = "fill";
|
|
panel.alignment = "center";
|
|
panel.opacity = "adaptive";
|
|
|
|
// LEFT — auxiliary. Parent Call is not one of the room's applications and
|
|
// is deliberately not in the row with them.
|
|
// Quick Launch: a launcher, and only a launcher.
|
|
//
|
|
// Two other widgets were tried on a real panel. The Icon widget copies
|
|
// the desktop entry into the user's home and uses the copy, so an update
|
|
// could never change the icon or the command — it kept showing a bell out
|
|
// of a package that no longer contained one. A task manager reads
|
|
// /usr/share/applications live, which fixes that, but a task manager also
|
|
// shows every running window, so the auxiliary corner filled up with the
|
|
// lesson's tasks.
|
|
//
|
|
// Quick Launch reads the entry live and shows nothing but the button.
|
|
var parentCall = panel.addWidget("org.kde.plasma.quicklaunch");
|
|
parentCall.currentConfigGroup = ["General"];
|
|
// A file URL, not applications:. Quick Launch resolves the entry itself
|
|
// and does not copy it, so the path is a live reference to what the
|
|
// package installs.
|
|
parentCall.writeConfig(
|
|
"launcherUrls",
|
|
"file:///usr/share/applications/ekklesia-parent-call.desktop"
|
|
);
|
|
parentCall.writeConfig("showLauncherNames", false);
|
|
parentCall.writeConfig("enablePopup", false);
|
|
parentCall.writeConfig("maxSectionCount", 1);
|
|
|
|
var leftGap = panel.addWidget("org.kde.plasma.panelspacer");
|
|
leftGap.currentConfigGroup = ["General"];
|
|
leftGap.writeConfig("expanding", true);
|
|
|
|
// CENTRE — the applications, and the whole of the running/minimised/active
|
|
// story. This applet is the reason the classroom stops writing a dock.
|
|
var tasks = panel.addWidget("org.kde.plasma.icontasks");
|
|
tasks.currentConfigGroup = ["General"];
|
|
// Two of these three lines are rewritten by classroom-desktop-layout to
|
|
// match the machine, and each is marked so the edit is a line replacement
|
|
// and not a parse.
|
|
//
|
|
// The disc player follows the hardware: a room with no optical drive has
|
|
// it removed, so no teacher is offered a player that cannot exist.
|
|
//
|
|
// Wonder Ink follows WONDERINK_ENABLED in device.conf: a device that is
|
|
// not a classroom — the laptop that goes out with a volunteer — has no
|
|
// lesson application, and the ordinary browser is unaffected either way.
|
|
tasks.writeConfig("launchers", [
|
|
/* CLASSROOM-WONDERINK-LAUNCHER */ "applications:ekklesia-wonderink.desktop",
|
|
"applications:google-chrome.desktop",
|
|
/* CLASSROOM-DVD-LAUNCHER */ "applications:ekklesia-dvd.desktop"
|
|
].join(","));
|
|
// Every window gets its own button. Teachers should never have to open a
|
|
// group to find out that a second window exists.
|
|
tasks.writeConfig("groupingStrategy", 0);
|
|
tasks.writeConfig("showOnlyCurrentDesktop", false);
|
|
tasks.writeConfig("showOnlyCurrentActivity", false);
|
|
tasks.writeConfig("showOnlyCurrentScreen", false);
|
|
// Clicking a button always means "show me this", and never "hide it".
|
|
//
|
|
// This was measured before it was decided. Plasma 6.3 draws a minimised
|
|
// task and a task that is merely behind another window identically: no
|
|
// indicator for closed, a grey indicator for running, a teal one for
|
|
// active — three marks for four states. Rather than write a task manager
|
|
// that draws a fourth, the classroom removes the state. There is nothing
|
|
// on the idle desktop for a teacher to want to see, so minimising an
|
|
// application in this room has no purpose and only ever loses it.
|
|
//
|
|
// A window minimised some other way is still shown as running and comes
|
|
// back with one click, so it can never be lost even then.
|
|
tasks.writeConfig("minimizeActiveTaskOnClick", false);
|
|
tasks.writeConfig("fill", false);
|
|
tasks.writeConfig("maxStripes", 1);
|
|
tasks.writeConfig("indicateAudioStreams", false);
|
|
tasks.writeConfig("wheelEnabled", false);
|
|
tasks.writeConfig("middleClickAction", "NewInstance");
|
|
tasks.writeConfig("showToolTips", true);
|
|
tasks.writeConfig("iconSpacing", 2);
|
|
|
|
var rightGap = panel.addWidget("org.kde.plasma.panelspacer");
|
|
rightGap.currentConfigGroup = ["General"];
|
|
rightGap.writeConfig("expanding", true);
|
|
|
|
// RIGHT — status.
|
|
//
|
|
// Three ordinary applets straight on the bar, and no system tray around
|
|
// them. A tray is a container that discovers whatever is installed and
|
|
// then hides most of it behind an arrow; measured in the lab it collected
|
|
// battery, brightness, clipboard, keyboard layout, camera, vaults and
|
|
// screen management on a machine that has none of those to say anything
|
|
// about. Naming the four the classroom actually uses is both smaller and
|
|
// deterministic, and it leaves nothing for a teacher to expand.
|
|
panel.addWidget("org.kde.plasma.networkmanagement");
|
|
|
|
var volume = panel.addWidget("org.kde.plasma.volume");
|
|
volume.currentConfigGroup = ["General"];
|
|
// No chime when the volume changes. A classroom is often listening to
|
|
// something when somebody adjusts it, and a beep over a lesson is worse
|
|
// than no feedback at all — the on-screen display is the feedback, and it
|
|
// is in the look and feel package where a television can read it.
|
|
//
|
|
// Plasma 6 moved this setting out of the applet and into a global one,
|
|
// and the applet migrates a false written here into it on first run. So
|
|
// this is both the setting and the instruction to carry it across.
|
|
volume.writeConfig("volumeFeedback", false);
|
|
volume.writeConfig("volumeOsd", true);
|
|
volume.writeConfig("muteOsd", true);
|
|
volume.writeConfig("outputChangeOsd", true);
|
|
// A teacher pressing the volume key expects a step they can hear.
|
|
volume.writeConfig("volumeStep", 5);
|
|
|
|
panel.addWidget("org.kde.plasma.notifications");
|
|
|
|
var clock = panel.addWidget("org.kde.plasma.digitalclock");
|
|
clock.currentConfigGroup = ["Appearance"];
|
|
clock.writeConfig("showDate", true);
|
|
clock.writeConfig("dateFormat", "custom");
|
|
clock.writeConfig("customDateFormat", "ddd d MMM");
|
|
clock.writeConfig("showSeconds", 0);
|
|
clock.writeConfig("dateDisplayFormat", "BelowTime");
|
|
// Sized by the bar, not by a number written here.
|
|
//
|
|
// It used to be pinned at 15 points to stop it becoming the largest text
|
|
// in the room. On a 4K television that was a clock a teacher could not
|
|
// read from the back of the room, and it was the one thing in the
|
|
// classroom that did not follow UI_SCALE, because a point size written
|
|
// into a layout is the same point size at every scale. Plasma's own
|
|
// sizing follows the bar, and the bar follows the scale.
|
|
clock.writeConfig("autoFontAndSize", true);
|
|
clock.writeConfig("fontWeight", 500);
|