Просмотр исходного кода

refactor: modules structure (#11)

* wip inlined conditions

* wip extract sub-modules from the main app

* add missing params docs

* wip add views and scenes

* add build and run, move render

* completed working prototype

* complete cleanup

* assign better event names

* update docs
Mikhail Gubenko 3 лет назад
Родитель
Сommit
70e3a71689

+ 1 - 1
application.fam

@@ -2,7 +2,7 @@ App(
     appid="flipp_pomodoro",
     appid="flipp_pomodoro",
     name="Flipp Pomodoro",
     name="Flipp Pomodoro",
     apptype=FlipperAppType.EXTERNAL,
     apptype=FlipperAppType.EXTERNAL,
-    entry_point="flipp_pomodoro_main",
+    entry_point="flipp_pomodoro_app",
     requires=["gui", "notification", "dolphin"],
     requires=["gui", "notification", "dolphin"],
     stack_size=1 * 1024,
     stack_size=1 * 1024,
     fap_category="Productivity",
     fap_category="Productivity",

+ 74 - 295
flipp_pomodoro_app.c

@@ -1,322 +1,101 @@
 #include "flipp_pomodoro_app_i.h"
 #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);
     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;
     return 0;
-}
+};

+ 34 - 0
flipp_pomodoro_app.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <notification/notification_messages.h>
+#include "views/flipp_pomodoro_timer_view.h"
+
+#include "modules/flipp_pomodoro.h"
+
+typedef enum
+{
+    // Reserve first 100 events for button types and indexes, starting from 0
+    FlippPomodoroAppCustomEventStageSkip = 100,
+    FlippPomodoroAppCustomEventStageComplete, // By Expiration
+    FlippPomodoroAppCustomEventTimerTick,
+} FlippPomodoroAppCustomEvent;
+
+typedef struct
+{
+    SceneManager *scene_manager;
+    ViewDispatcher *view_dispatcher;
+    Gui *gui;
+    NotificationApp *notification_app;
+    FlippPomodoroTimerView *timer_view;
+    FlippPomodoroState *state;
+} FlippPomodoroApp;
+
+typedef enum
+{
+    FlippPomodoroAppViewTimer,
+} FlippPomodoroAppView;

+ 24 - 2
flipp_pomodoro_app_i.h

@@ -1,9 +1,31 @@
 #pragma once
 #pragma once
 
 
+#define FURI_DEBUG 1
+
 /**
 /**
  * Index of dependencies for the main app
  * Index of dependencies for the main app
-*/
+ */
+
+// Platform Imports
 
 
 #include <furi.h>
 #include <furi.h>
 #include <furi_hal.h>
 #include <furi_hal.h>
-#include "helpers/time.h"
+#include <gui/gui.h>
+#include <gui/view_stack.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+#include <input/input.h>
+
+// App resource imports
+
+#include "helpers/time.h"
+#include "helpers/notifications.h"
+#include "modules/flipp_pomodoro.h"
+#include "flipp_pomodoro_app.h"
+#include "scenes/flipp_pomodoro_scene.h"
+#include "views/flipp_pomodoro_timer_view.h"
+
+// Auto-compiled icons
+#include "flipp_pomodoro_icons.h"

+ 5 - 0
helpers/debug.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#include <furi.h>
+
+#define TAG "FlippPomodoro"

+ 49 - 0
helpers/notifications.c

@@ -0,0 +1,49 @@
+#include <notification/notification_messages.h>
+
+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,
+};
+
+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,
+};

+ 13 - 0
helpers/notifications.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include "../modules/flipp_pomodoro.h"
+#include <notification/notification_messages.h>
+
+extern const NotificationSequence work_start_notification;
+extern const NotificationSequence rest_start_notification;
+
+/// @brief Defines a notification sequence that should indicate start of specific pomodoro stage.
+const NotificationSequence *stage_start_notification_sequence_map[] = {
+    [Work] = &work_start_notification,
+    [Rest] = &rest_start_notification,
+};

