Skip to content

Commit c41827f

Browse files
authored
Add Day Progress applet - A visual progress indicator for the current day (#7846)
* Add Day Progress applet - A visual progress indicator for the current day Features: - Shows elapsed and remaining time as pie chart or progress bar - Customizable start and end hours (default 8:00 - 18:00) - Two visualization styles: Pie and Pie (no border) - Adjustable size (width/height) - Click to view elapsed/remaining time - Settings menu integration - Full i18n support (English and Spanish translations included) The applet helps users visualize how much of their workday has passed and how much time remains, directly from the Cinnamon panel." * - Renamed the shared `.label` style to `.day-progress-label` to avoid clashing with Cinnamon themes or other xlets. - Translated the remaining Spanish source comments to English for consistency across the codebase. * Regenerate translation template and remove en.po
1 parent be5cdb7 commit c41827f

File tree

11 files changed

+674
-0
lines changed

11 files changed

+674
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## [1.0.1] - 2025-12-11
6+
7+
### Changed
8+
- Renamed the shared `.label` style to `.day-progress-label` to avoid clashing with Cinnamon themes or other xlets.
9+
- Translated the remaining Spanish source comments to English for consistency across the codebase.
10+
11+
## [1.0] - 2025-10-03
12+
13+
### Added
14+
- Initial release of Day Progress applet
15+
- Customizable pie chart progress indicator
16+
- Time range configuration (start/end hours)
17+
- Toggle between elapsed and remaining time display
18+
- Two visual styles: Pie with border and Pie without border
19+
- Adjustable size (width and height)
20+
- Detailed information popup menu
21+
- Translation support with English and Spanish languages
22+
- Settings panel integration
23+
24+
### Features
25+
- Real-time progress tracking
26+
- Auto-update every 10 seconds
27+
- Support for 24-hour cycle or custom time ranges
28+
- Midnight wrap-around support
29+
- Clean, minimal design that fits any panel theme

day-progress@chmodmasx/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Day Progress Applet
2+
3+
A Cinnamon applet that displays a customizable progress pie chart in the panel to help you track your day time.
4+
5+
## Features
6+
7+
- **Visual Progress Indicator**: Shows your day's progress as a pie chart in the panel
8+
- **Customizable Time Range**: Set custom start and end hours to track your work day, school hours, or any time period
9+
- **Multiple Display Modes**: Choose between showing elapsed or remaining time
10+
- **Flexible Styling**: Two pie chart styles - with or without border
11+
- **Configurable Size**: Adjust width and height to fit your panel perfectly
12+
- **Detailed Information**: Click the applet to see exact elapsed and remaining time with percentages
13+
14+
## Settings
15+
16+
- **Show time elapsed**: Toggle between showing elapsed time or remaining time
17+
- **Width**: Adjust the width of the indicator (5-150)
18+
- **Height**: Adjust the height of the indicator (5-50)
19+
- **Style**: Choose between "Pie" (with border) or "Pie (no border)"
20+
- **Start hour**: Set the hour when your tracking period begins (0-23)
21+
- **End hour**: Set the hour when your tracking period ends (0-23)
22+
23+
## Usage
24+
25+
1. Add the applet to your panel
26+
2. Configure the start and end hours according to your needs (e.g., 9 AM to 6 PM for work hours)
27+
3. The pie chart will automatically update every 10 seconds showing your progress
28+
4. Click the applet to see detailed elapsed and remaining time
29+
30+
## Examples
31+
32+
- Track your work day: Set start hour to 9 and end hour to 17 (9 AM to 5 PM)
33+
- Track your full day: Set both start and end hour to 0 for a complete 24-hour cycle
34+
- Track school hours: Set start hour to 8 and end hour to 15 (8 AM to 3 PM)
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
const Applet = imports.ui.applet;
2+
const St = imports.gi.St;
3+
const Lang = imports.lang;
4+
const GLib = imports.gi.GLib;
5+
const Clutter = imports.gi.Clutter;
6+
const Cairo = imports.cairo;
7+
const Settings = imports.ui.settings;
8+
const PopupMenu = imports.ui.popupMenu;
9+
const Gettext = imports.gettext;
10+
11+
const UUID = "day-progress@chmodmasx";
12+
13+
Gettext.bindtextdomain(UUID, GLib.get_home_dir() + "/.local/share/locale");
14+
15+
function _(str) {
16+
return Gettext.dgettext(UUID, str);
17+
}
18+
19+
// Pie chart drawing class
20+
function Pie() {
21+
this._init.apply(this, arguments);
22+
}
23+
24+
Pie.prototype = {
25+
_init: function() {
26+
this._angle = 0;
27+
this._outerBorder = true;
28+
this.actor = new St.DrawingArea({
29+
style_class: 'pie',
30+
visible: false
31+
});
32+
this.actor.connect('repaint', Lang.bind(this, this._onRepaint));
33+
},
34+
35+
setAngle: function(angle) {
36+
if (this._angle === angle)
37+
return;
38+
this._angle = angle;
39+
this.actor.queue_repaint();
40+
},
41+
42+
calculateStyles: function(width, height, outerBorder) {
43+
let min = Math.min(width, height);
44+
min += 0.5;
45+
this.actor.set_style('width: ' + min + 'em; height: ' + min + 'em;');
46+
this._outerBorder = outerBorder;
47+
},
48+
49+
_onRepaint: function(area) {
50+
let [width, height] = area.get_surface_size();
51+
let cr = area.get_context();
52+
let themeNode = area.get_theme_node();
53+
54+
let fillColor = themeNode.get_color('-pie-color');
55+
let bgColor = themeNode.get_color('-pie-background-color');
56+
let borderColor = themeNode.get_color('-pie-border-color');
57+
let borderWidth = themeNode.get_length('-pie-border-width');
58+
let radius = Math.min(width / 2, height / 2);
59+
60+
let startAngle = 3 * Math.PI / 2;
61+
let endAngle = startAngle + this._angle;
62+
63+
cr.setLineCap(Cairo.LineCap.ROUND);
64+
cr.setLineJoin(Cairo.LineJoin.ROUND);
65+
cr.translate(width / 2, height / 2);
66+
67+
if (this._angle < 2 * Math.PI)
68+
cr.moveTo(0, 0);
69+
70+
cr.arc(0, 0, radius - borderWidth * (this._outerBorder ? 2.6 : 1), startAngle, endAngle);
71+
72+
if (this._angle < 2 * Math.PI)
73+
cr.lineTo(0, 0);
74+
75+
cr.closePath();
76+
77+
cr.setLineWidth(0);
78+
Clutter.cairo_set_source_color(cr, fillColor);
79+
cr.fill();
80+
81+
if (!this._outerBorder) {
82+
cr.moveTo(0, 0);
83+
84+
if (this._angle >= 2 * Math.PI || this._angle >= 0) {
85+
cr.arc(0, 0, radius - borderWidth, startAngle, startAngle - 0.000000000001);
86+
} else {
87+
cr.arc(0, 0, radius - borderWidth, endAngle, startAngle);
88+
}
89+
90+
cr.lineTo(0, 0);
91+
cr.closePath();
92+
cr.setLineWidth(0);
93+
Clutter.cairo_set_source_color(cr, bgColor);
94+
cr.fill();
95+
}
96+
97+
// Draw outer border
98+
if (this._outerBorder) {
99+
cr.arc(0, 0, radius - borderWidth, startAngle, startAngle + 2 * Math.PI);
100+
cr.setLineWidth(borderWidth);
101+
Clutter.cairo_set_source_color(cr, borderColor);
102+
cr.stroke();
103+
}
104+
},
105+
106+
destroy: function() {
107+
this.actor.destroy();
108+
}
109+
};
110+
111+
// Main applet
112+
function DayProgressApplet(metadata, orientation, panelHeight, instanceId) {
113+
this._init(metadata, orientation, panelHeight, instanceId);
114+
}
115+
116+
DayProgressApplet.prototype = {
117+
__proto__: Applet.Applet.prototype,
118+
119+
_init: function(metadata, orientation, panelHeight, instanceId) {
120+
Applet.Applet.prototype._init.call(this, orientation, panelHeight, instanceId);
121+
122+
this.metadata = metadata;
123+
this.instanceId = instanceId;
124+
125+
// Configuration
126+
this.settings = new Settings.AppletSettings(this, metadata.uuid, instanceId);
127+
128+
this.settings.bind("show-elapsed", "showElapsed", Lang.bind(this, this.updateBar));
129+
this.settings.bind("width", "width", Lang.bind(this, this.onSizeChanged));
130+
this.settings.bind("height", "height", Lang.bind(this, this.onSizeChanged));
131+
this.settings.bind("style", "style", Lang.bind(this, this.onStyleChanged));
132+
this.settings.bind("start-hour", "startHour", Lang.bind(this, this.updateBar));
133+
this.settings.bind("end-hour", "endHour", Lang.bind(this, this.updateBar));
134+
135+
// Initialize minutes to 0 since they are not configurable
136+
this.startMinute = 0;
137+
this.endMinute = 0;
138+
139+
// Build UI
140+
this.box = new St.BoxLayout({
141+
xAlign: Clutter.ActorAlign.CENTER,
142+
yAlign: Clutter.ActorAlign.CENTER
143+
});
144+
145+
this.pie = new Pie();
146+
147+
this.box.add_child(this.pie.actor);
148+
this.actor.add_actor(this.box);
149+
150+
// Menu
151+
this.menuManager = new PopupMenu.PopupMenuManager(this);
152+
this.menu = new Applet.AppletPopupMenu(this, orientation);
153+
this.menuManager.addMenu(this.menu);
154+
155+
// Menu items
156+
this.menuElapsedContainer = new PopupMenu.PopupBaseMenuItem({ reactive: false });
157+
this.elapsedLabel = new St.Label({
158+
text: _("Elapsed"),
159+
x_align: Clutter.ActorAlign.START,
160+
x_expand: true,
161+
style_class: 'day-progress-label'
162+
});
163+
this.elapsedValue = new St.Label({ text: '' });
164+
this.menuElapsedContainer.addActor(this.elapsedLabel);
165+
this.menuElapsedContainer.addActor(this.elapsedValue);
166+
167+
this.menuRemainingContainer = new PopupMenu.PopupBaseMenuItem({ reactive: false });
168+
this.remainingLabel = new St.Label({
169+
text: _("Remaining"),
170+
x_align: Clutter.ActorAlign.START,
171+
x_expand: true,
172+
style_class: 'day-progress-label'
173+
});
174+
this.remainingValue = new St.Label({ text: '' });
175+
this.menuRemainingContainer.addActor(this.remainingLabel);
176+
this.menuRemainingContainer.addActor(this.remainingValue);
177+
178+
this.menu.addMenuItem(this.menuElapsedContainer);
179+
this.menu.addMenuItem(this.menuRemainingContainer);
180+
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
181+
182+
let settingsItem = new PopupMenu.PopupMenuItem(_("Settings"));
183+
settingsItem.connect('activate', Lang.bind(this, function() {
184+
imports.gi.Gio.Subprocess.new(
185+
['cinnamon-settings', 'applets', this.metadata.uuid, this.instanceId.toString()],
186+
imports.gi.Gio.SubprocessFlags.NONE
187+
);
188+
}));
189+
this.menu.addMenuItem(settingsItem);
190+
191+
// Initialize styles and values
192+
this.calculateStyles();
193+
194+
// Update immediately to populate the menu values
195+
this.updateBar();
196+
197+
// Timer to update every 10 seconds
198+
this.timerID = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, Lang.bind(this, function() {
199+
this.updateBar();
200+
return GLib.SOURCE_CONTINUE;
201+
}));
202+
},
203+
204+
on_applet_clicked: function(event) {
205+
this.menu.toggle();
206+
},
207+
208+
onSizeChanged: function() {
209+
this.calculateStyles();
210+
this.updateBar();
211+
},
212+
213+
onStyleChanged: function() {
214+
this.calculateStyles();
215+
},
216+
217+
calculateStyles: function() {
218+
let w = this.width / 5;
219+
let h = this.height / 10;
220+
221+
// style 0 = Pie (with border), style 1 = Pie (no border)
222+
this.pie.calculateStyles(w, h, this.style == 0);
223+
this.pie.actor.visible = true;
224+
225+
this.updateBar();
226+
},
227+
228+
updateBar: function() {
229+
let localDateTime = GLib.DateTime.new_now_local();
230+
let startTimeFraction = this.startHour / 24 + this.startMinute / (60 * 24);
231+
let endTimeFraction = this.endHour / 24 + this.endMinute / (60 * 24);
232+
233+
let currentTimeFraction = (localDateTime.get_hour() + localDateTime.get_minute() / 60 + localDateTime.get_second() / 3600) / 24;
234+
235+
let percentElapsedOfPeriod;
236+
237+
// If startHour and endHour are equal, consider it as a full 24-hour period
238+
if (this.startHour === this.endHour && this.startMinute === this.endMinute) {
239+
percentElapsedOfPeriod = currentTimeFraction;
240+
}
241+
// No midnight wrap-around
242+
else if (endTimeFraction > startTimeFraction) {
243+
percentElapsedOfPeriod = this.mapNumber(
244+
this.clamp(currentTimeFraction, startTimeFraction, endTimeFraction),
245+
startTimeFraction, endTimeFraction, 0, 1
246+
);
247+
} else {
248+
// Midnight wrap-around
249+
if (currentTimeFraction >= endTimeFraction && currentTimeFraction < startTimeFraction) {
250+
percentElapsedOfPeriod = 1;
251+
} else {
252+
let durationFraction = (1 - (startTimeFraction - endTimeFraction));
253+
let offset = 1 - startTimeFraction;
254+
let offsettedTimeFraction = (currentTimeFraction + 1 + offset) % 1;
255+
percentElapsedOfPeriod = this.mapNumber(
256+
this.clamp(offsettedTimeFraction, 0, durationFraction),
257+
0, durationFraction, 0, 1
258+
);
259+
}
260+
}
261+
262+
let percentRemainingOfPeriod = 1 - percentElapsedOfPeriod;
263+
264+
// Update pie angle
265+
this.pie.setAngle((this.showElapsed ? percentElapsedOfPeriod : percentRemainingOfPeriod) * (Math.PI * 2.0));
266+
267+
let duration;
268+
if (this.startHour === this.endHour && this.startMinute === this.endMinute) {
269+
duration = 1; // Full 24 hours
270+
} else if (endTimeFraction > startTimeFraction) {
271+
duration = (endTimeFraction - startTimeFraction);
272+
} else {
273+
duration = (1 - (startTimeFraction - endTimeFraction));
274+
}
275+
276+
let elapsedHours = Math.floor(percentElapsedOfPeriod * duration * 24);
277+
let elapsedMinutes = Math.floor((percentElapsedOfPeriod * duration * 24 * 60) % 60);
278+
let remainingHours = Math.floor(percentRemainingOfPeriod * duration * 24);
279+
let remainingMinutes = Math.floor((percentRemainingOfPeriod * duration * 24 * 60) % 60);
280+
281+
this.elapsedValue.text = elapsedHours + 'h ' + elapsedMinutes + 'm | ' + Math.round(percentElapsedOfPeriod * 100) + '%';
282+
this.remainingValue.text = remainingHours + 'h ' + remainingMinutes + 'm | ' + Math.round(percentRemainingOfPeriod * 100) + '%';
283+
},
284+
285+
mapNumber: function(number, inMin, inMax, outMin, outMax) {
286+
return (number - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
287+
},
288+
289+
clamp: function(number, min, max) {
290+
return Math.max(min, Math.min(number, max));
291+
},
292+
293+
on_applet_removed_from_panel: function() {
294+
if (this.timerID) {
295+
GLib.Source.remove(this.timerID);
296+
this.timerID = null;
297+
}
298+
this.settings.finalize();
299+
}
300+
};
301+
302+
function main(metadata, orientation, panelHeight, instanceId) {
303+
return new DayProgressApplet(metadata, orientation, panelHeight, instanceId);
304+
}
6.06 KB
Loading
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"uuid": "day-progress@chmodmasx",
3+
"name": "Day Progress",
4+
"description": "Displays a customizable progress pie chart in the panel to help you track your day time.",
5+
"version": "1.0.1",
6+
"cinnamon-version": ["6.4"],
7+
"author": "chmodmasx"
8+
}

0 commit comments

Comments
 (0)