Skip to content

Commit 33be9b2

Browse files
committed
timer: Add launcher with recent timer history
Replace timer UI with launcher screen showing 4 quick-start options: - Three most recently used timers - Timer button for manual entry Recent timers display MM:SS format and auto-start when selected. Using a recent timer moves it to front of history. Custom button opens timer UI with most recent duration and allows the user to start it manually. Timer history persists across reboots.
1 parent 99ae2f3 commit 33be9b2

File tree

5 files changed

+274
-50
lines changed

5 files changed

+274
-50
lines changed

src/components/settings/Settings.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,34 @@ void Settings::SaveSettingsToFile() {
4545
fs.FileWrite(&settingsFile, reinterpret_cast<uint8_t*>(&settings), sizeof(settings));
4646
fs.FileClose(&settingsFile);
4747
}
48+
49+
void Settings::AddTimerDuration(uint32_t duration) {
50+
// Search for existing duration in array
51+
int existingIndex = -1;
52+
for (int i = 0; i < 3; i++) {
53+
if (settings.lastTimerDurations[i] == duration) {
54+
existingIndex = i;
55+
break;
56+
}
57+
}
58+
59+
// If duration already exists, move it to front
60+
if (existingIndex >= 0) {
61+
if (existingIndex == 0) {
62+
// Already at front, no change needed
63+
return;
64+
}
65+
// Shift elements before existingIndex forward by one
66+
for (int i = existingIndex; i > 0; i--) {
67+
settings.lastTimerDurations[i] = settings.lastTimerDurations[i - 1];
68+
}
69+
settings.lastTimerDurations[0] = duration;
70+
} else {
71+
// New duration - shift all down and insert at front
72+
settings.lastTimerDurations[2] = settings.lastTimerDurations[1];
73+
settings.lastTimerDurations[1] = settings.lastTimerDurations[0];
74+
settings.lastTimerDurations[0] = duration;
75+
}
76+
77+
settingsChanged = true;
78+
}

src/components/settings/Settings.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,15 @@ namespace Pinetime {
305305
return settings.stepsGoal;
306306
};
307307

308+
uint32_t GetLastTimerDuration(uint8_t index) const {
309+
if (index >= 3) {
310+
return settings.lastTimerDurations[0];
311+
}
312+
return settings.lastTimerDurations[index];
313+
};
314+
315+
void AddTimerDuration(uint32_t duration);
316+
308317
void SetBleRadioEnabled(bool enabled) {
309318
bleRadioEnabled = enabled;
310319
};
@@ -383,6 +392,8 @@ namespace Pinetime {
383392

384393
bool dfuAndFsEnabledOnBoot = false;
385394
uint16_t heartRateBackgroundPeriod = std::numeric_limits<uint16_t>::max(); // Disabled by default
395+
396+
uint32_t lastTimerDurations[3] = {300000, 600000, 900000};
386397
};
387398

388399
SettingsData settings;

src/displayapp/fonts/fonts.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"sources": [
2929
{
3030
"file": "JetBrainsMono-Light.ttf",
31-
"range": "0x25, 0x2D, 0x2F, 0x30-0x3a, 0x43, 0x46, 0xb0"
31+
"range": "0x25, 0x2B, 0x2D, 0x2F, 0x30-0x3a, 0x43, 0x46, 0xb0"
3232
}
3333
],
3434
"bpp": 1,

src/displayapp/screens/Timer.cpp

Lines changed: 211 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ using namespace Pinetime::Applications::Screens;
88