+ 6 - 6
helpers/time.c

@@ -5,17 +5,17 @@
 const int TIME_SECONDS_IN_MINUTE = 60;
 const int TIME_SECONDS_IN_MINUTE = 60;
 const int TIME_MINUTES_IN_HOUR = 60;
 const int TIME_MINUTES_IN_HOUR = 60;
 
 
-uint32_t time_now() {
+uint32_t time_now()
+{
     return furi_hal_rtc_get_timestamp();
     return furi_hal_rtc_get_timestamp();
 };
 };
 
 
-TimeDifference time_difference_seconds(uint32_t begin, uint32_t end) {
+TimeDifference time_difference_seconds(uint32_t begin, uint32_t end)
+{
     const uint32_t duration_seconds = end - begin;
     const uint32_t duration_seconds = end - begin;
 
 
     uint32_t minutes = (duration_seconds / TIME_MINUTES_IN_HOUR) % TIME_MINUTES_IN_HOUR;
     uint32_t minutes = (duration_seconds / TIME_MINUTES_IN_HOUR) % TIME_MINUTES_IN_HOUR;
     uint32_t seconds = duration_seconds % TIME_SECONDS_IN_MINUTE;
     uint32_t seconds = duration_seconds % TIME_SECONDS_IN_MINUTE;
 
 
-    return (TimeDifference){.total_seconds=duration_seconds, .minutes=minutes, .seconds=seconds};
-}
-
-
+    return (TimeDifference){.total_seconds = duration_seconds, .minutes = minutes, .seconds = seconds};
+};

+ 2 - 2
helpers/time.h

@@ -7,7 +7,8 @@ extern const int TIME_SECONDS_IN_MINUTE;
 extern const int TIME_MINUTES_IN_HOUR;
 extern const int TIME_MINUTES_IN_HOUR;
 
 
 /// @brief Container for a time period
 /// @brief Container for a time period
-typedef struct {
+typedef struct
+{
     uint8_t seconds;
     uint8_t seconds;
     uint8_t minutes;
     uint8_t minutes;
     uint32_t total_seconds;
     uint32_t total_seconds;
@@ -22,4 +23,3 @@ uint32_t time_now();
 /// @param end - end timestamp of the period to measure
 /// @param end - end timestamp of the period to measure
 /// @return TimeDifference struct
 /// @return TimeDifference struct
 TimeDifference time_difference_seconds(uint32_t begin, uint32_t end);
 TimeDifference time_difference_seconds(uint32_t begin, uint32_t end);
-

+ 72 - 0
modules/flipp_pomodoro.c

@@ -0,0 +1,72 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include "../helpers/time.h"
+#include "flipp_pomodoro.h"
+
+char *next_stage_label[] = {
+    [Work] = "Get Rest",
+    [Rest] = "Start Work",
+};
+
+const PomodoroStage stage_rotaion_map[] = {
+    [Work] = Rest,
+    [Rest] = Work,
+};
+
+const PomodoroStage default_stage = Work;
+
+void flipp_pomodoro__toggle_stage(FlippPomodoroState *state)
+{
+    furi_assert(state);
+    state->stage = stage_rotaion_map[state->stage];
+    state->started_at_timestamp = time_now();
+};
+
+char *flipp_pomodoro__next_stage_label(FlippPomodoroState *state)
+{
+    furi_assert(state);
+    return next_stage_label[state->stage];
+};
+
+void flipp_pomodoro__destroy(FlippPomodoroState *state)
+{
+    furi_assert(state);
+    free(state);
+};
+
+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];
+};
+
+uint32_t flipp_pomodoro__stage_expires_timestamp(FlippPomodoroState *state)
+{
+    return state->started_at_timestamp + flipp_pomodoro__current_stage_total_duration(state);
+};
+
+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);
+};
+
+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;
+};
+
+FlippPomodoroState *flipp_pomodoro__new()
+{
+    FlippPomodoroState *state = malloc(sizeof(FlippPomodoroState));
+    const uint32_t now = time_now();
+    state->started_at_timestamp = now;
+    state->stage = default_stage;
+    return state;
+};

