From e85f1201902fb852bc25a56a21a4f313f3820940 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Mon, 13 Apr 2026 15:44:53 +0200 Subject: [PATCH 1/5] playlist_viewer: add character-based Now Playing indicator Addresses issue where it became difficult to find the currently playing track once you scrolled away from it, either with icons disabled in the Playlist Viewer, or if the theme's icon set used whitespace for the Music icon. An example scenario would be when you're moving some track and are trying to put it next to the currently playing one. Change-Id: I073a7e55fb723eeff755e32a78f88dcc7db1c245 --- apps/playlist_viewer.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/playlist_viewer.c b/apps/playlist_viewer.c index 88f2590c3e..392131a8a9 100644 --- a/apps/playlist_viewer.c +++ b/apps/playlist_viewer.c @@ -515,9 +515,15 @@ static void format_line(struct playlist_entry* track, char* str, int len) { char *id3viewc = NULL; - char *skipped = ""; + char *skipped, *prefix, *suffix; + skipped = prefix = suffix = ""; if (track->attr & PLAYLIST_ATTR_SKIPPED) skipped = "(ERR) "; + if (track->index == viewer.current_playing_track) + { + prefix = "["; + suffix = "]"; + } if (!(track->attr & PLAYLIST_ATTR_RETRIEVE_ID3_ATTEMPTED) && (global_settings.playlist_viewer_track_display == PLAYLIST_VIEWER_ENTRY_SHOW_ID3_TITLE_AND_ALBUM || @@ -591,9 +597,10 @@ static void format_line(struct playlist_entry* track, char* str, format_name(name, track->name, sizeof(name)); if (global_settings.playlist_viewer_indices) /* Display playlist index */ - snprintf(str, len, "%d. %s%s", track->display_index, skipped, name); + snprintf(str, len, "%s%d. %s%s%s", + prefix, track->display_index, skipped, name, suffix); else - snprintf(str, len, "%s%s", skipped, name); + snprintf(str, len, "%s%s%s%s", prefix, skipped, name, suffix); } else { @@ -603,9 +610,10 @@ static void format_line(struct playlist_entry* track, char* str, } if (global_settings.playlist_viewer_indices) /* Display playlist index */ - snprintf(str, len, "%d. %s%s", track->display_index, skipped, id3viewc); + snprintf(str, len, "%s%d. %s%s%s", + prefix, track->display_index, skipped, id3viewc, suffix); else - snprintf(str, len, "%s%s", skipped, id3viewc); + snprintf(str, len, "%s%s%s%s", prefix, skipped, id3viewc, suffix); } } From e405858b9ee0df066c3f25f3bb4e257fadf255d8 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Tue, 7 Apr 2026 03:18:50 +0200 Subject: [PATCH 2/5] wps: Replace menu items "Open With" & "Delete" with "Show in Files" Limit these (less commonly used) "file handling" operations to the File Browser while making it quicker to reveal the file that is currently playing from the WPS. "Open With" and "Delete" remain available as WPS shortcuts. Change-Id: I91e582a45998160f07a3f7e16d475d770e8c5212 --- apps/gui/wps.c | 2 ++ apps/lang/english.lang | 28 +++++++++++++++++++ apps/onplay.c | 20 +++++++++++-- apps/onplay.h | 1 + .../browsing_and_playing.tex | 4 ++- manual/rockbox_interface/wps.tex | 11 ++------ 6 files changed, 54 insertions(+), 12 deletions(-) diff --git a/apps/gui/wps.c b/apps/gui/wps.c index 78e49ac4f8..0e52323dc2 100644 --- a/apps/gui/wps.c +++ b/apps/gui/wps.c @@ -829,6 +829,8 @@ long gui_wps_show(void) if (retval == ONPLAY_MAINMENU || !audio_status()) return GO_TO_ROOT; + else if (retval == ONPLAY_REVEAL_FILE) + return GO_TO_FILEBROWSER; else if (retval == ONPLAY_PLAYLIST) return GO_TO_PLAYLIST_VIEWER; else if (retval == ONPLAY_PLUGIN) diff --git a/apps/lang/english.lang b/apps/lang/english.lang index aeedbc2541..d0afa75c43 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -17057,3 +17057,31 @@ *: "U S B" + + id: LANG_FILE_NOT_FOUND + desc: When file does not exist + user: core + + *: "File not found" + + + *: "File not found" + + + *: "File not found" + + + + id: LANG_SHOW_IN_FILES + desc: Reveal item in File Browser + user: core + + *: "Show in Files" + + + *: "Show in Files" + + + *: "Show in Files" + + diff --git a/apps/onplay.c b/apps/onplay.c index 15b519871f..974a65298d 100644 --- a/apps/onplay.c +++ b/apps/onplay.c @@ -897,6 +897,22 @@ static bool onplay_load_plugin(void *param) return false; } +static int reveal(void) +{ + if (!file_exists(selected_file.path)) + { + splash(HZ*2, ID2P(LANG_FILE_NOT_FOUND)); + return 0; + } + + strmemccpy(global_status.browse_last_folder, selected_file.path, + sizeof global_status.browse_last_folder); + onplay_result = ONPLAY_REVEAL_FILE; + return 0; +} + +MENUITEM_FUNCTION(reveal_item, 0, ID2P(LANG_SHOW_IN_FILES), + reveal, NULL, Icon_file_view_menu); MENUITEM_FUNCTION(list_viewers_item, 0, ID2P(LANG_ONPLAY_OPEN_WITH), list_viewers, clipboard_callback, Icon_NOICON); MENUITEM_FUNCTION_W_PARAM(properties_item, 0, ID2P(LANG_PROPERTIES), @@ -1124,8 +1140,8 @@ MAKE_ONPLAYMENU( wps_onplay_menu, ID2P(LANG_ONPLAY_MENU_TITLE), #endif &bookmark_menu, &plugin_item, - &browse_id3_item, &list_viewers_item, - &delete_file_item, &view_cue_item, + &browse_id3_item, + &reveal_item, &view_cue_item, #ifdef HAVE_PITCHCONTROL &pitch_menu, #endif diff --git a/apps/onplay.h b/apps/onplay.h index 26b1247f7b..4e5e6eeb69 100644 --- a/apps/onplay.h +++ b/apps/onplay.h @@ -38,6 +38,7 @@ enum { ONPLAY_MAINMENU = -1, ONPLAY_OK = 0, ONPLAY_RELOAD_DIR, + ONPLAY_REVEAL_FILE, ONPLAY_START_PLAY, ONPLAY_PLAYLIST, ONPLAY_PLUGIN, diff --git a/manual/rockbox_interface/browsing_and_playing.tex b/manual/rockbox_interface/browsing_and_playing.tex index 45a145ad3a..c6443a9351 100644 --- a/manual/rockbox_interface/browsing_and_playing.tex +++ b/manual/rockbox_interface/browsing_and_playing.tex @@ -143,7 +143,9 @@ each option pertains both to files and directories): Deletes the currently selected file. This option applies only to files, and not to directories. Rockbox will ask for confirmation before deleting a file. Press \ActionYesNoAccept{} - to confirm deletion or any other key to cancel. + to confirm deletion or any other key to cancel. When deleting a currently + playing file, the part of the file that has already been buffered (i.e. read + into the \daps\ memory) will continue playing. This may even be the whole track. \item [Delete Directory.] Deletes the currently selected directory and all of the files and subdirectories it may contain. Deleted directories cannot be recovered. Use this feature with diff --git a/manual/rockbox_interface/wps.tex b/manual/rockbox_interface/wps.tex index 3ed0d961e6..918d62a436 100644 --- a/manual/rockbox_interface/wps.tex +++ b/manual/rockbox_interface/wps.tex @@ -254,15 +254,8 @@ from the WPS, % \setting{Context Menu}. Then select \setting{Show Track Info}. The same steps work in the Playlist Viewer as well.} -\subsubsection{Open With...} -This \setting{Open With} function is the same as the \setting{Open With} -function in the file browser's \setting{Context Menu}. - -\subsubsection{Delete} -Delete the currently playing file. The file will be deleted but the playback -of the file will not stop immediately. Instead, the part of the file that -has already been buffered (i.e. read into the \daps\ memory) will be played. -This may even be the whole track. +\subsubsection{Show in Files} +Reveal the currently playing track in the \setting{File Browser}. \opt{pitchscreen}{ \subsubsection{\label{sec:pitchscreen}Pitch} From 5ac105c837ca697427f8e71c024387957b1378ed Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Tue, 7 Apr 2026 03:31:57 +0200 Subject: [PATCH 3/5] tagtree: Add menu item "Show in Files" Quickly reveal a database item in the File Browser. In case of entire tables, the first entry will be revealed. Change-Id: I4191f27ea2ab7cacbe375719314d7eb23301fd07 --- apps/onplay.c | 14 +++++++++++--- apps/tree.c | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/onplay.c b/apps/onplay.c index 974a65298d..82b5ab8931 100644 --- a/apps/onplay.c +++ b/apps/onplay.c @@ -899,6 +899,10 @@ static bool onplay_load_plugin(void *param) static int reveal(void) { +#ifdef HAVE_TAGCACHE + if (!prepare_database_sel(NULL)) + return 0; +#endif if (!file_exists(selected_file.path)) { splash(HZ*2, ID2P(LANG_FILE_NOT_FOUND)); @@ -912,7 +916,7 @@ static int reveal(void) } MENUITEM_FUNCTION(reveal_item, 0, ID2P(LANG_SHOW_IN_FILES), - reveal, NULL, Icon_file_view_menu); + reveal, clipboard_callback, Icon_file_view_menu); MENUITEM_FUNCTION(list_viewers_item, 0, ID2P(LANG_ONPLAY_OPEN_WITH), list_viewers, clipboard_callback, Icon_NOICON); MENUITEM_FUNCTION_W_PARAM(properties_item, 0, ID2P(LANG_PROPERTIES), @@ -1049,7 +1053,8 @@ static int clipboard_callback(int action, if (selected_file.context == CONTEXT_ID3DB) { if (this_item == &track_info_item || - this_item == &pictureflow_item) + this_item == &pictureflow_item || + this_item == &reveal_item) return action; return ACTION_EXIT_MENUITEM; } @@ -1104,7 +1109,9 @@ static int clipboard_callback(int action, if (*tree_get_context()->dirfilter != SHOW_M3U) return action; } - else if (this_item == &delete_file_item) + else if (this_item == &delete_file_item || + (this_item == &reveal_item && + selected_file.context == CONTEXT_WPS)) return action; #if LCD_DEPTH > 1 else if (this_item == &set_backdrop_item) @@ -1178,6 +1185,7 @@ MAKE_ONPLAYMENU( tree_onplay_menu, ID2P(LANG_ONPLAY_MENU_TITLE), &rename_file_item, &clipboard_cut_item, &clipboard_copy_item, &clipboard_paste_item, &delete_file_item, &delete_dir_item, &list_viewers_item, &create_dir_item, &properties_item, &track_info_item, + &reveal_item, #ifdef HAVE_TAGCACHE &pictureflow_item, #endif diff --git a/apps/tree.c b/apps/tree.c index 5df204bbf2..387a070e3a 100644 --- a/apps/tree.c +++ b/apps/tree.c @@ -975,6 +975,10 @@ static int dirbrowse(void) return exit_to_new_screen(GO_TO_ROOT); break; + case ONPLAY_REVEAL_FILE: + return exit_to_new_screen(GO_TO_FILEBROWSER); + break; + case ONPLAY_OK: restore = do_restore_display; break; From cc7418dd8bc4caa16940dff9acf7e4e8bca5be31 Mon Sep 17 00:00:00 2001 From: Christian Soffke Date: Thu, 16 Apr 2026 10:52:16 +0200 Subject: [PATCH 4/5] dsp: add option to swap left and right channels Change-Id: Id4b518638436576cfb5e747548f10ece6e58eba0 --- apps/lang/english.lang | 14 +++++++++++++ apps/settings_list.c | 7 ++++--- firmware/export/audiohw.h | 1 + lib/rbcodec/dsp/channel_mode.c | 22 +++++++++++++++++++++ manual/configure_rockbox/sound_settings.tex | 3 +++ 5 files changed, 44 insertions(+), 3 deletions(-) diff --git a/apps/lang/english.lang b/apps/lang/english.lang index d0afa75c43..685dffb133 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -17085,3 +17085,17 @@ *: "Show in Files" + + id: LANG_CHANNEL_SWAP + desc: in sound_settings + user: core + + *: "Swap Left & Right" + + + *: "Swap Left & Right" + + + *: "Swap Left & Right" + + diff --git a/apps/settings_list.c b/apps/settings_list.c index 826f85825d..59fd4537a0 100644 --- a/apps/settings_list.c +++ b/apps/settings_list.c @@ -1060,11 +1060,12 @@ const struct settings_list settings[] = { /* 3-d enhancement effect */ CHOICE_SETTING(0, channel_config, LANG_CHANNEL_CONFIGURATION, 0,"channels", - "stereo,mono,custom,mono left,mono right,karaoke", - sound_set_channels, 6, + "stereo,mono,custom,mono left,mono right,karaoke,swap", + sound_set_channels, 7, ID2P(LANG_CHANNEL_STEREO), ID2P(LANG_CHANNEL_MONO), ID2P(LANG_CHANNEL_CUSTOM), ID2P(LANG_CHANNEL_LEFT), - ID2P(LANG_CHANNEL_RIGHT), ID2P(LANG_CHANNEL_KARAOKE)), + ID2P(LANG_CHANNEL_RIGHT), ID2P(LANG_CHANNEL_KARAOKE), + ID2P(LANG_CHANNEL_SWAP)), SOUND_SETTING(0, stereo_width, LANG_STEREO_WIDTH, "stereo_width", SOUND_STEREO_WIDTH), #ifdef AUDIOHW_HAVE_DEPTH_3D diff --git a/firmware/export/audiohw.h b/firmware/export/audiohw.h index 65570abb5b..ec465ca7b7 100644 --- a/firmware/export/audiohw.h +++ b/firmware/export/audiohw.h @@ -674,6 +674,7 @@ enum AUDIOHW_CHANNEL_CONFIG SOUND_CHAN_MONO_LEFT, SOUND_CHAN_MONO_RIGHT, SOUND_CHAN_KARAOKE, + SOUND_CHAN_SWAP, SOUND_CHAN_NUM_MODES, }; diff --git a/lib/rbcodec/dsp/channel_mode.c b/lib/rbcodec/dsp/channel_mode.c index 7a83236648..86dc02e3a4 100644 --- a/lib/rbcodec/dsp/channel_mode.c +++ b/lib/rbcodec/dsp/channel_mode.c @@ -43,6 +43,8 @@ void channel_mode_proc_custom(struct dsp_proc_entry *this, struct dsp_buffer **buf_p); void channel_mode_proc_karaoke(struct dsp_proc_entry *this, struct dsp_buffer **buf_p); +void channel_mode_proc_swap(struct dsp_proc_entry *this, + struct dsp_buffer **buf_p); static struct channel_mode_data { @@ -131,6 +133,25 @@ void channel_mode_proc_karaoke(struct dsp_proc_entry *this, } #endif /* CPU */ +void channel_mode_proc_swap(struct dsp_proc_entry *this, + struct dsp_buffer **buf_p) +{ + struct dsp_buffer *buf = *buf_p; + int32_t *sl = buf->p32[0]; + int32_t *sr = buf->p32[1]; + int count = buf->remcount; + + do + { + int32_t l = *sl; + *sl++ = *sr; + *sr++ = l; + } + while (--count > 0); + + (void)this; +} + void channel_mode_proc_mono_left(struct dsp_proc_entry *this, struct dsp_buffer **buf_p) { @@ -194,6 +215,7 @@ static void update_process_fn(struct dsp_proc_entry *this) [SOUND_CHAN_MONO_LEFT] = channel_mode_proc_mono_left, [SOUND_CHAN_MONO_RIGHT] = channel_mode_proc_mono_right, [SOUND_CHAN_KARAOKE] = channel_mode_proc_karaoke, + [SOUND_CHAN_SWAP] = channel_mode_proc_swap, }; this->process = fns[((struct channel_mode_data *)this->data)->mode]; diff --git a/manual/configure_rockbox/sound_settings.tex b/manual/configure_rockbox/sound_settings.tex index f9c85b6fbb..a8e94737b9 100644 --- a/manual/configure_rockbox/sound_settings.tex +++ b/manual/configure_rockbox/sound_settings.tex @@ -178,6 +178,9 @@ change to customise your listening experience. to make the singer sound centrally placed, this often (but not always) has the effect of removing the voice track from a song. This setting also very often has other undesirable effects on the sound. + \item[Swap Left \& Right.] + Plays the left channel in the right stereo channel, and the right + channel in the left stereo channel. \end{description} \section{Stereo Width} From a1040cda5db6b21651f72b1f4090edee503ab6f2 Mon Sep 17 00:00:00 2001 From: seroteunine Date: Mon, 13 Apr 2026 23:25:58 +0200 Subject: [PATCH 5/5] 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 --- apps/lang/english-us.lang | 70 +++++++++ apps/lang/english.lang | 70 +++++++++ apps/plugins/CATEGORIES | 1 + apps/plugins/SOURCES | 1 + apps/plugins/countdown_timer.c | 242 +++++++++++++++++++++++++++++ docs/CREDITS | 1 + manual/plugins/countdown_timer.tex | 51 ++++++ manual/plugins/main.tex | 2 + 8 files changed, 438 insertions(+) create mode 100644 apps/plugins/countdown_timer.c create mode 100644 manual/plugins/countdown_timer.tex diff --git a/apps/lang/english-us.lang b/apps/lang/english-us.lang index db078e77af..d1f8c108ce 100644 --- a/apps/lang/english-us.lang +++ b/apps/lang/english-us.lang @@ -16984,3 +16984,73 @@ *: "U S B" + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "SET TIMER" + + + *: "Set timer" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "RUNNING" + + + *: "Running" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "PAUSED" + + + *: "Paused" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "OVERTIME" + + + *: "Overtime" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "FINISHED" + + + *: "Finished" + + diff --git a/apps/lang/english.lang b/apps/lang/english.lang index 685dffb133..45ed8938fc 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -17099,3 +17099,73 @@ *: "Swap Left & Right" + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "SET TIMER" + + + *: "Set timer" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "RUNNING" + + + *: "Running" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "PAUSED" + + + *: "Paused" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "OVERTIME" + + + *: "Overtime" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "FINISHED" + + + *: "Finished" + + diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES index c9b1781a15..bf646a5bf9 100644 --- a/apps/plugins/CATEGORIES +++ b/apps/plugins/CATEGORIES @@ -21,6 +21,7 @@ chopper,games clix,games clock,apps codebuster,games +countdown_timer,apps credits,viewers cube,demos cue_playlist,viewers diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index 3a57e3f9e5..797601e4dd 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -7,6 +7,7 @@ db_folder_select.c tagcache/tagcache.c #endif chessclock.c +countdown_timer.c credits.c cube.c cue_playlist.c diff --git a/apps/plugins/countdown_timer.c b/apps/plugins/countdown_timer.c new file mode 100644 index 0000000000..b520047bf3 --- /dev/null +++ b/apps/plugins/countdown_timer.c @@ -0,0 +1,242 @@ +/*************************************************************************** + * __________ __ ___. + * 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; + } + } +} diff --git a/docs/CREDITS b/docs/CREDITS index 06045db966..f1affc6eb8 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -760,6 +760,7 @@ Yegor Chernyshov Javier Gutiérrez Gertrúdix Sergey Puskov Eivind Ødegård +Teun van Dalen The libmad team The wavpack team diff --git a/manual/plugins/countdown_timer.tex b/manual/plugins/countdown_timer.tex new file mode 100644 index 0000000000..c022203a92 --- /dev/null +++ b/manual/plugins/countdown_timer.tex @@ -0,0 +1,51 @@ +% $Id$ % +\subsection{Countdown_timer} + +A countdown timer. Set the desired duration, start the countdown, and the +\dap{} will alert you with a beep sequence when time is up. The timer +defaults to 10 minutes on launch. + +\subsubsection{Setting the timer} + +When the plugin starts, the display shows the time to count down from with +the active field highlighted in brackets (e.g.\ \texttt{[10]:00}). + +\begin{btnmap} + \opt{scrollwheel}{\PluginScrollFwd{} / \PluginScrollBack{} or } + \PluginUp{} / \PluginDown + \opt{HAVEREMOTEKEYMAP}{& \PluginRCUp{} / \PluginRCDown} + & Increase / decrease the selected field \\ + + \PluginLeft{} / \PluginRight + \opt{HAVEREMOTEKEYMAP}{& \PluginRCLeft{} / \PluginRCRight} + & Switch between the minutes and seconds fields \\ + + \PluginSelect + \opt{HAVEREMOTEKEYMAP}{& \PluginRCSelect} + & Start the countdown \\ +\end{btnmap} + +Minutes can be set from 0 to 99 and seconds from 0 to 59. The timer cannot +be started if both fields are zero. Values wrap around when incremented or +decremented past their limits. + +\subsubsection{Running the timer} + +Once started, the remaining time counts down in \texttt{MM:SS} format. + +\begin{btnmap} + \PluginSelect + \opt{HAVEREMOTEKEYMAP}{& \PluginRCSelect} + & Pause / resume the timer \\ + + \nopt{IPOD_4G_PAD,IPOD_3G_PAD}{\PluginCancel} + \opt{IPOD_4G_PAD,IPOD_3G_PAD}{\ButtonMenu} + \opt{HAVEREMOTEKEYMAP}{& \PluginRCCancel} + & Quit (only available while paused) \\ +\end{btnmap} + +When the countdown reaches zero the \dap{} pauses playback, turns on the +backlight, and plays three beeps. After the alert the timer continues +running into overtime, displaying the elapsed overtime as +\texttt{-MM:SS}. Playback resumes automatically after the beep sequence. +The timer can be quit by pausing it and pressing the quit button. diff --git a/manual/plugins/main.tex b/manual/plugins/main.tex index d6601b8a5b..8ca4b86e6b 100644 --- a/manual/plugins/main.tex +++ b/manual/plugins/main.tex @@ -297,3 +297,5 @@ option from the \setting{Context Menu} (see \reference{ref:Contextmenu}).} \input{plugins/stopwatch.tex} \input{plugins/text_editor.tex} + +\input{plugins/countdown_timer.tex}