99
static void btnEventHandler(lv_obj_t* obj, lv_event_t event) {
1010
auto* screen = static_cast<Timer*>(obj->user_data);
11-
if (event == LV_EVENT_PRESSED) {
11+
if (screen->launcherMode && event == LV_EVENT_CLICKED) {
12+
screen->OnLauncherButtonClicked(obj);
13+
} else if (event == LV_EVENT_PRESSED) {
1214
screen->ButtonPressed();
1315
} else if (event == LV_EVENT_RELEASED || event == LV_EVENT_PRESS_LOST) {
1416
screen->MaskReset();
@@ -17,62 +19,25 @@ static void btnEventHandler(lv_obj_t* obj, lv_event_t event) {
1719
}
1820
}
1921

20-
Timer::Timer(Controllers::Timer& timerController) : timer {timerController} {
21-
22-
lv_obj_t* colonLabel = lv_label_create(lv_scr_act(), nullptr);
23-
lv_obj_set_style_local_text_font(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
24-
lv_obj_set_style_local_text_color(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
25-
lv_label_set_text_static(colonLabel, ":");
26-
lv_obj_align(colonLabel, lv_scr_act(), LV_ALIGN_CENTER, 0, -29);
27-
28-
minuteCounter.Create();
29-
secondCounter.Create();
30-
lv_obj_align(minuteCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0);
31-
lv_obj_align(secondCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0);
32-
33-
highlightObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
34-
lv_obj_set_size(highlightObjectMask, 240, 50);
35-
lv_obj_align(highlightObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
36-
37-
lv_draw_mask_line_param_t tmpMaskLine;
38-
39-
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_LEFT);
40-
highlightMask = lv_objmask_add_mask(highlightObjectMask, &tmpMaskLine);
41-
42-
lv_obj_t* btnHighlight = lv_obj_create(highlightObjectMask, nullptr);
43-
lv_obj_set_style_local_radius(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
44-
lv_obj_set_style_local_bg_color(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE);
45-
lv_obj_set_size(btnHighlight, LV_HOR_RES, 50);
46-
lv_obj_align(btnHighlight, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
47-
48-
btnObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
49-
lv_obj_set_size(btnObjectMask, 240, 50);
50-
lv_obj_align(btnObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
51-
52-
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_RIGHT);
53-
btnMask = lv_objmask_add_mask(btnObjectMask, &tmpMaskLine);
54-
55-
btnPlayPause = lv_btn_create(btnObjectMask, nullptr);
56-
btnPlayPause->user_data = this;
57-
lv_obj_set_style_local_radius(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
58-
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
59-
lv_obj_set_event_cb(btnPlayPause, btnEventHandler);
60-
lv_obj_set_size(btnPlayPause, LV_HOR_RES, 50);
61-
62-
// Create the label as a child of the button so it stays centered by default
63-
txtPlayPause = lv_label_create(btnPlayPause, nullptr);
22+
Timer::Timer(Controllers::Timer& timerController, Controllers::MotorController& motorController, Controllers::Settings& settingsController)
23+
: timer {timerController}, motorController {motorController}, settingsController {settingsController} {
6424

25+
// If timer is already running, skip launcher and go directly to timer UI
6526
if (timer.IsRunning()) {
66-
SetTimerRunning();
27+
uint32_t durationMs = settingsController.GetLastTimerDuration(0);
28+
CreateTimerUI(durationMs, false);
6729
} else {
68-
SetTimerStopped();
30+
CreateLauncherUI();
6931
}
7032

7133
taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this);
7234
}
7335

7436
Timer::~Timer() {
7537
lv_task_del(taskRefresh);
38+
if (launcherMode) {
39+
lv_style_reset(&btnStyle);
40+
}
7641
lv_obj_clean(lv_scr_act());
7742
}
7843

@@ -103,6 +68,18 @@ void Timer::UpdateMask() {
10368
}
10469

10570
void Timer::Refresh() {
71+
// Don't try to update timer display if we're in launcher mode (counters don't exist)
72+
if (launcherMode) {
73+
// If timer starts while in launcher, transition to timer UI
74+
if (timer.IsRunning()) {
75+
uint32_t durationMs = settingsController.GetLastTimerDuration(0);
76+
lv_style_reset(&btnStyle);
77+
lv_obj_clean(lv_scr_act());
78+
CreateTimerUI(durationMs, false);
79+
}
80+
return;
81+
}
82+
10683
if (timer.IsRunning()) {
10784
DisplayTime();
10885
} else if (buttonPressing && xTaskGetTickCount() - pressTime > pdMS_TO_TICKS(150)) {
@@ -127,15 +104,34 @@ void Timer::DisplayTime() {
127104
}
128105

129106
void Timer::SetTimerRunning() {
107+
if (launcherMode) {
108+
return;
109+
}
130110
minuteCounter.HideControls();
131111
secondCounter.HideControls();
132112
lv_label_set_text_static(txtPlayPause, "Pause");
113+
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
133114
}
134115

135116
void Timer::SetTimerStopped() {
117+
if (launcherMode) {
118+
return;
119+
}
136120
minuteCounter.ShowControls();
137121
secondCounter.ShowControls();
138122
lv_label_set_text_static(txtPlayPause, "Start");
123+
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GREEN);
124+
}
125+
126+
void Timer::SetTimerRinging() {
127+
if (launcherMode) {
128+
// Timer expired while in launcher mode - transition will happen in Refresh()
129+
return;
130+
}
131+
minuteCounter.HideControls();
132+
secondCounter.HideControls();
133+
lv_label_set_text_static(txtPlayPause, "Reset");
134+
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED);
139135
}
140136

141137
void Timer::ToggleRunning() {
@@ -146,6 +142,11 @@ void Timer::ToggleRunning() {
146142
} else if (secondCounter.GetValue() + minuteCounter.GetValue() > 0) {
147143
auto timerDuration = std::chrono::minutes(minuteCounter.GetValue()) + std::chrono::seconds(secondCounter.GetValue());
148144
timer.StartTimer(timerDuration);
145+
146+
// Save the timer duration to MRU list
147+
uint32_t durationMs = (minuteCounter.GetValue() * 60 + secondCounter.GetValue()) * 1000;
148+
settingsController.AddTimerDuration(durationMs);
149+
149150
Refresh();
150151
SetTimerRunning();
151152
}
@@ -155,3 +156,166 @@ void Timer::Reset() {
155156
DisplayTime();
156157
SetTimerStopped();
157158
}
159+
160+
void Timer::CreateLauncherUI() {
161+
static constexpr uint8_t innerDistance = 10;
162+
static constexpr uint8_t buttonHeight = (LV_VER_RES_MAX - innerDistance) / 2;
163+
static constexpr uint8_t buttonWidth = (LV_HOR_RES_MAX - innerDistance) / 2;
164+
165+
lv_style_init(&btnStyle);
166+
lv_style_set_radius(&btnStyle, LV_STATE_DEFAULT, buttonHeight / 4);
167+
lv_style_set_bg_color(&btnStyle, LV_STATE_DEFAULT, Colors::bgAlt);
168+
169+
// Layout positions for the 3 recent timer buttons
170+
static constexpr lv_align_t buttonAlignments[numRecentTimers] = {
171+
LV_ALIGN_IN_TOP_LEFT, // Button 0: Top-left
172+
LV_ALIGN_IN_TOP_RIGHT, // Button 1: Top-right
173+
LV_ALIGN_IN_BOTTOM_LEFT // Button 2: Bottom-left
174+
};
175+
176+
// Create each of the recent timer buttons
177+
for (int i = 0; i < numRecentTimers; i++) {
178+
btnRecent[i] = lv_btn_create(lv_scr_act(), nullptr);
179+
btnRecent[i]->user_data = this;
180+
lv_obj_set_event_cb(btnRecent[i], btnEventHandler);
181+
lv_obj_add_style(btnRecent[i], LV_BTN_PART_MAIN, &btnStyle);
182+
lv_obj_set_size(btnRecent[i], buttonWidth, buttonHeight);
183+
lv_obj_align(btnRecent[i], nullptr, buttonAlignments[i], 0, 0);
184+
185+
uint32_t duration = settingsController.GetLastTimerDuration(i);
186+
uint32_t minutes = duration / 60000;
187+
uint32_t seconds = (duration % 60000) / 1000;
188+
189+
labelRecent[i] = lv_label_create(btnRecent[i], nullptr);
190+
lv_obj_t* labelIcon = lv_label_create(btnRecent[i], nullptr);
191+
192+
// Show the minutes
193+
lv_obj_set_style_local_text_font(labelRecent[i], LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
194+
lv_label_set_text_fmt(labelRecent[i], "%lu", minutes);
195+
lv_obj_align(labelRecent[i], btnRecent[i], LV_ALIGN_CENTER, 0, -20);
196+
197+
// Show the seconds, or "min" below
198+
lv_obj_set_style_local_text_font(labelIcon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
199+
if (seconds == 0) {
200+
lv_label_set_text_static(labelIcon, "min");
201+
} else {
202+
lv_label_set_text_fmt(labelIcon, ":%02lu", seconds);
203+
}
204+
lv_obj_align(labelIcon, btnRecent[i], LV_ALIGN_CENTER, 0, 20);
205+
}
206+
207+
// Bottom-right: New timer
208+
btnCustom = lv_btn_create(lv_scr_act(), nullptr);
209+
btnCustom->user_data = this;
210+
lv_obj_set_event_cb(btnCustom, btnEventHandler);
211+
lv_obj_add_style(btnCustom, LV_BTN_PART_MAIN, &btnStyle);
212+
lv_obj_set_size(btnCustom, buttonWidth, buttonHeight);
213+
lv_obj_align(btnCustom, nullptr, LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0);
214+
215+
labelCustom = lv_label_create(btnCustom, nullptr);
216+
lv_obj_set_style_local_text_font(labelCustom, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
217+
lv_label_set_text_static(labelCustom, "+");
218+
}
219+
220+
void Timer::CreateTimerUI(uint32_t startDurationMs, bool autoStart) {
221+
launcherMode = false;
222+
223+
lv_obj_t* colonLabel = lv_label_create(lv_scr_act(), nullptr);
224+
lv_obj_set_style_local_text_font(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76);
225+
lv_obj_set_style_local_text_color(colonLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
226+
lv_label_set_text_static(colonLabel, ":");
227+
lv_obj_align(colonLabel, lv_scr_act(), LV_ALIGN_CENTER, 0, -29);
228+
229+
minuteCounter.Create();
230+
secondCounter.Create();
231+
lv_obj_align(minuteCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0);
232+
lv_obj_align(secondCounter.GetObject(), nullptr, LV_ALIGN_IN_TOP_RIGHT, 0, 0);
233+
234+
highlightObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
235+
lv_obj_set_size(highlightObjectMask, 240, 50);
236+
lv_obj_align(highlightObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
237+
238+
lv_draw_mask_line_param_t tmpMaskLine;
239+
240+
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_LEFT);
241+
highlightMask = lv_objmask_add_mask(highlightObjectMask, &tmpMaskLine);
242+
243+
lv_obj_t* btnHighlight = lv_obj_create(highlightObjectMask, nullptr);
244+
lv_obj_set_style_local_radius(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
245+
lv_obj_set_style_local_bg_color(btnHighlight, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE);
246+
lv_obj_set_size(btnHighlight, LV_HOR_RES, 50);
247+
lv_obj_align(btnHighlight, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
248+
249+
btnObjectMask = lv_objmask_create(lv_scr_act(), nullptr);
250+
lv_obj_set_size(btnObjectMask, 240, 50);
251+
lv_obj_align(btnObjectMask, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, 0);
252+
253+
lv_draw_mask_line_points_init(&tmpMaskLine, 0, 0, 0, 240, LV_DRAW_MASK_LINE_SIDE_RIGHT);
254+
btnMask = lv_objmask_add_mask(btnObjectMask, &tmpMaskLine);
255+
256+
btnPlayPause = lv_btn_create(btnObjectMask, nullptr);
257+
btnPlayPause->user_data = this;
258+
lv_obj_set_style_local_radius(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_RADIUS_CIRCLE);
259+
lv_obj_set_style_local_bg_color(btnPlayPause, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
260+
lv_obj_set_event_cb(btnPlayPause, btnEventHandler);
261+
lv_obj_set_size(btnPlayPause, LV_HOR_RES, 50);
262+
263+
// Create the label as a child of the button so it stays centered by default
264+
txtPlayPause = lv_label_create(btnPlayPause, nullptr);
265+
266+
// Reset button press state
267+
buttonPressing = false;
268+
pressTime = 0;
269+
270+
if (timer.IsRunning()) {
271+
SetTimerRunning();
272+
DisplayTime();
273+
} else if (autoStart) {
274+
auto timerDuration = std::chrono::milliseconds(startDurationMs);
275+
timer.StartTimer(timerDuration);
276+
settingsController.AddTimerDuration(startDurationMs);
277+
SetTimerRunning();
278+
DisplayTime();
279+
} else {
280+
// Set the initial duration only when timer is stopped
281+
uint32_t minutes = startDurationMs / 60000;
282+
uint32_t seconds = (startDurationMs % 60000) / 1000;
283+
minuteCounter.SetValue(minutes);
284+
secondCounter.SetValue(seconds);
285+
SetTimerStopped();
286+
}
287+
}
288+
289+
void Timer::OnLauncherButtonClicked(lv_obj_t* obj) {
290+
uint32_t durationMs;
291+
bool autoStart;
292+
293+
// Check if it's one of the recent timer buttons
294+
bool found = false;
295+
for (int i = 0; i < numRecentTimers; i++) {
296+
if (obj == btnRecent[i]) {
297+
durationMs = settingsController.GetLastTimerDuration(i);
298+
autoStart = true;
299+
found = true;
300+
break;
301+
}
302+
}
303+
304+
// Check if it's the custom timer button
305+
if (!found) {
306+
if (obj == btnCustom) {
307+
durationMs = 0;
308+
autoStart = false;
309+
} else {
310+
return;
311+
}
312+
}
313+
314+
lv_style_reset(&btnStyle);
315+
lv_obj_clean(lv_scr_act());
316+
317+
CreateTimerUI(durationMs, autoStart);
318+
319+
// Wait for button release to prevent the press state from carrying over to the new UI
320+
lv_indev_wait_release(lv_indev_get_act());
321+
}

0 commit comments

Comments
 (0)