+ 45 - 0
modules/flipp_pomodoro.h

@@ -0,0 +1,45 @@
+#pragma once
+
+#include <furi_hal.h>
+#include "../helpers/time.h"
+
+/// @brief Options of pomodoro stages
+typedef enum
+{
+    Work,
+    Rest,
+} PomodoroStage;
+
+/// @brief State of the pomodoro timer
+typedef struct
+{
+    PomodoroStage stage;
+    uint32_t started_at_timestamp;
+} FlippPomodoroState;
+
+/// @brief Generates initial state
+/// @param state - pointer to the state of pomorodo.
+/// @returns A new pre-populated state for pomodoro timer
+FlippPomodoroState *flipp_pomodoro__new();
+
+/// @brief Destroys state of timer and it's dependencies
+void flipp_pomodoro__destroy(FlippPomodoroState *state);
+
+/// @brief Get remaining stage time.
+/// @param state - pointer to the state of pomorodo.
+/// @returns Time difference to the end of current stage
+TimeDifference flipp_pomodoro__stage_remaining_duration(FlippPomodoroState *state);
+
+/// @brief Label of transition to the next stage
+/// @param state - pointer to the state of pomorodo.
+/// @returns string with the label of the "skipp" button
+char *flipp_pomodoro__next_stage_label(FlippPomodoroState *state);
+
+/// @brief Check if current stage is expired
+/// @param state - pointer to the state of pomorodo.
+/// @returns expriations status - true means stage is expired
+bool flipp_pomodoro__is_stage_expired(FlippPomodoroState *state);
+
+/// @brief Rotate stage of the timer
+/// @param state - pointer to the state of pomorodo.
+void flipp_pomodoro__toggle_stage(FlippPomodoroState *state);

+ 0 - 0
scenes/.keep


+ 1 - 0
scenes/config/flipp_pomodoro_scene_config.h

@@ -0,0 +1 @@
+ADD_SCENE(flipp_pomodoro, timer, Timer)

+ 30 - 0
scenes/flipp_pomodoro_scene.c

@@ -0,0 +1,30 @@
+#include "flipp_pomodoro_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const flipp_pomodoro_scene_on_enter_handlers[])(void*) = {
+#include "config/flipp_pomodoro_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const flipp_pomodoro_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "config/flipp_pomodoro_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const flipp_pomodoro_scene_on_exit_handlers[])(void* context) = {
+#include "config/flipp_pomodoro_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers flipp_pomodoro_scene_handlers = {
+    .on_enter_handlers = flipp_pomodoro_scene_on_enter_handlers,
+    .on_event_handlers = flipp_pomodoro_scene_on_event_handlers,
+    .on_exit_handlers = flipp_pomodoro_scene_on_exit_handlers,
+    .scene_num = FlippPomodoroSceneNum,
+};

+ 28 - 0
scenes/flipp_pomodoro_scene.h

@@ -0,0 +1,28 @@
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) FlippPomodoroScene##id,
+typedef enum
+{
+#include "config/flipp_pomodoro_scene_config.h"
+    FlippPomodoroSceneNum,
+} FlippPomodoroScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers flipp_pomodoro_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void *);
+#include "config/flipp_pomodoro_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void *context, SceneManagerEvent event);
+#include "config/flipp_pomodoro_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void *context);
+#include "config/flipp_pomodoro_scene_config.h"
+#undef ADD_SCENE

+ 75 - 0
scenes/flipp_pomodoro_scene_timer.c

