summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-12 10:42:17 +0300
committerPaul Buetow <paul@buetow.org>2026-04-12 10:42:17 +0300
commit1d75f00bfefae8633dc5ebd9b4a0ec67ac7abb7b (patch)
tree7373130630c4db5e7e7eb5b558c44afedc45db0d
parent6b8d82a7e32c631d8ec56b3d332c3c675b01a38c (diff)
e2: implement SimpleMenuLayer main menu shell
-rw-r--r--src/c/fastforge.c848
1 files changed, 630 insertions, 218 deletions
diff --git a/src/c/fastforge.c b/src/c/fastforge.c
index 87656d2..05bf97f 100644
--- a/src/c/fastforge.c
+++ b/src/c/fastforge.c
@@ -1,6 +1,29 @@
#include <pebble.h>
+#include <string.h>
#include "fastforge.h"
+enum {
+ MAIN_MENU_INDEX_START_NEW = 0,
+ MAIN_MENU_INDEX_CURRENT_TIMER = 1,
+ MAIN_MENU_INDEX_STOP_CURRENT = 2,
+ MAIN_MENU_INDEX_HISTORY = 3,
+ MAIN_MENU_INDEX_STATS = 4,
+ MAIN_MENU_INDEX_SCIENCE = 5,
+ MAIN_MENU_INDEX_SETTINGS = 6,
+ MAIN_MENU_INDEX_BACKUP = 7,
+ MAIN_MENU_ITEM_COUNT = 8
+};
+
+enum {
+ PRESET_MENU_INDEX_DEFAULT = 0,
+ PRESET_MENU_INDEX_16H = 1,
+ PRESET_MENU_INDEX_18H = 2,
+ PRESET_MENU_INDEX_20H = 3,
+ PRESET_MENU_INDEX_24H = 4,
+ PRESET_MENU_INDEX_36H = 5,
+ PRESET_MENU_ITEM_COUNT = 6
+};
+
FastEntry history[MAX_FASTS];
int history_count = 0;
FastEntry current_fast = {0};
@@ -9,25 +32,46 @@ StreakData streak_data = {0};
AppTimer *alarm_timer = NULL;
time_t target_time = 0;
-static Window *s_main_window;
+static Window *s_menu_window;
+static Window *s_timer_window;
static Window *s_goal_window;
-static Layer *s_goal_background_layer;
-static Layer *s_progress_layer;
+static Window *s_presets_window;
+static Window *s_detail_window;
+
+static SimpleMenuLayer *s_main_menu_layer;
+static SimpleMenuLayer *s_presets_menu_layer;
+static SimpleMenuSection s_main_menu_sections[1];
+static SimpleMenuSection s_presets_menu_sections[1];
+static SimpleMenuItem s_main_menu_items[MAIN_MENU_ITEM_COUNT];
+static SimpleMenuItem s_presets_menu_items[PRESET_MENU_ITEM_COUNT];
+
static TextLayer *s_title_layer;
static TextLayer *s_timer_layer;
static TextLayer *s_detail_layer;
static TextLayer *s_stage_layer;
static TextLayer *s_hint_layer;
+static Layer *s_progress_layer;
+
+static Layer *s_goal_background_layer;
static TextLayer *s_goal_title_layer;
static TextLayer *s_goal_time_layer;
static TextLayer *s_goal_stage_layer;
static TextLayer *s_goal_hint_layer;
+
+static TextLayer *s_placeholder_title_layer;
+static TextLayer *s_placeholder_body_layer;
+static TextLayer *s_placeholder_hint_layer;
+
static char s_title_text[24];
static char s_timer_text[16];
static char s_detail_text[48];
static char s_stage_text[32];
static char s_goal_time_text[24];
static char s_goal_stage_text[24];
+static char s_menu_stop_subtitle[32];
+static char s_placeholder_title_text[24];
+static char s_placeholder_body_text[160];
+static char s_placeholder_hint_text[24];
static const uint32_t s_alarm_vibe[] = {200, 100, 200, 100, 400, 100, 200};
static VibePattern s_alarm_pattern = {
.durations = s_alarm_vibe,
@@ -36,116 +80,17 @@ static VibePattern s_alarm_pattern = {
static void refresh_timer_view(void);
static void refresh_goal_window_content(void);
+static void sync_main_menu_state(void);
-static int progress_width_for_elapsed(time_t elapsed_seconds, uint32_t total_seconds, int width) {
- if (width <= 0 || total_seconds == 0) {
- return 0;
- }
- if (elapsed_seconds <= 0) {
- return 0;
- }
- if ((uint32_t)elapsed_seconds >= total_seconds) {
- return width;
- }
- return (int)((elapsed_seconds * width) / (time_t)total_seconds);
-}
-
-static int tick_x_for_seconds(uint32_t total_seconds, uint32_t tick_seconds, int width) {
- if (width <= 0 || total_seconds == 0 || tick_seconds > total_seconds) {
- return -1;
- }
- int tick_x = (int)((tick_seconds * (uint32_t)width) / total_seconds);
- if (tick_x >= width) {
- tick_x = width - 1;
- }
- return tick_x;
-}
-
-static void timer_progress_update_proc(Layer *layer, GContext *ctx) {
- GRect bounds = layer_get_bounds(layer);
- graphics_context_set_fill_color(ctx, GColorLightGray);
- graphics_fill_rect(ctx, bounds, 2, GCornersAll);
-
- if (!fast_is_running()) {
- return;
- }
-
- time_t elapsed = time(NULL) - current_fast.start_time;
- if (elapsed < 0) {
- elapsed = 0;
- }
-
- uint32_t total_seconds = (uint32_t)current_fast.target_minutes * 60;
- int fill_width = progress_width_for_elapsed(elapsed, total_seconds, bounds.size.w);
- if (fill_width > 0) {
- graphics_context_set_fill_color(ctx, GColorGreen);
- graphics_fill_rect(ctx, GRect(0, 0, fill_width, bounds.size.h), 2, GCornersAll);
- }
-
- graphics_context_set_stroke_color(ctx, GColorBlack);
- int tick_12h_x = tick_x_for_seconds(total_seconds, 12 * 3600, bounds.size.w);
- int tick_18h_x = tick_x_for_seconds(total_seconds, 18 * 3600, bounds.size.w);
- int tick_24h_x = tick_x_for_seconds(total_seconds, 24 * 3600, bounds.size.w);
- if (tick_12h_x >= 0) {
- graphics_draw_line(ctx, GPoint(tick_12h_x, 0), GPoint(tick_12h_x, bounds.size.h - 1));
- }
- if (tick_18h_x >= 0) {
- graphics_draw_line(ctx, GPoint(tick_18h_x, 0), GPoint(tick_18h_x, bounds.size.h - 1));
- }
- if (tick_24h_x >= 0) {
- graphics_draw_line(ctx, GPoint(tick_24h_x, 0), GPoint(tick_24h_x, bounds.size.h - 1));
- }
-}
-
-static void goal_background_update_proc(Layer *layer, GContext *ctx) {
- graphics_context_set_fill_color(ctx, GColorBlack);
- graphics_fill_rect(ctx, layer_get_bounds(layer), 0, GCornerNone);
-}
-
-static void show_goal_reached_window(void) {
- if (!s_goal_window) {
- return;
- }
- refresh_goal_window_content();
- if (!window_stack_contains_window(s_goal_window)) {
- window_stack_push(s_goal_window, true);
- }
-}
-
-static void alarm_callback(void *data) {
- (void)data;
- alarm_timer = NULL;
- target_time = 0;
- vibes_enqueue_custom_pattern(s_alarm_pattern);
- light_enable_interaction();
- show_goal_reached_window();
- refresh_timer_view();
-}
-
-static void schedule_alarm_if_needed(void) {
- if (alarm_timer) {
- app_timer_cancel(alarm_timer);
- alarm_timer = NULL;
- }
-
- if (!fast_is_running() || current_fast.target_minutes == 0) {
- target_time = 0;
- return;
+static bool safe_push_window(Window *window, bool animated) {
+ if (!window) {
+ return false;
}
-
- target_time = current_fast.start_time + (time_t)current_fast.target_minutes * 60;
- time_t now = time(NULL);
- if (target_time <= now) {
- alarm_callback(NULL);
- return;
+ if (window_stack_contains_window(window)) {
+ return false;
}
-
- uint32_t ms_until_alarm = (uint32_t)(target_time - now) * 1000;
- alarm_timer = app_timer_register(ms_until_alarm, alarm_callback, NULL);
-}
-
-bool fast_is_running(void) {
- return current_fast.start_time != 0;
+ window_stack_push(window, animated);
+ return true;
}
static int clamp_history_count(int value) {
@@ -220,14 +165,18 @@ void load_all_data(void) {
normalize_loaded_data();
}
+bool fast_is_running(void) {
+ return current_fast.start_time != 0;
+}
+
static void format_hhmmss(time_t seconds, char *buffer, size_t size) {
if (seconds < 0) {
seconds = 0;
}
- int hours = (int)(seconds / 3600);
- int minutes = (int)((seconds % 3600) / 60);
- int secs = (int)(seconds % 60);
- snprintf(buffer, size, "%02d:%02d:%02d", hours, minutes, secs);
+ snprintf(buffer, size, "%02d:%02d:%02d",
+ (int)(seconds / 3600),
+ (int)((seconds % 3600) / 60),
+ (int)(seconds % 60));
}
static uint8_t stage_level_for_elapsed(time_t elapsed_seconds) {
@@ -268,43 +217,196 @@ static void update_max_stage_if_needed(time_t elapsed_seconds) {
}
}
-static void refresh_goal_window_content(void) {
- if (!s_goal_time_layer || !s_goal_stage_layer) {
+static void append_history_entry(const FastEntry *entry) {
+ if (!entry) {
return;
}
- time_t elapsed = 0;
+ if (history_count < MAX_FASTS) {
+ history[history_count++] = *entry;
+ return;
+ }
+
+ memmove(&history[0], &history[1], sizeof(FastEntry) * (MAX_FASTS - 1));
+ history[MAX_FASTS - 1] = *entry;
+}
+
+static void mark_fast_completed(time_t end_time) {
+ streak_data.current_streak++;
+ if (streak_data.current_streak > streak_data.longest_streak) {
+ streak_data.longest_streak = streak_data.current_streak;
+ }
+ streak_data.last_completed_fast_end = end_time;
+}
+
+static uint16_t resolve_target_minutes(uint16_t preset_target_minutes) {
+ if (preset_target_minutes > 0) {
+ global_target_minutes = preset_target_minutes;
+ }
+ if (global_target_minutes == 0) {
+ global_target_minutes = DEFAULT_TARGET_MINUTES;
+ }
+ return global_target_minutes;
+}
+
+static void alarm_callback(void *data);
+
+static void schedule_alarm_if_needed(void) {
+ if (alarm_timer) {
+ app_timer_cancel(alarm_timer);
+ alarm_timer = NULL;
+ }
+
+ if (!fast_is_running() || current_fast.target_minutes == 0) {
+ target_time = 0;
+ return;
+ }
+
+ target_time = current_fast.start_time + (time_t)current_fast.target_minutes * 60;
+ time_t now = time(NULL);
+ if (target_time <= now) {
+ alarm_callback(NULL);
+ return;
+ }
+
+ uint32_t ms_until_alarm = (uint32_t)(target_time - now) * 1000;
+ alarm_timer = app_timer_register(ms_until_alarm, alarm_callback, NULL);
+}
+
+bool fast_start(uint16_t preset_target_minutes) {
if (fast_is_running()) {
- elapsed = time(NULL) - current_fast.start_time;
- if (elapsed < 0) {
- elapsed = 0;
- }
- update_max_stage_if_needed(elapsed);
+ return false;
}
- char elapsed_text[16];
- format_hhmmss(elapsed, elapsed_text, sizeof(elapsed_text));
- snprintf(s_goal_time_text, sizeof(s_goal_time_text), "Elapsed %s", elapsed_text);
- snprintf(s_goal_stage_text, sizeof(s_goal_stage_text), "Stage: %s", stage_text_for_elapsed(elapsed));
+ current_fast.start_time = time(NULL);
+ current_fast.end_time = 0;
+ current_fast.target_minutes = resolve_target_minutes(preset_target_minutes);
+ memset(current_fast.note, 0, sizeof(current_fast.note));
+ current_fast.max_stage_reached = 0;
+ save_all_data();
+ schedule_alarm_if_needed();
+ APP_LOG(APP_LOG_LEVEL_INFO, "Started fast at %ld target=%u",
+ (long)current_fast.start_time, current_fast.target_minutes);
+ return true;
+}
- text_layer_set_text(s_goal_time_layer, s_goal_time_text);
- text_layer_set_text(s_goal_stage_layer, s_goal_stage_text);
+bool fast_stop(void) {
+ if (!fast_is_running()) {
+ return false;
+ }
+
+ FastEntry completed = current_fast;
+ completed.end_time = time(NULL);
+ if (completed.end_time < completed.start_time) {
+ completed.end_time = completed.start_time;
+ }
+ append_history_entry(&completed);
+ mark_fast_completed(completed.end_time);
+ memset(&current_fast, 0, sizeof(current_fast));
+ if (alarm_timer) {
+ app_timer_cancel(alarm_timer);
+ alarm_timer = NULL;
+ }
+ target_time = 0;
+ save_all_data();
+ APP_LOG(APP_LOG_LEVEL_INFO, "Stopped fast and saved history_count=%d", history_count);
+ return true;
+}
+
+static void set_placeholder_content(const char *title, const char *body, const char *hint) {
+ snprintf(s_placeholder_title_text, sizeof(s_placeholder_title_text), "%s", title ? title : "");
+ snprintf(s_placeholder_body_text, sizeof(s_placeholder_body_text), "%s", body ? body : "");
+ snprintf(s_placeholder_hint_text, sizeof(s_placeholder_hint_text), "%s", hint ? hint : "BACK Menu");
+ if (s_placeholder_title_layer) {
+ text_layer_set_text(s_placeholder_title_layer, s_placeholder_title_text);
+ }
+ if (s_placeholder_body_layer) {
+ text_layer_set_text(s_placeholder_body_layer, s_placeholder_body_text);
+ }
+ if (s_placeholder_hint_layer) {
+ text_layer_set_text(s_placeholder_hint_layer, s_placeholder_hint_text);
+ }
+}
+
+static void show_placeholder_window(const char *title, const char *body, const char *hint) {
+ set_placeholder_content(title, body, hint);
+ if (!window_stack_contains_window(s_detail_window)) {
+ window_stack_push(s_detail_window, true);
+ }
+}
+
+static int progress_width_for_elapsed(time_t elapsed_seconds, uint32_t total_seconds, int width) {
+ if (width <= 0 || total_seconds == 0 || elapsed_seconds <= 0) {
+ return 0;
+ }
+ if ((uint32_t)elapsed_seconds >= total_seconds) {
+ return width;
+ }
+ return (int)((elapsed_seconds * width) / (time_t)total_seconds);
+}
+
+static int tick_x_for_seconds(uint32_t total_seconds, uint32_t tick_seconds, int width) {
+ if (width <= 0 || total_seconds == 0 || tick_seconds > total_seconds) {
+ return -1;
+ }
+ int tick_x = (int)((tick_seconds * (uint32_t)width) / total_seconds);
+ if (tick_x >= width) {
+ return width - 1;
+ }
+ return tick_x;
+}
+
+static void timer_progress_update_proc(Layer *layer, GContext *ctx) {
+ GRect bounds = layer_get_bounds(layer);
+ graphics_context_set_fill_color(ctx, GColorLightGray);
+ graphics_fill_rect(ctx, bounds, 2, GCornersAll);
+ if (!fast_is_running()) {
+ return;
+ }
+
+ time_t elapsed = time(NULL) - current_fast.start_time;
+ if (elapsed < 0) {
+ elapsed = 0;
+ }
+
+ uint32_t total_seconds = (uint32_t)current_fast.target_minutes * 60;
+ int fill_width = progress_width_for_elapsed(elapsed, total_seconds, bounds.size.w);
+ if (fill_width > 0) {
+ graphics_context_set_fill_color(ctx, GColorGreen);
+ graphics_fill_rect(ctx, GRect(0, 0, fill_width, bounds.size.h), 2, GCornersAll);
+ }
+
+ graphics_context_set_stroke_color(ctx, GColorBlack);
+ int tick_12h_x = tick_x_for_seconds(total_seconds, 12 * 3600, bounds.size.w);
+ int tick_18h_x = tick_x_for_seconds(total_seconds, 18 * 3600, bounds.size.w);
+ int tick_24h_x = tick_x_for_seconds(total_seconds, 24 * 3600, bounds.size.w);
+ if (tick_12h_x >= 0) {
+ graphics_draw_line(ctx, GPoint(tick_12h_x, 0), GPoint(tick_12h_x, bounds.size.h - 1));
+ }
+ if (tick_18h_x >= 0) {
+ graphics_draw_line(ctx, GPoint(tick_18h_x, 0), GPoint(tick_18h_x, bounds.size.h - 1));
+ }
+ if (tick_24h_x >= 0) {
+ graphics_draw_line(ctx, GPoint(tick_24h_x, 0), GPoint(tick_24h_x, bounds.size.h - 1));
+ }
}
static void refresh_timer_view(void) {
+ if (!s_title_layer || !s_timer_layer || !s_detail_layer || !s_stage_layer || !s_hint_layer) {
+ return;
+ }
+
if (!fast_is_running()) {
snprintf(s_title_text, sizeof(s_title_text), "NO FAST RUNNING");
format_hhmmss(0, s_timer_text, sizeof(s_timer_text));
snprintf(s_detail_text, sizeof(s_detail_text), "Target: %um", global_target_minutes);
snprintf(s_stage_text, sizeof(s_stage_text), "Stage: --");
- text_layer_set_text(s_hint_layer, "SELECT Start DOWN Quit");
+ text_layer_set_text(s_hint_layer, "SELECT Start BACK Menu");
} else {
- time_t now = time(NULL);
- time_t elapsed = now - current_fast.start_time;
+ time_t elapsed = time(NULL) - current_fast.start_time;
if (elapsed < 0) {
elapsed = 0;
}
-
update_max_stage_if_needed(elapsed);
uint32_t target_seconds = current_fast.target_minutes * 60;
if (target_seconds > 0) {
@@ -324,9 +426,8 @@ static void refresh_timer_view(void) {
format_hhmmss(elapsed, s_timer_text, sizeof(s_timer_text));
snprintf(s_detail_text, sizeof(s_detail_text), "No target set");
}
-
snprintf(s_stage_text, sizeof(s_stage_text), "Stage: %s", stage_text_for_elapsed(elapsed));
- text_layer_set_text(s_hint_layer, "SELECT Stop DOWN Quit");
+ text_layer_set_text(s_hint_layer, "SELECT Stop BACK Menu");
}
text_layer_set_text(s_title_layer, s_title_text);
@@ -338,100 +439,202 @@ static void refresh_timer_view(void) {
}
}
-static void append_history_entry(const FastEntry *entry) {
- if (!entry) {
+static void refresh_goal_window_content(void) {
+ if (!s_goal_time_layer || !s_goal_stage_layer) {
return;
}
- if (history_count < MAX_FASTS) {
- history[history_count++] = *entry;
- return;
+ time_t elapsed = 0;
+ if (fast_is_running()) {
+ elapsed = time(NULL) - current_fast.start_time;
+ if (elapsed < 0) {
+ elapsed = 0;
+ }
+ update_max_stage_if_needed(elapsed);
}
- memmove(&history[0], &history[1], sizeof(FastEntry) * (MAX_FASTS - 1));
- history[MAX_FASTS - 1] = *entry;
+ char elapsed_text[16];
+ format_hhmmss(elapsed, elapsed_text, sizeof(elapsed_text));
+ snprintf(s_goal_time_text, sizeof(s_goal_time_text), "Elapsed %s", elapsed_text);
+ snprintf(s_goal_stage_text, sizeof(s_goal_stage_text), "Stage: %s", stage_text_for_elapsed(elapsed));
+ text_layer_set_text(s_goal_time_layer, s_goal_time_text);
+ text_layer_set_text(s_goal_stage_layer, s_goal_stage_text);
}
-static void mark_fast_completed(time_t end_time) {
- streak_data.current_streak++;
- if (streak_data.current_streak > streak_data.longest_streak) {
- streak_data.longest_streak = streak_data.current_streak;
+static void sync_main_menu_state(void) {
+ if (fast_is_running()) {
+ snprintf(s_menu_stop_subtitle, sizeof(s_menu_stop_subtitle), "End now and save");
+ } else {
+ snprintf(s_menu_stop_subtitle, sizeof(s_menu_stop_subtitle), "No fast running");
}
- streak_data.last_completed_fast_end = end_time;
-}
+ s_main_menu_items[MAIN_MENU_INDEX_STOP_CURRENT].subtitle = s_menu_stop_subtitle;
-static uint16_t resolve_target_minutes(uint16_t preset_target_minutes) {
- if (preset_target_minutes > 0) {
- global_target_minutes = preset_target_minutes;
+ if (s_main_menu_layer) {
+ menu_layer_reload_data(simple_menu_layer_get_menu_layer(s_main_menu_layer));
}
- if (global_target_minutes == 0) {
- global_target_minutes = DEFAULT_TARGET_MINUTES;
- }
- return global_target_minutes;
}
-bool fast_start(uint16_t preset_target_minutes) {
- if (fast_is_running()) {
- return false;
- }
+static void refresh_all_ui_state(void) {
+ refresh_timer_view();
+ refresh_goal_window_content();
+ sync_main_menu_state();
+}
- current_fast.start_time = time(NULL);
- current_fast.end_time = 0;
- current_fast.target_minutes = resolve_target_minutes(preset_target_minutes);
- memset(current_fast.note, 0, sizeof(current_fast.note));
- current_fast.max_stage_reached = 0;
- save_all_data();
- schedule_alarm_if_needed();
+static void show_goal_reached_window(void) {
+ refresh_goal_window_content();
+ safe_push_window(s_goal_window, true);
+}
- APP_LOG(APP_LOG_LEVEL_INFO, "Started fast at %ld (target=%u)",
- (long)current_fast.start_time, current_fast.target_minutes);
- return true;
+static void alarm_callback(void *data) {
+ (void)data;
+ alarm_timer = NULL;
+ target_time = 0;
+ vibes_enqueue_custom_pattern(s_alarm_pattern);
+ light_enable_interaction();
+ show_goal_reached_window();
+ refresh_all_ui_state();
}
-bool fast_stop(void) {
- if (!fast_is_running()) {
- return false;
+static void start_fast_from_preset(uint16_t target_minutes, const char *label) {
+ (void)label;
+ if (!fast_start(target_minutes)) {
+ show_placeholder_window("FAST RUNNING",
+ "Stop the current fast before starting a new one.",
+ "BACK Menu");
+ return;
}
- FastEntry completed = current_fast;
- completed.end_time = time(NULL);
- if (completed.end_time < completed.start_time) {
- completed.end_time = completed.start_time;
+ if (window_stack_contains_window(s_presets_window)) {
+ window_stack_remove(s_presets_window, false);
}
- append_history_entry(&completed);
- mark_fast_completed(completed.end_time);
- memset(&current_fast, 0, sizeof(current_fast));
- if (alarm_timer) {
- app_timer_cancel(alarm_timer);
- alarm_timer = NULL;
+ safe_push_window(s_timer_window, true);
+ refresh_all_ui_state();
+}
+
+static void menu_start_new_fast_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ safe_push_window(s_presets_window, true);
+}
+
+static void menu_current_timer_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ safe_push_window(s_timer_window, true);
+}
+
+static void menu_stop_current_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ if (!fast_stop()) {
+ show_placeholder_window("NOT RUNNING", "There is no active fast to stop.", "BACK Menu");
+ return;
}
- target_time = 0;
- save_all_data();
+ show_placeholder_window("FAST STOPPED", "Current fast saved to history.", "BACK Menu");
+ refresh_all_ui_state();
+}
- APP_LOG(APP_LOG_LEVEL_INFO, "Stopped fast and saved to history (count=%d)", history_count);
- return true;
+static void menu_history_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ char message[80];
+ snprintf(message, sizeof(message), "History screen pending.\nSaved fasts: %d", history_count);
+ show_placeholder_window("HISTORY", message, "BACK Menu");
}
-// ==================== BUTTON HANDLERS ====================
+static void menu_statistics_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ char message[100];
+ snprintf(message, sizeof(message),
+ "Stats screen pending.\nStreak: %u Longest: %u",
+ streak_data.current_streak, streak_data.longest_streak);
+ show_placeholder_window("STATISTICS", message, "BACK Menu");
+}
-static void select_click_handler(ClickRecognizerRef recognizer, void *context) {
- (void)recognizer;
+static void menu_science_callback(int index, void *context) {
+ (void)index;
(void)context;
+ show_placeholder_window("FASTING SCIENCE",
+ "Screen pending.\nTimeline + protocol comparison will live here.",
+ "BACK Menu");
+}
+
+static void menu_settings_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ char message[96];
+ snprintf(message, sizeof(message),
+ "Settings pending.\nDefault target is %u minutes.",
+ global_target_minutes);
+ show_placeholder_window("SETTINGS", message, "BACK Menu");
+}
+static void menu_backup_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ show_placeholder_window("BACKUP TO PHONE",
+ "Backup flow pending.\nAppMessage export entry point is ready.",
+ "BACK Menu");
+}
+
+static void preset_default_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ start_fast_from_preset(global_target_minutes, "STARTED");
+}
+
+static void preset_16h_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ start_fast_from_preset(16 * 60, "16H STARTED");
+}
+
+static void preset_18h_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ start_fast_from_preset(18 * 60, "18H STARTED");
+}
+
+static void preset_20h_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ start_fast_from_preset(20 * 60, "20H STARTED");
+}
+
+static void preset_24h_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ start_fast_from_preset(24 * 60, "24H STARTED");
+}
+
+static void preset_36h_callback(int index, void *context) {
+ (void)index;
+ (void)context;
+ start_fast_from_preset(36 * 60, "36H STARTED");
+}
+
+static void timer_select_click_handler(ClickRecognizerRef recognizer, void *context) {
+ (void)recognizer;
+ (void)context;
if (fast_is_running()) {
fast_stop();
} else {
fast_start(0);
}
- refresh_timer_view();
+ refresh_all_ui_state();
}
-static void down_click_handler(ClickRecognizerRef recognizer, void *context) {
+static void timer_down_click_handler(ClickRecognizerRef recognizer, void *context) {
(void)recognizer;
(void)context;
+ window_stack_remove(s_timer_window, true);
+}
- APP_LOG(APP_LOG_LEVEL_INFO, "DOWN pressed - Quitting app...");
- window_stack_pop_all(false); // Cleanly quit the app
+static void timer_click_config_provider(void *context) {
+ (void)context;
+ window_single_click_subscribe(BUTTON_ID_SELECT, timer_select_click_handler);
+ window_single_click_subscribe(BUTTON_ID_DOWN, timer_down_click_handler);
}
static void goal_window_stop_handler(ClickRecognizerRef recognizer, void *context) {
@@ -439,21 +642,14 @@ static void goal_window_stop_handler(ClickRecognizerRef recognizer, void *contex
(void)context;
fast_stop();
window_stack_remove(s_goal_window, true);
- refresh_timer_view();
+ refresh_all_ui_state();
}
static void goal_window_continue_handler(ClickRecognizerRef recognizer, void *context) {
(void)recognizer;
(void)context;
window_stack_remove(s_goal_window, true);
- refresh_timer_view();
-}
-
-// ==================== CLICK CONFIG ====================
-
-static void click_config_provider(void *context) {
- window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);
- window_single_click_subscribe(BUTTON_ID_DOWN, down_click_handler);
+ refresh_all_ui_state();
}
static void goal_click_config_provider(void *context) {
@@ -464,15 +660,47 @@ static void goal_click_config_provider(void *context) {
window_single_click_subscribe(BUTTON_ID_UP, goal_window_continue_handler);
}
+static void placeholder_dismiss_handler(ClickRecognizerRef recognizer, void *context) {
+ (void)recognizer;
+ (void)context;
+ window_stack_remove(s_detail_window, true);
+}
+
+static void placeholder_click_config_provider(void *context) {
+ (void)context;
+ window_single_click_subscribe(BUTTON_ID_SELECT, placeholder_dismiss_handler);
+ window_single_click_subscribe(BUTTON_ID_DOWN, placeholder_dismiss_handler);
+ window_single_click_subscribe(BUTTON_ID_BACK, placeholder_dismiss_handler);
+ window_single_click_subscribe(BUTTON_ID_UP, placeholder_dismiss_handler);
+}
+
static void tick_handler(struct tm *tick_time, TimeUnits units_changed) {
(void)tick_time;
(void)units_changed;
refresh_timer_view();
}
-// ==================== WINDOW LOAD / UNLOAD ====================
+static void goal_background_update_proc(Layer *layer, GContext *ctx) {
+ graphics_context_set_fill_color(ctx, GColorBlack);
+ graphics_fill_rect(ctx, layer_get_bounds(layer), 0, GCornerNone);
+}
+
+static void menu_window_load(Window *window) {
+ Layer *window_layer = window_get_root_layer(window);
+ GRect bounds = layer_get_bounds(window_layer);
+ s_main_menu_layer = simple_menu_layer_create(bounds, window, s_main_menu_sections, 1, NULL);
+ layer_add_child(window_layer, simple_menu_layer_get_layer(s_main_menu_layer));
+ sync_main_menu_state();
+}
+
+static void menu_window_unload(Window *window) {
+ (void)window;
+ simple_menu_layer_destroy(s_main_menu_layer);
+ s_main_menu_layer = NULL;
+ save_all_data();
+}
-static void window_load(Window *window) {
+static void timer_window_load(Window *window) {
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
@@ -515,20 +743,24 @@ static void window_load(Window *window) {
layer_add_child(window_layer, s_progress_layer);
layer_add_child(window_layer, text_layer_get_layer(s_stage_layer));
layer_add_child(window_layer, text_layer_get_layer(s_hint_layer));
-
refresh_timer_view();
}
-static void window_unload(Window *window) {
+static void timer_window_unload(Window *window) {
(void)window;
save_all_data();
layer_destroy(s_progress_layer);
s_progress_layer = NULL;
text_layer_destroy(s_title_layer);
+ s_title_layer = NULL;
text_layer_destroy(s_timer_layer);
+ s_timer_layer = NULL;
text_layer_destroy(s_detail_layer);
+ s_detail_layer = NULL;
text_layer_destroy(s_stage_layer);
+ s_stage_layer = NULL;
text_layer_destroy(s_hint_layer);
+ s_hint_layer = NULL;
}
static void goal_window_load(Window *window) {
@@ -577,35 +809,216 @@ static void goal_window_load(Window *window) {
static void goal_window_unload(Window *window) {
(void)window;
layer_destroy(s_goal_background_layer);
+ s_goal_background_layer = NULL;
text_layer_destroy(s_goal_title_layer);
+ s_goal_title_layer = NULL;
text_layer_destroy(s_goal_time_layer);
+ s_goal_time_layer = NULL;
text_layer_destroy(s_goal_stage_layer);
+ s_goal_stage_layer = NULL;
text_layer_destroy(s_goal_hint_layer);
+ s_goal_hint_layer = NULL;
}
-// ==================== APP INIT / DEINIT ====================
+static void presets_window_load(Window *window) {
+ Layer *window_layer = window_get_root_layer(window);
+ GRect bounds = layer_get_bounds(window_layer);
+ s_presets_menu_layer = simple_menu_layer_create(bounds, window, s_presets_menu_sections, 1, NULL);
+ layer_add_child(window_layer, simple_menu_layer_get_layer(s_presets_menu_layer));
+}
-static void init(void) {
- load_all_data();
- tick_timer_service_subscribe(SECOND_UNIT, tick_handler);
- s_main_window = window_create();
- window_set_click_config_provider(s_main_window, click_config_provider);
- window_set_window_handlers(s_main_window, (WindowHandlers) {
- .load = window_load,
- .unload = window_unload,
+static void presets_window_unload(Window *window) {
+ (void)window;
+ simple_menu_layer_destroy(s_presets_menu_layer);
+ s_presets_menu_layer = NULL;
+}
+
+static void detail_window_load(Window *window) {
+ Layer *window_layer = window_get_root_layer(window);
+ GRect bounds = layer_get_bounds(window_layer);
+
+ s_placeholder_title_layer = text_layer_create(GRect(4, 8, bounds.size.w - 8, 26));
+ text_layer_set_background_color(s_placeholder_title_layer, GColorClear);
+ text_layer_set_text_color(s_placeholder_title_layer, GColorBlack);
+ text_layer_set_text_alignment(s_placeholder_title_layer, GTextAlignmentCenter);
+ text_layer_set_font(s_placeholder_title_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
+
+ s_placeholder_body_layer = text_layer_create(GRect(6, 40, bounds.size.w - 12, 98));
+ text_layer_set_background_color(s_placeholder_body_layer, GColorClear);
+ text_layer_set_text_color(s_placeholder_body_layer, GColorBlack);
+ text_layer_set_text_alignment(s_placeholder_body_layer, GTextAlignmentCenter);
+ text_layer_set_font(s_placeholder_body_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
+
+ s_placeholder_hint_layer = text_layer_create(GRect(4, 140, bounds.size.w - 8, 24));
+ text_layer_set_background_color(s_placeholder_hint_layer, GColorClear);
+ text_layer_set_text_color(s_placeholder_hint_layer, GColorBlack);
+ text_layer_set_text_alignment(s_placeholder_hint_layer, GTextAlignmentCenter);
+ text_layer_set_font(s_placeholder_hint_layer, fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD));
+
+ layer_add_child(window_layer, text_layer_get_layer(s_placeholder_title_layer));
+ layer_add_child(window_layer, text_layer_get_layer(s_placeholder_body_layer));
+ layer_add_child(window_layer, text_layer_get_layer(s_placeholder_hint_layer));
+ set_placeholder_content(s_placeholder_title_text, s_placeholder_body_text, s_placeholder_hint_text);
+}
+
+static void detail_window_unload(Window *window) {
+ (void)window;
+ text_layer_destroy(s_placeholder_title_layer);
+ s_placeholder_title_layer = NULL;
+ text_layer_destroy(s_placeholder_body_layer);
+ s_placeholder_body_layer = NULL;
+ text_layer_destroy(s_placeholder_hint_layer);
+ s_placeholder_hint_layer = NULL;
+}
+
+static void menu_window_appear(Window *window) {
+ (void)window;
+ sync_main_menu_state();
+}
+
+static void configure_main_menu_items(void) {
+ s_main_menu_items[MAIN_MENU_INDEX_START_NEW] = (SimpleMenuItem) {
+ .title = "Start New Fast",
+ .subtitle = "Open preset targets",
+ .callback = menu_start_new_fast_callback
+ };
+ s_main_menu_items[MAIN_MENU_INDEX_CURRENT_TIMER] = (SimpleMenuItem) {
+ .title = "Current Timer",
+ .subtitle = "Live countdown screen",
+ .callback = menu_current_timer_callback
+ };
+ s_main_menu_items[MAIN_MENU_INDEX_STOP_CURRENT] = (SimpleMenuItem) {
+ .title = "Stop Current Fast",
+ .subtitle = "",
+ .callback = menu_stop_current_callback
+ };
+ s_main_menu_items[MAIN_MENU_INDEX_HISTORY] = (SimpleMenuItem) {
+ .title = "History",
+ .subtitle = "Completed fasts",
+ .callback = menu_history_callback
+ };
+ s_main_menu_items[MAIN_MENU_INDEX_STATS] = (SimpleMenuItem) {
+ .title = "Statistics",
+ .subtitle = "Streak and totals",
+ .callback = menu_statistics_callback
+ };
+ s_main_menu_items[MAIN_MENU_INDEX_SCIENCE] = (SimpleMenuItem) {
+ .title = "Fasting Science",
+ .subtitle = "Physiology timeline",
+ .callback = menu_science_callback
+ };
+ s_main_menu_items[MAIN_MENU_INDEX_SETTINGS] = (SimpleMenuItem) {
+ .title = "Settings",
+ .subtitle = "Defaults and behavior",
+ .callback = menu_settings_callback
+ };
+ s_main_menu_items[MAIN_MENU_INDEX_BACKUP] = (SimpleMenuItem) {
+ .title = "Backup to Phone",
+ .subtitle = "Export history data",
+ .callback = menu_backup_callback
+ };
+
+ s_main_menu_sections[0] = (SimpleMenuSection) {
+ .title = "FastForge",
+ .num_items = MAIN_MENU_ITEM_COUNT,
+ .items = s_main_menu_items
+ };
+}
+
+static void configure_preset_items(void) {
+ s_presets_menu_items[PRESET_MENU_INDEX_DEFAULT] = (SimpleMenuItem) {
+ .title = "Use Default Target",
+ .subtitle = "Current app setting",
+ .callback = preset_default_callback
+ };
+ s_presets_menu_items[PRESET_MENU_INDEX_16H] = (SimpleMenuItem) {
+ .title = "16 hours",
+ .subtitle = "Beginner baseline",
+ .callback = preset_16h_callback
+ };
+ s_presets_menu_items[PRESET_MENU_INDEX_18H] = (SimpleMenuItem) {
+ .title = "18 hours",
+ .subtitle = "Moderate challenge",
+ .callback = preset_18h_callback
+ };
+ s_presets_menu_items[PRESET_MENU_INDEX_20H] = (SimpleMenuItem) {
+ .title = "20 hours",
+ .subtitle = "Aggressive cutting",
+ .callback = preset_20h_callback
+ };
+ s_presets_menu_items[PRESET_MENU_INDEX_24H] = (SimpleMenuItem) {
+ .title = "24 hours",
+ .subtitle = "OMAD extended",
+ .callback = preset_24h_callback
+ };
+ s_presets_menu_items[PRESET_MENU_INDEX_36H] = (SimpleMenuItem) {
+ .title = "36 hours",
+ .subtitle = "Deep ketosis push",
+ .callback = preset_36h_callback
+ };
+
+ s_presets_menu_sections[0] = (SimpleMenuSection) {
+ .title = "Start New Fast",
+ .num_items = PRESET_MENU_ITEM_COUNT,
+ .items = s_presets_menu_items
+ };
+}
+
+static void init_windows(void) {
+ s_menu_window = window_create();
+ window_set_window_handlers(s_menu_window, (WindowHandlers) {
+ .load = menu_window_load,
+ .appear = menu_window_appear,
+ .unload = menu_window_unload
});
- window_stack_push(s_main_window, true);
+ s_timer_window = window_create();
+ window_set_click_config_provider(s_timer_window, timer_click_config_provider);
+ window_set_window_handlers(s_timer_window, (WindowHandlers) {
+ .load = timer_window_load,
+ .unload = timer_window_unload
+ });
s_goal_window = window_create();
window_set_background_color(s_goal_window, GColorBlack);
window_set_click_config_provider(s_goal_window, goal_click_config_provider);
window_set_window_handlers(s_goal_window, (WindowHandlers) {
.load = goal_window_load,
- .unload = goal_window_unload,
+ .unload = goal_window_unload
+ });
+
+ s_presets_window = window_create();
+ window_set_window_handlers(s_presets_window, (WindowHandlers) {
+ .load = presets_window_load,
+ .unload = presets_window_unload
});
+ s_detail_window = window_create();
+ window_set_click_config_provider(s_detail_window, placeholder_click_config_provider);
+ window_set_window_handlers(s_detail_window, (WindowHandlers) {
+ .load = detail_window_load,
+ .unload = detail_window_unload
+ });
+}
+
+static void destroy_windows(void) {
+ window_destroy(s_detail_window);
+ window_destroy(s_presets_window);
+ window_destroy(s_goal_window);
+ window_destroy(s_timer_window);
+ window_destroy(s_menu_window);
+}
+
+static void init(void) {
+ load_all_data();
+ tick_timer_service_subscribe(SECOND_UNIT, tick_handler);
+ configure_main_menu_items();
+ configure_preset_items();
+ set_placeholder_content("DETAIL", "FastForge destination placeholder.", "BACK Menu");
+ init_windows();
+ window_stack_push(s_menu_window, true);
schedule_alarm_if_needed();
+ sync_main_menu_state();
}
static void deinit(void) {
@@ -616,8 +1029,7 @@ static void deinit(void) {
target_time = 0;
save_all_data();
tick_timer_service_unsubscribe();
- window_destroy(s_goal_window);
- window_destroy(s_main_window);
+ destroy_windows();
}
int main(void) {