summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-12 10:02:51 +0300
committerPaul Buetow <paul@buetow.org>2026-04-12 10:02:51 +0300
commit080dfaf5b48b60b2d8a8925c398ccd1e060ba077 (patch)
tree4b52949e8653eb95a44fad824289fb146ea4bbf8
parent58382f5f5376ba6ab3832e8606467881089ecf15 (diff)
g2: implement day-1 persistence load/save layer
-rw-r--r--src/c/fastforge.c143
-rw-r--r--src/c/fastforge.h10
2 files changed, 150 insertions, 3 deletions
diff --git a/src/c/fastforge.c b/src/c/fastforge.c
index d248ab8..79d9217 100644
--- a/src/c/fastforge.c
+++ b/src/c/fastforge.c
@@ -5,20 +5,153 @@ FastEntry history[MAX_FASTS];
int history_count = 0;
FastEntry current_fast = {0};
uint16_t global_target_minutes = DEFAULT_TARGET_MINUTES;
+StreakData streak_data = {0};
AppTimer *alarm_timer = NULL;
time_t target_time = 0;
static Window *s_main_window;
static TextLayer *s_text_layer;
+static char s_status_text[96];
+
+static int clamp_history_count(int value) {
+ if (value < 0) {
+ return 0;
+ }
+ if (value > MAX_FASTS) {
+ return MAX_FASTS;
+ }
+ return value;
+}
+
+static void normalize_loaded_data(void) {
+ if (global_target_minutes == 0) {
+ global_target_minutes = DEFAULT_TARGET_MINUTES;
+ }
+
+ if (current_fast.start_time != 0 && current_fast.target_minutes == 0) {
+ current_fast.target_minutes = global_target_minutes;
+ }
+
+ if (current_fast.end_time != 0 && current_fast.end_time < current_fast.start_time) {
+ current_fast.end_time = 0;
+ }
+}
+
+void save_all_data(void) {
+ persist_write_int(KEY_HISTORY_COUNT, history_count);
+ persist_write_data(KEY_HISTORY_DATA, history, sizeof(FastEntry) * history_count);
+ persist_write_data(KEY_CURRENT_FAST, &current_fast, sizeof(FastEntry));
+ persist_write_int(KEY_TARGET_MIN, global_target_minutes);
+ persist_write_data(KEY_STREAK_DATA, &streak_data, sizeof(StreakData));
+}
+
+void load_all_data(void) {
+ history_count = 0;
+ memset(history, 0, sizeof(history));
+ memset(&current_fast, 0, sizeof(current_fast));
+ global_target_minutes = DEFAULT_TARGET_MINUTES;
+ memset(&streak_data, 0, sizeof(streak_data));
+
+ if (persist_exists(KEY_HISTORY_COUNT)) {
+ history_count = clamp_history_count(persist_read_int(KEY_HISTORY_COUNT));
+ }
+ if (history_count > 0 && persist_exists(KEY_HISTORY_DATA)) {
+ const int expected_size = sizeof(FastEntry) * history_count;
+ const int read_size = persist_read_data(KEY_HISTORY_DATA, history, expected_size);
+ if (read_size != expected_size) {
+ history_count = 0;
+ memset(history, 0, sizeof(history));
+ }
+ }
+ if (persist_exists(KEY_CURRENT_FAST)) {
+ const int read_size = persist_read_data(KEY_CURRENT_FAST, &current_fast, sizeof(FastEntry));
+ if (read_size != (int)sizeof(FastEntry)) {
+ memset(&current_fast, 0, sizeof(current_fast));
+ }
+ }
+ if (persist_exists(KEY_TARGET_MIN)) {
+ const int read_target = persist_read_int(KEY_TARGET_MIN);
+ if (read_target > 0 && read_target <= UINT16_MAX) {
+ global_target_minutes = (uint16_t)read_target;
+ }
+ }
+ if (persist_exists(KEY_STREAK_DATA)) {
+ const int read_size = persist_read_data(KEY_STREAK_DATA, &streak_data, sizeof(StreakData));
+ if (read_size != (int)sizeof(StreakData)) {
+ memset(&streak_data, 0, sizeof(streak_data));
+ }
+ }
+
+ normalize_loaded_data();
+}
+
+static void refresh_status_text(void) {
+ if (current_fast.start_time == 0) {
+ snprintf(s_status_text, sizeof(s_status_text), "No active fast\nTarget: %u min\nSELECT: Start\nDOWN: Quit",
+ global_target_minutes);
+ } else {
+ const time_t elapsed = time(NULL) - current_fast.start_time;
+ const int elapsed_hours = (int)(elapsed / 3600);
+ const int elapsed_minutes = (int)((elapsed % 3600) / 60);
+ snprintf(s_status_text, sizeof(s_status_text),
+ "Fast running\n%02dh %02dm elapsed\nSELECT: Stop\nDOWN: Quit",
+ elapsed_hours, elapsed_minutes);
+ }
+ text_layer_set_text(s_text_layer, s_status_text);
+}
+
+static void append_history_entry(const FastEntry *entry) {
+ if (!entry) {
+ return;
+ }
+
+ 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;
+}
// ==================== BUTTON HANDLERS ====================
static void select_click_handler(ClickRecognizerRef recognizer, void *context) {
- text_layer_set_text(s_text_layer, "Hello!");
- APP_LOG(APP_LOG_LEVEL_INFO, "SELECT pressed - Hello!");
+ (void)recognizer;
+ (void)context;
+
+ if (current_fast.start_time == 0) {
+ current_fast.start_time = time(NULL);
+ current_fast.end_time = 0;
+ current_fast.target_minutes = global_target_minutes;
+ memset(current_fast.note, 0, sizeof(current_fast.note));
+ current_fast.max_stage_reached = 0;
+ APP_LOG(APP_LOG_LEVEL_INFO, "Started fast at %ld", (long)current_fast.start_time);
+ } else {
+ FastEntry completed = current_fast;
+ completed.end_time = time(NULL);
+ append_history_entry(&completed);
+ mark_fast_completed(completed.end_time);
+ memset(&current_fast, 0, sizeof(current_fast));
+ APP_LOG(APP_LOG_LEVEL_INFO, "Stopped fast and saved to history (count=%d)", history_count);
+ }
+
+ save_all_data();
+ refresh_status_text();
}
static void down_click_handler(ClickRecognizerRef recognizer, void *context) {
+ (void)recognizer;
+ (void)context;
+
APP_LOG(APP_LOG_LEVEL_INFO, "DOWN pressed - Quitting app...");
window_stack_pop_all(false); // Cleanly quit the app
}
@@ -41,18 +174,21 @@ static void window_load(Window *window) {
text_layer_set_text_color(s_text_layer, GColorBlack);
text_layer_set_text_alignment(s_text_layer, GTextAlignmentCenter);
text_layer_set_font(s_text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD));
- text_layer_set_text(s_text_layer, "Press SELECT\nfor Hello\n\nDOWN to quit");
+ refresh_status_text();
layer_add_child(window_layer, text_layer_get_layer(s_text_layer));
}
static void window_unload(Window *window) {
+ (void)window;
+ save_all_data();
text_layer_destroy(s_text_layer);
}
// ==================== APP INIT / DEINIT ====================
static void init(void) {
+ load_all_data();
s_main_window = window_create();
window_set_click_config_provider(s_main_window, click_config_provider);
window_set_window_handlers(s_main_window, (WindowHandlers) {
@@ -64,6 +200,7 @@ static void init(void) {
}
static void deinit(void) {
+ save_all_data();
window_destroy(s_main_window);
}
diff --git a/src/c/fastforge.h b/src/c/fastforge.h
index 1ab5339..201ce11 100644
--- a/src/c/fastforge.h
+++ b/src/c/fastforge.h
@@ -11,6 +11,12 @@ typedef struct {
uint8_t max_stage_reached; // 0=none, 1=12h, 2=18h, 3=24h+
} FastEntry;
+typedef struct {
+ uint16_t current_streak;
+ uint16_t longest_streak;
+ time_t last_completed_fast_end;
+} StreakData;
+
#define MAX_FASTS 64
#define DEFAULT_TARGET_MINUTES (16 * 60)
@@ -24,7 +30,11 @@ extern FastEntry history[MAX_FASTS];
extern int history_count;
extern FastEntry current_fast;
extern uint16_t global_target_minutes;
+extern StreakData streak_data;
extern AppTimer *alarm_timer;
extern time_t target_time;
+void save_all_data(void);
+void load_all_data(void);
+
#endif