@@ -0,0 +1,75 @@
+#include <furi.h>
+#include <gui/scene_manager.h>
+#include <gui/view_dispatcher.h>
+#include "../flipp_pomodoro_app.h"
+#include "../views/flipp_pomodoro_timer_view.h"
+
+enum
+{
+    SceneEventConusmed = true,
+    SceneEventNotConusmed = false
+};
+
+uint8_t ExitSignal = 0;
+
+void flipp_pomodoro_scene_timer_on_next_stage(void *ctx)
+{
+    furi_assert(ctx);
+
+    FlippPomodoroApp *app = ctx;
+
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher,
+        FlippPomodoroAppCustomEventStageSkip);
+};
+
+void flipp_pomodoro_scene_timer_on_enter(void *context)
+{
+    furi_assert(context);
+
+    FlippPomodoroApp *app = context;
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlippPomodoroAppViewTimer);
+    flipp_pomodoro_view_timer_set_state(
+        flipp_pomodoro_view_timer_get_view(app->timer_view),
+        app->state);
+    flipp_pomodoro_view_timer_set_on_right_cb(
+        app->timer_view,
+        flipp_pomodoro_scene_timer_on_next_stage,
+        app);
+};
+
+void flipp_pomodoro_scene_timer_handle_custom_event(FlippPomodoroApp *app, FlippPomodoroAppCustomEvent custom_event)
+{
+    if (custom_event == FlippPomodoroAppCustomEventTimerTick && flipp_pomodoro__is_stage_expired(app->state))
+    {
+        view_dispatcher_send_custom_event(
+            app->view_dispatcher,
+            FlippPomodoroAppCustomEventStageComplete);
+    }
+};
+
+bool flipp_pomodoro_scene_timer_on_event(void *ctx, SceneManagerEvent event)
+{
+    furi_assert(ctx);
+    FlippPomodoroApp *app = ctx;
+
+    switch (event.type)
+    {
+    case SceneManagerEventTypeCustom:
+        flipp_pomodoro_scene_timer_handle_custom_event(
+            app,
+            event.event);
+        return SceneEventConusmed;
+    case SceneManagerEventTypeBack:
+        return ExitSignal;
+    default:
+        break;
+    };
+    return SceneEventNotConusmed;
+};
+
+void flipp_pomodoro_scene_timer_on_exit(void *context)
+{
+    UNUSED(context);
+};

+ 0 - 0
views/.keep


+ 151 - 0
views/flipp_pomodoro_timer_view.c

