425 lines
11 KiB
JavaScript
425 lines
11 KiB
JavaScript
|
// Portions of this file are taken from GNOME Shell and adapted.
|
|||
|
// Because of that, this gnome extension is distributed under
|
|||
|
// the terms of the GNU General Public License, version 2.
|
|||
|
|
|||
|
|
|||
|
const {
|
|||
|
AccountsService, Atk, Clutter, Gio,
|
|||
|
GLib, Graphene, Meta, Shell, St,
|
|||
|
} = imports.gi;
|
|||
|
|
|||
|
const Background = imports.ui.background;
|
|||
|
const Layout = imports.ui.layout;
|
|||
|
const Main = imports.ui.main;
|
|||
|
|
|||
|
// half of the time for which a frame is displayed
|
|||
|
const HALF_FRAME_TIME_MS = 8;
|
|||
|
|
|||
|
const BLUR_BRIGHTNESS = 0.55;
|
|||
|
const BLUR_SIGMA = 60;
|
|||
|
|
|||
|
const POINTER_HIDE_TIMEOUT = 10 * GLib.USEC_PER_SEC;
|
|||
|
|
|||
|
let actor;
|
|||
|
let lockDialog;
|
|||
|
let labelCountdown;
|
|||
|
let labelTitle;
|
|||
|
let labelMessage;
|
|||
|
let labelUser;
|
|||
|
|
|||
|
const bgManagers = [];
|
|||
|
let backgroundGroup;
|
|||
|
|
|||
|
let cursorTracker;
|
|||
|
let motionId = 0;
|
|||
|
let lastMotionTime = 0;
|
|||
|
let pointerHidden = false;
|
|||
|
let pointerHideId = 0;
|
|||
|
|
|||
|
let user;
|
|||
|
|
|||
|
let grab;
|
|||
|
let countdownTimeoutId = 0;
|
|||
|
|
|||
|
let configFile;
|
|||
|
let configMonitor;
|
|||
|
let config;
|
|||
|
let startTime;
|
|||
|
|
|||
|
let isExtensionEnabled = false;
|
|||
|
let isActive = false;
|
|||
|
let isShellReady = false;
|
|||
|
let isActiveChanging = false;
|
|||
|
|
|||
|
function extLog (msg) {
|
|||
|
log(`[contest-lock] ${msg}`)
|
|||
|
}
|
|||
|
|
|||
|
function extLogError (msg) {
|
|||
|
printerr(`[contest-lock] Error: ${msg}`);
|
|||
|
}
|
|||
|
|
|||
|
function loadConfig () {
|
|||
|
configFile.load_contents_async(null, (obj, res) => {
|
|||
|
// If there is a poblem with the config file, log an error and keep
|
|||
|
// using the old config.
|
|||
|
let newConfig;
|
|||
|
try {
|
|||
|
const [ok, bytes] = configFile.load_contents_finish(res);
|
|||
|
// TextDecoder is used in upstream gnome-shell, but not yet
|
|||
|
// supported in current Debian.
|
|||
|
const contentStr = imports.byteArray.toString(bytes);
|
|||
|
//const contentStr = new TextDecoder().decode(bytes);
|
|||
|
newConfig = JSON.parse(contentStr);
|
|||
|
} catch (err) {
|
|||
|
logError(err, '[contest-lock] config file');
|
|||
|
return;
|
|||
|
}
|
|||
|
if (!(typeof newConfig === 'object' && newConfig != null)) {
|
|||
|
extLogError('config file: invalid format');
|
|||
|
return;
|
|||
|
}
|
|||
|
if (typeof newConfig.title !== 'string') {
|
|||
|
extLogError('config file: "title" must be a string');
|
|||
|
return;
|
|||
|
}
|
|||
|
if (typeof newConfig.message !== 'string') {
|
|||
|
extLogError('config file: "message" must be a string');
|
|||
|
return;
|
|||
|
}
|
|||
|
if (
|
|||
|
typeof newConfig.startTime !== 'string' ||
|
|||
|
!/^\d{4,}-\d\d-\d\dT\d\d:\d\d:\d\d\+\d\d:\d\d$/.test(newConfig.startTime)
|
|||
|
) {
|
|||
|
extLogError('config file: "startTime" must be a string with format 0000-00-00T00:00:00+00:00');
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
extLog('Loaded new config.')
|
|||
|
config = newConfig;
|
|||
|
startTime = (new Date(newConfig.startTime)).getTime();
|
|||
|
|
|||
|
updateConfig();
|
|||
|
syncActive();
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
function syncActive () {
|
|||
|
if (isActiveChanging) return;
|
|||
|
let beforeStart = false;
|
|||
|
if (startTime != null) {
|
|||
|
const now = new Date();
|
|||
|
const timeToStart = startTime - now.getTime() - HALF_FRAME_TIME_MS;
|
|||
|
beforeStart = timeToStart > 0;
|
|||
|
}
|
|||
|
// ignore disable event when active
|
|||
|
if (beforeStart && isShellReady && (isExtensionEnabled || isActive)) {
|
|||
|
activate();
|
|||
|
} else {
|
|||
|
deactivate();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function updateConfig () {
|
|||
|
if (labelTitle != null) {
|
|||
|
labelTitle.text = config.title;
|
|||
|
}
|
|||
|
if (labelMessage != null) {
|
|||
|
labelMessage.text = config.message;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function updateUser () {
|
|||
|
if (labelUser != null) {
|
|||
|
const realName = user.get_real_name();
|
|||
|
if (realName != null) labelUser.text = realName;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function updateCountdown () {
|
|||
|
countdownTimeoutId = 0;
|
|||
|
const now = new Date();
|
|||
|
const nowTime = now.getTime() + HALF_FRAME_TIME_MS;
|
|||
|
const timeToStart = startTime - nowTime;
|
|||
|
const beforeStart = timeToStart > 0;
|
|||
|
if (!beforeStart) {
|
|||
|
deactivate();
|
|||
|
return GLib.SOURCE_REMOVE;
|
|||
|
}
|
|||
|
const allSecondsToStart = Math.floor(timeToStart / 1000);
|
|||
|
const secondsToStart = allSecondsToStart % 60
|
|||
|
const allMinutesToStart = Math.floor(allSecondsToStart / 60);
|
|||
|
const minutesToStart = allMinutesToStart % 60;
|
|||
|
const hoursToStart = Math.floor(allMinutesToStart / 60);
|
|||
|
|
|||
|
let hoursString = '';
|
|||
|
if (hoursToStart !== 0) hoursString = `${hoursToStart}∶`;
|
|||
|
labelCountdown.text = hoursString +
|
|||
|
minutesToStart.toString().padStart(2, '0') + '∶' +
|
|||
|
secondsToStart.toString().padStart(2, '0');
|
|||
|
|
|||
|
const nextUpdateTime = 1000 - nowTime % 1000
|
|||
|
countdownTimeoutId = GLib.timeout_add(
|
|||
|
GLib.PRIORITY_HIGH,
|
|||
|
nextUpdateTime,
|
|||
|
updateCountdown
|
|||
|
);
|
|||
|
GLib.Source.set_name_by_id(countdownTimeoutId, '[contest-lock] updateCountdown');
|
|||
|
|
|||
|
return GLib.SOURCE_REMOVE;
|
|||
|
}
|
|||
|
|
|||
|
function updateBackgrounds () {
|
|||
|
if (!isActive) return;
|
|||
|
while (bgManagers.length) bgManagers.pop().destroy();
|
|||
|
backgroundGroup.destroy_all_children();
|
|||
|
|
|||
|
for (let monitorIndex = 0; monitorIndex < Main.layoutManager.monitors.length; monitorIndex++) {
|
|||
|
const monitor = Main.layoutManager.monitors[monitorIndex];
|
|||
|
const widget = new St.Widget({
|
|||
|
style_class: 'screen-shield-background',
|
|||
|
x: monitor.x,
|
|||
|
y: monitor.y,
|
|||
|
width: monitor.width,
|
|||
|
height: monitor.height,
|
|||
|
effect: new Shell.BlurEffect({
|
|||
|
name: 'blur',
|
|||
|
brightness: BLUR_BRIGHTNESS,
|
|||
|
sigma: BLUR_SIGMA,
|
|||
|
}),
|
|||
|
});
|
|||
|
|
|||
|
const bgManager = new Background.BackgroundManager({
|
|||
|
container: widget,
|
|||
|
monitorIndex,
|
|||
|
controlPosition: false,
|
|||
|
});
|
|||
|
|
|||
|
bgManagers.push(bgManager);
|
|||
|
|
|||
|
backgroundGroup.add_child(widget);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function pointerHideTimer () {
|
|||
|
if (pointerHideId !== 0) {
|
|||
|
GLib.source_remove(pointerHideId);
|
|||
|
pointerHideId = 0;
|
|||
|
}
|
|||
|
if (!isActive) return GLib.SOURCE_REMOVE;
|
|||
|
|
|||
|
const timeToHide = lastMotionTime + POINTER_HIDE_TIMEOUT - GLib.get_monotonic_time();
|
|||
|
if (timeToHide <= 0) {
|
|||
|
cursorTracker.set_pointer_visible(false);
|
|||
|
pointerHidden = true;
|
|||
|
return GLib.SOURCE_REMOVE;
|
|||
|
}
|
|||
|
|
|||
|
pointerHideId = GLib.timeout_add(
|
|||
|
GLib.PRIORITY_HIGH,
|
|||
|
timeToHide / 1000 + 20,
|
|||
|
pointerHideTimer
|
|||
|
);
|
|||
|
GLib.Source.set_name_by_id(pointerHideId, '[contest-lock] pointerHide');
|
|||
|
|
|||
|
return GLib.SOURCE_REMOVE;
|
|||
|
}
|
|||
|
|
|||
|
function activate () {
|
|||
|
if (isActive) return;
|
|||
|
isActiveChanging = true;
|
|||
|
isActive = true;
|
|||
|
|
|||
|
grab = Main.pushModal(Main.uiGroup, { actionMode: Shell.ActionMode.LOCK_SCREEN });
|
|||
|
if (typeof grab === 'boolean') { // gnome 38
|
|||
|
if (!grab) {
|
|||
|
grab = Main.pushModal(Main.uiGroup, {
|
|||
|
options: Meta.ModalOptions.POINTER_ALREADY_GRABBED,
|
|||
|
actionMode: Shell.ActionMode.LOCK_SCREEN
|
|||
|
});
|
|||
|
}
|
|||
|
if (!grab) {
|
|||
|
extLogError('Failed to activate: Could not obtain keyboard grab.');
|
|||
|
return;
|
|||
|
}
|
|||
|
grab = Main.uiGroup;
|
|||
|
} else if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) {
|
|||
|
Main.popModal(grab);
|
|||
|
grab = null;
|
|||
|
extLogError('Failed to activate: Could not obtain keyboard grab.');
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
actor.show();
|
|||
|
|
|||
|
Main.sessionMode.pushMode('unlock-dialog');
|
|||
|
|
|||
|
backgroundGroup = new Clutter.Actor();
|
|||
|
|
|||
|
motionId = global.stage.connect('captured-event', (stage, event) => {
|
|||
|
if (event.type() === Clutter.EventType.MOTION) {
|
|||
|
lastMotionTime = GLib.get_monotonic_time();
|
|||
|
if (pointerHidden) {
|
|||
|
cursorTracker.set_pointer_visible(true);
|
|||
|
pointerHidden = false;
|
|||
|
pointerHideTimer();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return Clutter.EVENT_PROPAGATE;
|
|||
|
});
|
|||
|
cursorTracker.set_pointer_visible(false);
|
|||
|
pointerHidden = true;
|
|||
|
|
|||
|
labelCountdown = new St.Label({
|
|||
|
style_class: 'contest-lock-countdown',
|
|||
|
x_align: Clutter.ActorAlign.CENTER,
|
|||
|
});
|
|||
|
labelTitle = new St.Label({
|
|||
|
style_class: 'contest-lock-title',
|
|||
|
x_align: Clutter.ActorAlign.CENTER,
|
|||
|
});
|
|||
|
labelMessage = new St.Label({
|
|||
|
style_class: 'contest-lock-message',
|
|||
|
x_align: Clutter.ActorAlign.CENTER,
|
|||
|
});
|
|||
|
labelUser = new St.Label({
|
|||
|
style_class: 'contest-lock-user',
|
|||
|
x_align: Clutter.ActorAlign.CENTER,
|
|||
|
});
|
|||
|
|
|||
|
const stack = new St.BoxLayout({
|
|||
|
style_class: 'contest-lock-stack',
|
|||
|
vertical: true,
|
|||
|
x_expand: true,
|
|||
|
y_expand: true,
|
|||
|
x_align: Clutter.ActorAlign.CENTER,
|
|||
|
y_align: Clutter.ActorAlign.CENTER,
|
|||
|
})
|
|||
|
stack.add_child(labelUser);
|
|||
|
stack.add_child(labelCountdown);
|
|||
|
stack.add_child(labelTitle);
|
|||
|
stack.add_child(labelMessage);
|
|||
|
|
|||
|
const mainBox = new St.BoxLayout();
|
|||
|
mainBox.add_constraint(new Layout.MonitorConstraint({ primary: true }));
|
|||
|
mainBox.add_child(stack);
|
|||
|
|
|||
|
lockDialog = new St.Widget({
|
|||
|
name: 'contestLockDialog',
|
|||
|
accessible_role: Atk.Role.WINDOW,
|
|||
|
visible: false,
|
|||
|
reactive: true,
|
|||
|
can_focus: true,
|
|||
|
x_expand: true,
|
|||
|
y_expand: true,
|
|||
|
pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
|
|||
|
});
|
|||
|
lockDialog.add_child(backgroundGroup);
|
|||
|
lockDialog.add_child(mainBox);
|
|||
|
|
|||
|
updateConfig();
|
|||
|
updateUser();
|
|||
|
updateCountdown();
|
|||
|
updateBackgrounds();
|
|||
|
|
|||
|
// countdown may have just expired before we called updateCountdown
|
|||
|
if (!isActive) return;
|
|||
|
|
|||
|
actor.add_child(lockDialog);
|
|||
|
lockDialog.show();
|
|||
|
|
|||
|
extLog('Activated.')
|
|||
|
isActiveChanging = false;
|
|||
|
}
|
|||
|
|
|||
|
function deactivate () {
|
|||
|
if (!isActive) return;
|
|||
|
isActiveChanging = true;
|
|||
|
isActive = false;
|
|||
|
|
|||
|
if (Main.sessionMode.currentMode === 'unlock-dialog') {
|
|||
|
Main.sessionMode.popMode('unlock-dialog');
|
|||
|
}
|
|||
|
|
|||
|
Main.popModal(grab);
|
|||
|
grab = null;
|
|||
|
|
|||
|
if (countdownTimeoutId !== 0) {
|
|||
|
GLib.source_remove(countdownTimeoutId);
|
|||
|
countdownTimeoutId = 0;
|
|||
|
}
|
|||
|
|
|||
|
actor.hide();
|
|||
|
labelCountdown = null;
|
|||
|
labelTitle = null;
|
|||
|
labelMessage = null;
|
|||
|
labelUser = null;
|
|||
|
while (bgManagers.length) bgManagers.pop().destroy();
|
|||
|
lockDialog.destroy();
|
|||
|
lockDialog = null;
|
|||
|
|
|||
|
if (motionId) {
|
|||
|
global.stage.disconnect(motionId);
|
|||
|
motionId = 0;
|
|||
|
}
|
|||
|
cursorTracker.set_pointer_visible(true);
|
|||
|
|
|||
|
extLog('Deactivated.');
|
|||
|
isActiveChanging = false;
|
|||
|
}
|
|||
|
|
|||
|
function init (extension) {
|
|||
|
actor = Main.layoutManager.screenShieldGroup;
|
|||
|
|
|||
|
const userName = GLib.get_user_name();
|
|||
|
user = AccountsService.UserManager.get_default().get_user(userName);
|
|||
|
if (!user) return;
|
|||
|
|
|||
|
user.connect('notify::is-loaded', updateUser);
|
|||
|
user.connect('changed', updateUser);
|
|||
|
updateUser();
|
|||
|
|
|||
|
Main.layoutManager.connect('monitors-changed', updateBackgrounds);
|
|||
|
|
|||
|
cursorTracker = Meta.CursorTracker.get_for_display(global.display);
|
|||
|
|
|||
|
if (!Main.layoutManager._startingUp) {
|
|||
|
isShellReady = true;
|
|||
|
} else {
|
|||
|
Main.layoutManager.connect('startup-complete', () => {
|
|||
|
isShellReady = true;
|
|||
|
syncActive();
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// TODO: When we drop compatibility with gnome <42, remove this code,
|
|||
|
// rename the stylesheet back to stylesheet.css (so that it is loaded
|
|||
|
// by the extension system) and add a session-modes property which
|
|||
|
// includes unlock-dialog to metadata.json.
|
|||
|
const theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
|
|||
|
const stylesheetFile = extension.dir.get_child('stylesheet-always.css');
|
|||
|
theme.load_stylesheet(stylesheetFile);
|
|||
|
|
|||
|
// TODO: When we drop compatibility with gnome <42, remove this code.
|
|||
|
// gnome 38 has a bug that causes extensions to break when running
|
|||
|
// `dconf update` while the screen is locked.
|
|||
|
Main.extensionManager.reloadExtension = function () {};
|
|||
|
|
|||
|
configFile = Gio.File.new_for_path('/etc/contest-lock.json');
|
|||
|
configMonitor = configFile.monitor_file(Gio.FileMonitorFlags.NONE, null);
|
|||
|
configMonitor.set_rate_limit(1000);
|
|||
|
configMonitor.connect('changed', loadConfig);
|
|||
|
loadConfig();
|
|||
|
}
|
|||
|
|
|||
|
function enable () {
|
|||
|
isExtensionEnabled = true;
|
|||
|
syncActive();
|
|||
|
}
|
|||
|
|
|||
|
function disable () {
|
|||
|
isExtensionEnabled = false;
|
|||
|
syncActive();
|
|||
|
}
|