rockbox/apps/plugins/countdown_timer.c
seroteunine a1040cda5d plugins: add timer/countdown plugin
-New countdown timer plugin with pause, overtime support
-Add full name to credits and manual entry
-Make status strings translatable

Change-Id: I1437b2e5ac5ede292bdab8d36e58b81326ea2ba3
2026-04-16 21:35:52 -04:00

242 lines
8.1 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2026 Teun van Dalen
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include "plugin.h"
#include "lib/pluginlib_actions.h"
#include "lib/pluginlib_exit.h"
const struct button_mapping *plugin_contexts[] = { pla_main_ctx };
#define MAX_MINUTES 99
#define MAX_SECONDS 59
typedef enum {
STATE_SETUP,
STATE_RUNNING,
STATE_PAUSED,
STATE_FINISHED,
} timer_state_t;
typedef struct {
int set_minutes;
int set_seconds;
int current_field;
long remaining_ticks;
long last_tick;
timer_state_t state;
} timer_ctx_t;
static inline int get_button(void)
{
return pluginlib_getaction(HZ / 10, plugin_contexts,
ARRAYLEN(plugin_contexts));
}
static void draw(const timer_ctx_t *ctx)
{
char time_str[12];
const char *status_str;
int disp_min, disp_sec;
int w, h, x, y;
rb->lcd_clear_display();
if (ctx->state == STATE_SETUP) {
disp_min = ctx->set_minutes;
disp_sec = ctx->set_seconds;
if (ctx->current_field == 0)
rb->snprintf(time_str, sizeof(time_str), "[%02d]:%02d",
disp_min, disp_sec);
else
rb->snprintf(time_str, sizeof(time_str), "%02d:[%02d]",
disp_min, disp_sec);
status_str = rb->str(LANG_COUNTDOWN_TIMER_SET);
} else if (ctx->state == STATE_FINISHED) {
rb->snprintf(time_str, sizeof(time_str), "00:00");
status_str = rb->str(LANG_COUNTDOWN_TIMER_FINISHED);
} else {
if (ctx->remaining_ticks >= 0) {
int remaining_secs = (ctx->remaining_ticks + HZ - 1) / HZ;
disp_min = remaining_secs / 60;
disp_sec = remaining_secs % 60;
rb->snprintf(time_str, sizeof(time_str), "%02d:%02d",
disp_min, disp_sec);
} else {
int over_secs = ((-ctx->remaining_ticks) + HZ - 1) / HZ;
disp_min = over_secs / 60;
disp_sec = over_secs % 60;
rb->snprintf(time_str, sizeof(time_str), "-%02d:%02d",
disp_min, disp_sec);
}
if (ctx->state == STATE_RUNNING && ctx->remaining_ticks < 0)
status_str = rb->str(LANG_COUNTDOWN_TIMER_OVERTIME);
else if (ctx->state == STATE_RUNNING)
status_str = rb->str(LANG_COUNTDOWN_TIMER_RUNNING);
else
status_str = rb->str(LANG_COUNTDOWN_TIMER_PAUSED);
}
/* Draw status string above center */
rb->lcd_getstringsize(status_str, &w, &h);
x = (LCD_WIDTH - w) / 2;
y = (LCD_HEIGHT / 2) - h - 2;
rb->lcd_putsxy(x, y, status_str);
/* Draw time string at center */
rb->lcd_getstringsize(time_str, &w, &h);
x = (LCD_WIDTH - w) / 2;
y = LCD_HEIGHT / 2;
rb->lcd_putsxy(x, y, time_str);
rb->lcd_update();
}
enum plugin_status plugin_start(const void *parameter)
{
timer_ctx_t ctx = {
.set_minutes = 10,
.set_seconds = 0,
.current_field = 0,
.remaining_ticks = 60 * HZ,
.last_tick = 0,
.state = STATE_SETUP,
};
bool overtime_alerted = false;
int button;
(void)parameter;
while (true) {
if (ctx.state == STATE_RUNNING) {
long now = *rb->current_tick;
ctx.remaining_ticks -= now - ctx.last_tick;
ctx.last_tick = now;
if (ctx.remaining_ticks < 0 && !overtime_alerted) {
overtime_alerted = true;
ctx.state = STATE_FINISHED;
draw(&ctx);
rb->audio_pause();
#ifdef HAVE_BACKLIGHT
rb->backlight_on();
#endif
rb->beep_play(1000, 200, 1000);
rb->sleep(HZ / 4);
rb->beep_play(1000, 200, 1000);
rb->sleep(HZ / 4);
rb->beep_play(1000, 400, 1000);
rb->sleep(HZ + HZ / 2);
rb->audio_resume();
ctx.state = STATE_RUNNING;
/* Reset last_tick so the beep/sleep time is not counted
* against remaining_ticks on the next iteration. */
ctx.last_tick = *rb->current_tick;
}
}
draw(&ctx);
button = get_button();
switch (ctx.state) {
case STATE_SETUP: {
bool time_changed = false;
switch (button) {
#ifdef HAVE_SCROLLWHEEL
case PLA_SCROLL_FWD:
case PLA_SCROLL_FWD_REPEAT:
#endif
case PLA_UP:
case PLA_UP_REPEAT:
if (ctx.current_field == 0)
ctx.set_minutes = (ctx.set_minutes + 1) % (MAX_MINUTES + 1);
else
ctx.set_seconds = (ctx.set_seconds + 1) % (MAX_SECONDS + 1);
time_changed = true;
break;
#ifdef HAVE_SCROLLWHEEL
case PLA_SCROLL_BACK:
case PLA_SCROLL_BACK_REPEAT:
#endif
case PLA_DOWN:
case PLA_DOWN_REPEAT:
if (ctx.current_field == 0)
ctx.set_minutes = (ctx.set_minutes + MAX_MINUTES) % (MAX_MINUTES + 1);
else
ctx.set_seconds = (ctx.set_seconds + MAX_SECONDS) % (MAX_SECONDS + 1);
time_changed = true;
break;
case PLA_LEFT:
case PLA_LEFT_REPEAT:
case PLA_RIGHT:
case PLA_RIGHT_REPEAT:
ctx.current_field = (ctx.current_field + 1) % 2;
break;
case PLA_SELECT:
if (ctx.set_minutes > 0 || ctx.set_seconds > 0) {
ctx.remaining_ticks = (ctx.set_minutes * 60 + ctx.set_seconds) * HZ;
ctx.last_tick = *rb->current_tick;
ctx.state = STATE_RUNNING;
}
break;
default:
exit_on_usb(button);
break;
}
if (time_changed)
ctx.remaining_ticks = (ctx.set_minutes * 60 + ctx.set_seconds) * HZ;
break;
}
case STATE_RUNNING:
switch (button) {
case PLA_SELECT:
ctx.state = STATE_PAUSED;
break;
default:
exit_on_usb(button);
break;
}
break;
case STATE_FINISHED:
exit_on_usb(button);
break;
case STATE_PAUSED:
switch (button) {
case PLA_SELECT:
ctx.last_tick = *rb->current_tick;
ctx.state = STATE_RUNNING;
break;
case PLA_UP:
case PLA_UP_REPEAT:
case PLA_CANCEL:
case PLA_EXIT:
return PLUGIN_OK;
default:
exit_on_usb(button);
break;
}
break;
}
}
}