@@ -0,0 +1,151 @@
+#include "flipp_pomodoro_timer_view.h"
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <gui/view.h>
+#include "../helpers/debug.h"
+#include "../flipp_pomodoro_app.h"
+#include "../modules/flipp_pomodoro.h"
+
+// Auto-compiled icons
+#include "flipp_pomodoro_icons.h"
+
+enum
+{
+    ViewInputConsumed = true,
+    ViewInputNotConusmed = false,
+};
+
+struct FlippPomodoroTimerView
+{
+    View *view;
+    FlippPomodoroTimerViewInputCb right_cb;
+    void *right_cb_ctx;
+};
+
+typedef struct
+{
+    FlippPomodoroState *state;
+} FlippPomodoroTimerViewModel;
+
+static const Icon *stage_background_image[] = {
+    [Work] = &I_flipp_pomodoro_work_64,
+    [Rest] = &I_flipp_pomodoro_rest_64,
+};
+
+static void flipp_pomodoro_view_timer_draw_countdown(Canvas *canvas, TimeDifference remaining_time)
+{
+    canvas_set_font(canvas, FontBigNumbers);
+    const uint8_t right_border_margin = 1;
+
+    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 - right_border_margin;
+    const uint8_t countdown_box_y = 0;
+
+    elements_bold_rounded_frame(canvas,
+                                countdown_box_x,
+                                countdown_box_y,
+                                countdown_box_width,
+                                countdown_box_height);
+
+    FuriString *timer_string = furi_string_alloc();
+    furi_string_printf(timer_string, "%02u:%02u", remaining_time.minutes, remaining_time.seconds);
+    const char *remaining_stage_time_string = furi_string_get_cstr(timer_string);
+    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);
+
+    furi_string_free(timer_string);
+};
+
+static void flipp_pomodoro_view_timer_draw_callback(Canvas *canvas, void *_model)
+{
+    if (!_model)
+    {
+        return;
+    };
+
+    FlippPomodoroTimerViewModel *model = _model;
+
+    canvas_clear(canvas);
+    canvas_draw_icon(canvas, 0, 0, stage_background_image[model->state->stage]);
+    flipp_pomodoro_view_timer_draw_countdown(
+        canvas,
+        flipp_pomodoro__stage_remaining_duration(model->state));
+
+    canvas_set_font(canvas, FontSecondary);
+    elements_button_right(canvas, flipp_pomodoro__next_stage_label(model->state));
+};
+
+bool flipp_pomodoro_view_timer_input_callback(InputEvent *event, void *ctx)
+{
+    furi_assert(ctx);
+    furi_assert(event);
+    FlippPomodoroTimerView *timer = ctx;
+
+    const bool should_trigger_right_event_cb = (event->type == InputTypePress) &&
+                                               (event->key == InputKeyRight) &&
+                                               (timer->right_cb != NULL);
+
+    if (should_trigger_right_event_cb)
+    {
+        furi_assert(timer->right_cb);
+        furi_assert(timer->right_cb_ctx);
+        timer->right_cb(timer->right_cb_ctx);
+        return ViewInputConsumed;
+    };
+
+    return ViewInputNotConusmed;
+};
+
+View *flipp_pomodoro_view_timer_get_view(FlippPomodoroTimerView *timer)
+{
+    furi_assert(timer);
+    return timer->view;
+};
+
+FlippPomodoroTimerView *flipp_pomodoro_view_timer_alloc()
+{
+    FlippPomodoroTimerView *timer = malloc(sizeof(FlippPomodoroTimerView));
+    timer->view = view_alloc();
+
+    view_allocate_model(timer->view, ViewModelTypeLockFree, sizeof(FlippPomodoroTimerViewModel));
+    view_set_context(flipp_pomodoro_view_timer_get_view(timer), timer);
+    view_set_draw_callback(timer->view, flipp_pomodoro_view_timer_draw_callback);
+    view_set_input_callback(timer->view, flipp_pomodoro_view_timer_input_callback);
+
+    return timer;
+};
+
+void flipp_pomodoro_view_timer_set_on_right_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb right_cb, void *right_cb_ctx)
+{
+    furi_assert(right_cb);
+    furi_assert(right_cb_ctx);
+    timer->right_cb = right_cb;
+    timer->right_cb_ctx = right_cb_ctx;
+};
+
+void flipp_pomodoro_view_timer_set_state(View *view, FlippPomodoroState *state)
+{
+    furi_assert(view);
+    furi_assert(state);
+    with_view_model(
+        view,
+        FlippPomodoroTimerViewModel * model,
+        {
+            model->state = state;
+        },
+        false);
+};
+
+void flipp_pomodoro_view_timer_free(FlippPomodoroTimerView *timer)
+{
+    furi_assert(timer);
+    view_free(timer->view);
+    free(timer);
+};

+ 18 - 0
views/flipp_pomodoro_timer_view.h

@@ -0,0 +1,18 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../modules/flipp_pomodoro.h"
+
+typedef struct FlippPomodoroTimerView FlippPomodoroTimerView;
+
+typedef void (*FlippPomodoroTimerViewInputCb)(void *context);
+
+FlippPomodoroTimerView *flipp_pomodoro_view_timer_alloc();
+
+View *flipp_pomodoro_view_timer_get_view(FlippPomodoroTimerView *timer);
+
+void flipp_pomodoro_view_timer_free(FlippPomodoroTimerView *timer);
+
+void flipp_pomodoro_view_timer_set_state(View *view, FlippPomodoroState *state);
+
+void flipp_pomodoro_view_timer_set_on_right_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb right_cb, void *right_cb_ctx);