|
|
@@ -1,322 +1,101 @@
|
|
|
#include "flipp_pomodoro_app_i.h"
|
|
|
-#include <furi.h>
|
|
|
|
|
|
-#include <notification/notification_messages.h>
|
|
|
-#include <gui/gui.h>
|
|
|
-#include <dolphin/dolphin.h>
|
|
|
-#include <gui/elements.h>
|
|
|
-#include <input/input.h>
|
|
|
-
|
|
|
-/* Magic happens here -- this file is generated by fbt.
|
|
|
- * Just set fap_icon_assets in application.fam and #include {APPID}_icons.h */
|
|
|
-#include "flipp_pomodoro_icons.h"
|
|
|
-
|
|
|
-/// @brief Actions to be processed in a queue
|
|
|
-typedef enum {
|
|
|
- TimerTickType = 42,
|
|
|
- InputEventType,
|
|
|
-} ActionType;
|
|
|
-
|
|
|
-/// @brief Single action contains type and payload
|
|
|
-typedef struct {
|
|
|
- ActionType type;
|
|
|
- void* payload;
|
|
|
-} Action;
|
|
|
-
|
|
|
-typedef enum {
|
|
|
- Work,
|
|
|
- Rest,
|
|
|
-} PomodoroStage;
|
|
|
-
|
|
|
-static const NotificationSequence work_start_notification = {
|
|
|
- &message_display_backlight_on,
|
|
|
-
|
|
|
- &message_vibro_on,
|
|
|
-
|
|
|
- &message_note_b5,
|
|
|
- &message_delay_250,
|
|
|
-
|
|
|
- &message_note_d5,
|
|
|
- &message_delay_250,
|
|
|
-
|
|
|
- &message_sound_off,
|
|
|
- &message_vibro_off,
|
|
|
-
|
|
|
- &message_green_255,
|
|
|
- &message_delay_1000,
|
|
|
- &message_green_0,
|
|
|
- &message_delay_250,
|
|
|
- &message_green_255,
|
|
|
- &message_delay_1000,
|
|
|
-
|
|
|
- NULL,
|
|
|
-};
|
|
|
-
|
|
|
-static const NotificationSequence rest_start_notification = {
|
|
|
- &message_display_backlight_on,
|
|
|
-
|
|
|
- &message_vibro_on,
|
|
|
-
|
|
|
- &message_note_d5,
|
|
|
- &message_delay_250,
|
|
|
-
|
|
|
- &message_note_b5,
|
|
|
- &message_delay_250,
|
|
|
-
|
|
|
- &message_sound_off,
|
|
|
- &message_vibro_off,
|
|
|
-
|
|
|
- &message_red_255,
|
|
|
- &message_delay_1000,
|
|
|
- &message_red_0,
|
|
|
- &message_delay_250,
|
|
|
- &message_red_255,
|
|
|
- &message_delay_1000,
|
|
|
-
|
|
|
- NULL,
|
|
|
+enum
|
|
|
+{
|
|
|
+ CustomEventConsumed = true,
|
|
|
+ CustomEventNotConsumed = false,
|
|
|
};
|
|
|
|
|
|
-static const NotificationSequence* stage_start_notification_sequence_map[] = {
|
|
|
- [Work] = &work_start_notification,
|
|
|
- [Rest] = &rest_start_notification,
|
|
|
-};
|
|
|
-
|
|
|
-static char* next_stage_label[] = {
|
|
|
- [Work] = "Get Rest",
|
|
|
- [Rest] = "Start Work",
|
|
|
+static bool flipp_pomodoro_app_back_event_callback(void *ctx)
|
|
|
+{
|
|
|
+ furi_assert(ctx);
|
|
|
+ FlippPomodoroApp *app = ctx;
|
|
|
+ return scene_manager_handle_back_event(app->scene_manager);
|
|
|
};
|
|
|
|
|
|
-static const Icon* stage_background_image[] = {
|
|
|
- [Work] = &I_flipp_pomodoro_work_64,
|
|
|
- [Rest] = &I_flipp_pomodoro_rest_64,
|
|
|
-};
|
|
|
+static void flipp_pomodoro_app_tick_event_callback(void *ctx)
|
|
|
+{
|
|
|
+ furi_assert(ctx);
|
|
|
+ FlippPomodoroApp *app = ctx;
|
|
|
|
|
|
-static const PomodoroStage stage_rotaion_map[] = {
|
|
|
- [Work] = Rest,
|
|
|
- [Rest] = Work,
|
|
|
+ scene_manager_handle_custom_event(app->scene_manager, FlippPomodoroAppCustomEventTimerTick);
|
|
|
};
|
|
|
|
|
|
-const PomodoroStage default_stage = Work;
|
|
|
-
|
|
|
-typedef struct {
|
|
|
- PomodoroStage stage;
|
|
|
- uint32_t started_at_timestamp;
|
|
|
-} FlippPomodoroState;
|
|
|
-
|
|
|
-static void flipp_pomodoro__toggle_stage(FlippPomodoroState* state) {
|
|
|
- furi_assert(state);
|
|
|
- state->stage = stage_rotaion_map[state->stage];
|
|
|
- state->started_at_timestamp = time_now();
|
|
|
-}
|
|
|
+static bool flipp_pomodoro_app_custom_event_callback(void *ctx, uint32_t event)
|
|
|
+{
|
|
|
+ furi_assert(ctx);
|
|
|
+ FlippPomodoroApp *app = ctx;
|
|
|
+
|
|
|
+ switch (event)
|
|
|
+ {
|
|
|
+ case FlippPomodoroAppCustomEventStageSkip:
|
|
|
+ flipp_pomodoro__toggle_stage(app->state);
|
|
|
+ return CustomEventConsumed;
|
|
|
+ case FlippPomodoroAppCustomEventStageComplete:
|
|
|
+ if (app->state->stage == Work)
|
|
|
+ {
|
|
|
+ // REGISTER a deed on work stage complete to get an acheivement
|
|
|
+ DOLPHIN_DEED(DolphinDeedPluginGameWin);
|
|
|
+ };
|
|
|
|
|
|
-static char* flipp_pomodoro__next_stage_label(FlippPomodoroState* state) {
|
|
|
- furi_assert(state);
|
|
|
- return next_stage_label[state->stage];
|
|
|
+ flipp_pomodoro__toggle_stage(app->state);
|
|
|
+ notification_message(app->notification_app, stage_start_notification_sequence_map[app->state->stage]);
|
|
|
+ return CustomEventConsumed;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return scene_manager_handle_custom_event(app->scene_manager, event);
|
|
|
};
|
|
|
|
|
|
+FlippPomodoroApp *flipp_pomodoro_app_alloc()
|
|
|
+{
|
|
|
+ FlippPomodoroApp *app = malloc(sizeof(FlippPomodoroApp));
|
|
|
+ app->state = flipp_pomodoro__new();
|
|
|
|
|
|
-static void flipp_pomodoro__destroy(FlippPomodoroState* state) {
|
|
|
- furi_assert(state);
|
|
|
- free(state);
|
|
|
-}
|
|
|
-
|
|
|
-static uint32_t flipp_pomodoro__current_stage_total_duration(FlippPomodoroState* state) {
|
|
|
- const int32_t stage_duration_seconds_map[] = {
|
|
|
- [Work] = 25 * TIME_SECONDS_IN_MINUTE,
|
|
|
- [Rest] = 5 * TIME_SECONDS_IN_MINUTE,
|
|
|
- };
|
|
|
-
|
|
|
- return stage_duration_seconds_map[state->stage];
|
|
|
-}
|
|
|
-
|
|
|
-static uint32_t flipp_pomodoro__stage_expires_timestamp(FlippPomodoroState* state) {
|
|
|
- return state->started_at_timestamp + flipp_pomodoro__current_stage_total_duration(state);
|
|
|
-}
|
|
|
-
|
|
|
-static TimeDifference flipp_pomodoro__stage_remaining_duration(FlippPomodoroState* state) {
|
|
|
- const uint32_t stage_ends_at = flipp_pomodoro__stage_expires_timestamp(state);
|
|
|
- return time_difference_seconds(time_now(), stage_ends_at);
|
|
|
-}
|
|
|
-
|
|
|
-static bool flipp_pomodoro__is_stage_expired(FlippPomodoroState* state) {
|
|
|
- const uint32_t expired_by = flipp_pomodoro__stage_expires_timestamp(state);
|
|
|
- const uint8_t seamless_change_span_seconds = 1;
|
|
|
- return (time_now() - seamless_change_span_seconds) >= expired_by;
|
|
|
-}
|
|
|
-
|
|
|
-static FlippPomodoroState flipp_pomodoro__new() {
|
|
|
- const uint32_t now = time_now();
|
|
|
- const FlippPomodoroState new_state = {.stage=default_stage, .started_at_timestamp=now};
|
|
|
- return new_state;
|
|
|
-}
|
|
|
-
|
|
|
-typedef struct {
|
|
|
- FlippPomodoroState* state;
|
|
|
-} DrawContext;
|
|
|
-
|
|
|
-// Screen is 128x64 px
|
|
|
-static void app_draw_callback(Canvas* canvas, void* ctx) {
|
|
|
- // WARNING: place no side-effects into rener cycle!!
|
|
|
- DrawContext* draw_context = ctx;
|
|
|
-
|
|
|
- const TimeDifference remaining_stage_time = flipp_pomodoro__stage_remaining_duration(draw_context->state);
|
|
|
+ app->scene_manager = scene_manager_alloc(&flipp_pomodoro_scene_handlers, app);
|
|
|
+ app->gui = furi_record_open(RECORD_GUI);
|
|
|
+ app->notification_app = furi_record_open(RECORD_NOTIFICATION);
|
|
|
|
|
|
- // Format remaining stage time;
|
|
|
- FuriString* timer_string = furi_string_alloc();
|
|
|
- furi_string_printf(timer_string, "%02u:%02u", remaining_stage_time.minutes, remaining_stage_time.seconds);
|
|
|
- const char* remaining_stage_time_string = furi_string_get_cstr(timer_string);
|
|
|
+ app->view_dispatcher = view_dispatcher_alloc();
|
|
|
+ view_dispatcher_enable_queue(app->view_dispatcher);
|
|
|
+ view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
|
|
+ view_dispatcher_set_custom_event_callback(app->view_dispatcher, flipp_pomodoro_app_custom_event_callback);
|
|
|
+ view_dispatcher_set_tick_event_callback(app->view_dispatcher, flipp_pomodoro_app_tick_event_callback, 1000);
|
|
|
+ view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
|
|
+ view_dispatcher_set_navigation_event_callback(app->view_dispatcher, flipp_pomodoro_app_back_event_callback);
|
|
|
|
|
|
- // Render interface
|
|
|
- canvas_clear(canvas);
|
|
|
+ app->timer_view = flipp_pomodoro_view_timer_alloc();
|
|
|
|
|
|
- canvas_draw_icon(canvas, 0, 0, stage_background_image[draw_context->state->stage]);
|
|
|
+ view_dispatcher_add_view(
|
|
|
+ app->view_dispatcher,
|
|
|
+ FlippPomodoroAppViewTimer,
|
|
|
+ flipp_pomodoro_view_timer_get_view(app->timer_view));
|
|
|
|
|
|
- // Countdown section
|
|
|
- const uint8_t countdown_box_height = canvas_height(canvas)*0.4;
|
|
|
- const uint8_t countdown_box_width = canvas_width(canvas)*0.5;
|
|
|
- const uint8_t countdown_box_x = canvas_width(canvas) - countdown_box_width - 2;
|
|
|
- const uint8_t countdown_box_y = 0;
|
|
|
+ scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneTimer);
|
|
|
|
|
|
- elements_bold_rounded_frame(canvas,
|
|
|
- countdown_box_x,
|
|
|
- countdown_box_y,
|
|
|
- countdown_box_width,
|
|
|
- countdown_box_height
|
|
|
- );
|
|
|
-
|
|
|
- canvas_set_font(canvas, FontBigNumbers);
|
|
|
- canvas_draw_str_aligned(
|
|
|
- canvas,
|
|
|
- countdown_box_x + (countdown_box_width/2),
|
|
|
- countdown_box_y + (countdown_box_height/2),
|
|
|
- AlignCenter,
|
|
|
- AlignCenter,
|
|
|
- remaining_stage_time_string
|
|
|
- );
|
|
|
-
|
|
|
- // Draw layout
|
|
|
- canvas_set_font(canvas, FontSecondary);
|
|
|
- elements_button_right(canvas, flipp_pomodoro__next_stage_label(draw_context->state));
|
|
|
-
|
|
|
-
|
|
|
- // Cleanup
|
|
|
- furi_string_free(timer_string);
|
|
|
-}
|
|
|
-
|
|
|
-static void clock_tick_callback(void* ctx) {
|
|
|
- furi_assert(ctx);
|
|
|
- FuriMessageQueue* queue = ctx;
|
|
|
- Action action = {.type = TimerTickType};
|
|
|
- furi_message_queue_put(queue, &action, 0);
|
|
|
-}
|
|
|
-
|
|
|
-static void app_input_callback(InputEvent* input_event, void* ctx) {
|
|
|
- furi_assert(ctx);
|
|
|
-
|
|
|
- Action action = {.type=InputEventType, .payload=input_event};
|
|
|
-
|
|
|
- FuriMessageQueue* event_queue = ctx;
|
|
|
- furi_message_queue_put(event_queue, &action, FuriWaitForever);
|
|
|
+ return app;
|
|
|
};
|
|
|
|
|
|
-static bool is_input_event(Action action) {
|
|
|
- return action.type == InputEventType;
|
|
|
-}
|
|
|
-
|
|
|
-static bool is_press_or_repeat(InputEvent* input_event) {
|
|
|
- return (input_event->type == InputTypePress) || (input_event->type == InputTypeRepeat);
|
|
|
-}
|
|
|
-
|
|
|
-static bool is_button_back_pressed(Action action) {
|
|
|
- return is_input_event(action)
|
|
|
- && is_press_or_repeat(action.payload)
|
|
|
- && ((InputEvent*)action.payload)->key == InputKeyBack;
|
|
|
-}
|
|
|
-
|
|
|
-static bool is_button_right_pressed(Action action) {
|
|
|
- return is_input_event(action)
|
|
|
- && is_press_or_repeat(action.payload)
|
|
|
- && ((InputEvent*)action.payload)->key == InputKeyRight;
|
|
|
-}
|
|
|
-
|
|
|
-static bool is_timer_tick(Action action) {
|
|
|
- return action.type == TimerTickType;
|
|
|
+void flipp_pomodoro_app_free(FlippPomodoroApp *app)
|
|
|
+{
|
|
|
+ view_dispatcher_remove_view(app->view_dispatcher, FlippPomodoroAppViewTimer);
|
|
|
+ view_dispatcher_free(app->view_dispatcher);
|
|
|
+ scene_manager_free(app->scene_manager);
|
|
|
+ flipp_pomodoro_view_timer_free(app->timer_view);
|
|
|
+ flipp_pomodoro__destroy(app->state);
|
|
|
+ free(app);
|
|
|
+ furi_record_close(RECORD_GUI);
|
|
|
+ furi_record_close(RECORD_NOTIFICATION);
|
|
|
};
|
|
|
|
|
|
-int32_t flipp_pomodoro_main(void* p) {
|
|
|
+int32_t flipp_pomodoro_app(void *p)
|
|
|
+{
|
|
|
UNUSED(p);
|
|
|
- FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(Action));
|
|
|
-
|
|
|
- FlippPomodoroState state = flipp_pomodoro__new();
|
|
|
-
|
|
|
- // Configure view port
|
|
|
- ViewPort* view_port = view_port_alloc();
|
|
|
- DrawContext draw_context = {.state=&state};
|
|
|
- view_port_draw_callback_set(view_port, app_draw_callback, &draw_context);
|
|
|
- view_port_input_callback_set(view_port, app_input_callback, event_queue);
|
|
|
-
|
|
|
- // Initiate timer
|
|
|
- FuriTimer* timer = furi_timer_alloc(clock_tick_callback, FuriTimerTypePeriodic, event_queue);
|
|
|
- furi_timer_start(timer, 500);
|
|
|
+ FlippPomodoroApp *app = flipp_pomodoro_app_alloc();
|
|
|
|
|
|
- NotificationApp* notification_app = furi_record_open(RECORD_NOTIFICATION);
|
|
|
+ view_dispatcher_run(app->view_dispatcher);
|
|
|
|
|
|
- // Register view port in GUI
|
|
|
- Gui* gui = furi_record_open(RECORD_GUI);
|
|
|
- gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
|
|
-
|
|
|
-
|
|
|
- bool running = true;
|
|
|
-
|
|
|
- const int queue_reading_window_tics = 200;
|
|
|
-
|
|
|
- while(running) {
|
|
|
- Action action;
|
|
|
- if(furi_message_queue_get(event_queue, &action, queue_reading_window_tics) != FuriStatusOk) {
|
|
|
- // Queue read is failed
|
|
|
- continue;
|
|
|
- };
|
|
|
-
|
|
|
- if(!action.type) {
|
|
|
- // No item in queue
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- if(is_button_back_pressed(action)) {
|
|
|
- running = false;
|
|
|
- continue;
|
|
|
- };
|
|
|
-
|
|
|
- if(is_timer_tick(action)) {
|
|
|
- if(flipp_pomodoro__is_stage_expired(&state)) {
|
|
|
- if(state.stage == Work) {
|
|
|
- // REGISTER a deed on work stage complete to get an acheivement
|
|
|
- DOLPHIN_DEED(DolphinDeedPluginGameWin);
|
|
|
- };
|
|
|
-
|
|
|
- flipp_pomodoro__toggle_stage(&state);
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- notification_message(notification_app, stage_start_notification_sequence_map[state.stage]);
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- if(is_button_right_pressed(action)) {
|
|
|
- flipp_pomodoro__toggle_stage(&state);
|
|
|
- };
|
|
|
-
|
|
|
- view_port_update(view_port); // Only re-draw on event
|
|
|
- }
|
|
|
-
|
|
|
- view_port_enabled_set(view_port, false);
|
|
|
- gui_remove_view_port(gui, view_port);
|
|
|
- view_port_free(view_port);
|
|
|
- furi_message_queue_free(event_queue);
|
|
|
- furi_record_close(RECORD_GUI);
|
|
|
- furi_timer_free(timer);
|
|
|
- flipp_pomodoro__destroy(&state);
|
|
|
- furi_record_close(RECORD_NOTIFICATION);
|
|
|
+ flipp_pomodoro_app_free(app);
|
|
|
|
|
|
return 0;
|
|
|
-}
|
|
|
+};
|