// taper
// linux session timer and alert
// vgmlr.com/mlwrk
const Main = imports.ui.main;
const Applet = imports.ui.applet;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const PopupMenu = imports.ui.popupMenu;
const Settings = imports.ui.settings;
class SessionTimerApplet extends Applet.TextIconApplet {
constructor(metadata, orientation, panel_height, instance_id) {
super(orientation, panel_height, instance_id);
this.seconds = 0;
this._currentInterval = 0;
try {
this.set_applet_label("00:00:00");
this._buildMenu(orientation);
this.settings = new Settings.AppletSettings(this, metadata.uuid, instance_id);
this.settings.bindProperty(Settings.BindingDirection.BIDIRECTIONAL, "history", "history", this._updateHistoryMenu, null);
this.settings.bindProperty(Settings.BindingDirection.BIDIRECTIONAL, "last_recorded_time", "last_recorded_time", null, null);
this.settings.bindProperty(Settings.BindingDirection.BIDIRECTIONAL, "session_start_timestamp", "session_start_timestamp", null, null);
if (this.last_recorded_time > 0 && this.session_start_timestamp > 0) {
this._archiveSession(this.session_start_timestamp, this.last_recorded_time);
}
this._startNewSession();
this._setupDBus();
} catch (e) {
global.logError("Taper Error: " + e);
}
}
_buildMenu(orientation) {
this.menuManager = new PopupMenu.PopupMenuManager(this);
this.menu = new Applet.AppletPopupMenu(this, orientation);
this.menuManager.addMenu(this.menu);
this._intervalSection = new PopupMenu.PopupMenuSection();
this.menu.addMenuItem(this._intervalSection);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this._historySection = new PopupMenu.PopupMenuSection();
this.menu.addMenuItem(this._historySection);
}
_startNewSession() {
this.seconds = 0;
this.session_start_timestamp = Date.now();
this.last_recorded_time = 0;
this.settings.setValue("session_start_timestamp", this.session_start_timestamp);
this.settings.setValue("last_recorded_time", 0);
this._updateLabel();
this._updateTooltip("No Alert");
this._updateHistoryMenu();
this._updateIntervalMenu();
if (!this._timerId) {
this._timerId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => this._onTick());
}
}
_onTick() {
this.seconds++;
this._updateLabel();
if (this.seconds % 10 === 0) {
this.last_recorded_time = this.seconds;
this.settings.setValue("last_recorded_time", this.seconds);
}
if (this._currentInterval > 0 && this.seconds % (this._currentInterval * 60) === 0) {
Main.notify("Taper Session", `${this._currentInterval} Minutes Elapsed`);
}
return true;
}
_archiveSession(timestamp, durationSeconds) {
let h = Math.floor(durationSeconds / 3600);
let m = Math.floor((durationSeconds % 3600) / 60);
let s = durationSeconds % 60;
let durationStr = h.toString().padStart(2, '0') + ":" + m.toString().padStart(2, '0') + ":" + s.toString().padStart(2, '0');
let startDate = new Date(timestamp);
let currentYear = startDate.getFullYear();
let march20Current = new Date(currentYear, 2, 20);
let newYearDate = (startDate < march20Current) ? new Date(currentYear - 1, 2, 20) : march20Current;
let daysSince = Math.floor((startDate.getTime() - newYearDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
let hh = startDate.getHours().toString().padStart(2, '0');
let mm = startDate.getMinutes().toString().padStart(2, '0');
let ddMon = startDate.getDate().toString().padStart(2, '0') + " " + months[startDate.getMonth()];
let text = hh + ":" + mm + " " + daysSince + " (" + ddMon + ") " + durationStr;
let tempHistory = [...this.history];
tempHistory.push(text);
if (tempHistory.length > 5) tempHistory.shift();
this.history = tempHistory;
this.settings.setValue("history", tempHistory);
}
_updateHistoryMenu() {
if (!this._historySection) return;
this._historySection.removeAll();
this.history.forEach(time => {
this._historySection.addMenuItem(new PopupMenu.PopupMenuItem(time, { reactive: false }));
});
}
_updateLabel() {
let h = Math.floor(this.seconds / 3600);
let m = Math.floor((this.seconds % 3600) / 60);
let s = this.seconds % 60;
let text = h.toString().padStart(2, '0') + ":" + m.toString().padStart(2, '0') + ":" + s.toString().padStart(2, '0');
this.set_applet_label(text);
}
_updateIntervalMenu() {
if (!this._intervalSection) return;
this._intervalSection.removeAll();
const options = [{l: "No Alert", v: 0}, {l: "15 Minutes", v: 15}, {l: "30 Minutes", v: 30}, {l: "60 Minutes", v: 60}];
options.forEach(opt => {
let item = new PopupMenu.PopupMenuItem(opt.l);
if (this._currentInterval === opt.v) {
item.setShowDot(true);
item.label.set_style("font-weight: bold;");
this._updateTooltip(opt.v === 0 ? "No Alert" : opt.l);
}
item.connect('activate', () => {
this._currentInterval = opt.v;
this._updateIntervalMenu();
});
this._intervalSection.addMenuItem(item);
});
}
_updateTooltip(intervalText) {
this.set_applet_tooltip(`Taper: ${intervalText}`);
}
_setupDBus() {
try {
let bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, null);
this._dbusId = bus.signal_subscribe(
null, null, 'PrepareForSleep', null, null, Gio.DBusSignalFlags.NONE,
(conn, sender, obj, iface, sig, params) => {
let sleeping = params.get_child_value(0).unpack();
if (sleeping) {
if (this.seconds > 0) {
this._archiveSession(this.session_start_timestamp, this.seconds);
}
} else {
this._startNewSession();
}
}
);
} catch (e) {
global.logError("D-Bus Error: " + e);
}
}
on_applet_clicked() {
this.menu.toggle();
}
on_applet_removed_from_panel() {
if (this._timerId) GLib.source_remove(this._timerId);
if (this._dbusId) {
let bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, null);
bus.signal_unsubscribe(this._dbusId);
}
}
}
function main(metadata, orientation, panel_height, instance_id) {
return new SessionTimerApplet(metadata, orientation, panel_height, instance_id);
}