Explorar el Código

Add pomodoro from https://github.com/Th3Un1q3/flipp_pomodoro

git-subtree-dir: pomodoro
git-subtree-mainline: aeafc0044ae7045680ed2abf8604c3c711a01722
git-subtree-split: d3299783cde2162f09b54deb48f6c0c80190c6d8
Willy-JL hace 2 años
padre
commit
e35b7d9333
Se han modificado 33 ficheros con 1454 adiciones y 0 borrados
  1. 1 0
      pomodoro/.gitsubtree
  2. 11 0
      pomodoro/application.fam
  3. BIN
      pomodoro/flipp_pomodoro_10.png
  4. 127 0
      pomodoro/flipp_pomodoro_app.c
  5. 42 0
      pomodoro/flipp_pomodoro_app.h
  6. 31 0
      pomodoro/flipp_pomodoro_app_i.h
  7. 5 0
      pomodoro/helpers/debug.h
  8. 49 0
      pomodoro/helpers/notifications.c
  9. 14 0
      pomodoro/helpers/notifications.h
  10. 21 0
      pomodoro/helpers/time.c
  11. 25 0
      pomodoro/helpers/time.h
  12. BIN
      pomodoro/images/flipp_pomodoro_focus_64/frame_00.png
  13. BIN
      pomodoro/images/flipp_pomodoro_focus_64/frame_01.png
  14. 1 0
      pomodoro/images/flipp_pomodoro_focus_64/frame_rate
  15. BIN
      pomodoro/images/flipp_pomodoro_learn_50x128.png
  16. BIN
      pomodoro/images/flipp_pomodoro_rest_64/frame_00.png
  17. BIN
      pomodoro/images/flipp_pomodoro_rest_64/frame_01.png
  18. 1 0
      pomodoro/images/flipp_pomodoro_rest_64/frame_rate
  19. 104 0
      pomodoro/modules/flipp_pomodoro.c
  20. 55 0
      pomodoro/modules/flipp_pomodoro.h
  21. 28 0
      pomodoro/modules/flipp_pomodoro_statistics.c
  22. 45 0
      pomodoro/modules/flipp_pomodoro_statistics.h
  23. 0 0
      pomodoro/scenes/.keep
  24. 2 0
      pomodoro/scenes/config/flipp_pomodoro_scene_config.h
  25. 30 0
      pomodoro/scenes/flipp_pomodoro_scene.c
  26. 29 0
      pomodoro/scenes/flipp_pomodoro_scene.h
  27. 67 0
      pomodoro/scenes/flipp_pomodoro_scene_info.c
  28. 179 0
      pomodoro/scenes/flipp_pomodoro_scene_timer.c
  29. 0 0
      pomodoro/views/.keep
  30. 167 0
      pomodoro/views/flipp_pomodoro_info_view.c
  31. 69 0
      pomodoro/views/flipp_pomodoro_info_view.h
  32. 327 0
      pomodoro/views/flipp_pomodoro_timer_view.c
  33. 24 0
      pomodoro/views/flipp_pomodoro_timer_view.h

+ 1 - 0
pomodoro/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/Th3Un1q3/flipp_pomodoro master flipp_pomodoro

+ 11 - 0
pomodoro/application.fam

@@ -0,0 +1,11 @@
+App(
+    appid="flipp_pomodoro",
+    name="Flipp Pomodoro",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="flipp_pomodoro_app",
+    requires=["gui", "notification", "dolphin"],
+    stack_size=1 * 1024,
+    fap_category="Productivity",
+    fap_icon_assets="images",
+    fap_icon="flipp_pomodoro_10.png",
+)

BIN
pomodoro/flipp_pomodoro_10.png


+ 127 - 0
pomodoro/flipp_pomodoro_app.c

@@ -0,0 +1,127 @@
+#include "flipp_pomodoro_app_i.h"
+
+#define TAG "FlippPomodoro"
+
+enum
+{
+    CustomEventConsumed = true,
+    CustomEventNotConsumed = false,
+};
+
+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 void flipp_pomodoro_app_tick_event_callback(void *ctx)
+{
+    furi_assert(ctx);
+    FlippPomodoroApp *app = ctx;
+
+    scene_manager_handle_custom_event(app->scene_manager, FlippPomodoroAppCustomEventTimerTick);
+};
+
+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);
+        view_dispatcher_send_custom_event(
+            app->view_dispatcher,
+            FlippPomodoroAppCustomEventStateUpdated);
+        return CustomEventConsumed;
+    case FlippPomodoroAppCustomEventStageComplete:
+        if (flipp_pomodoro__get_stage(app->state) == FlippPomodoroStageFocus)
+        {
+            // REGISTER a deed on work stage complete to get an acheivement
+            dolphin_deed(DolphinDeedPluginGameWin);
+            FURI_LOG_I(TAG, "Focus stage reward added");
+
+            flipp_pomodoro_statistics__increase_focus_stages_completed(app->statistics);
+        };
+
+        flipp_pomodoro__toggle_stage(app->state);
+        notification_message(app->notification_app, stage_start_notification_sequence_map[flipp_pomodoro__get_stage(app->state)]);
+        view_dispatcher_send_custom_event(
+            app->view_dispatcher,
+            FlippPomodoroAppCustomEventStateUpdated);
+        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();
+
+    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);
+
+    app->view_dispatcher = view_dispatcher_alloc();
+    app->statistics = flipp_pomodoro_statistics__new();
+
+    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);
+
+    app->timer_view = flipp_pomodoro_view_timer_alloc();
+    app->info_view = flipp_pomodoro_info_view_alloc();
+
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        FlippPomodoroAppViewTimer,
+        flipp_pomodoro_view_timer_get_view(app->timer_view));
+
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        FlippPomodoroAppViewInfo,
+        flipp_pomodoro_info_view_get_view(app->info_view));
+
+    scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneTimer);
+    FURI_LOG_I(TAG, "Alloc complete");
+    return app;
+};
+
+void flipp_pomodoro_app_free(FlippPomodoroApp *app)
+{
+    view_dispatcher_remove_view(app->view_dispatcher, FlippPomodoroAppViewTimer);
+    view_dispatcher_remove_view(app->view_dispatcher, FlippPomodoroAppViewInfo);
+    view_dispatcher_free(app->view_dispatcher);
+    scene_manager_free(app->scene_manager);
+    flipp_pomodoro_view_timer_free(app->timer_view);
+    flipp_pomodoro_info_view_free(app->info_view);
+    flipp_pomodoro_statistics__destroy(app->statistics);
+    flipp_pomodoro__destroy(app->state);
+    free(app);
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_NOTIFICATION);
+};
+
+int32_t flipp_pomodoro_app(void *p)
+{
+    UNUSED(p);
+    FURI_LOG_I(TAG, "Initial");
+    FlippPomodoroApp *app = flipp_pomodoro_app_alloc();
+
+    FURI_LOG_I(TAG, "Run deed added");
+    dolphin_deed(DolphinDeedPluginGameStart);
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    flipp_pomodoro_app_free(app);
+
+    return 0;
+};

+ 42 - 0
pomodoro/flipp_pomodoro_app.h

@@ -0,0 +1,42 @@
+#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 "views/flipp_pomodoro_info_view.h"
+
+#include "modules/flipp_pomodoro.h"
+#include "modules/flipp_pomodoro_statistics.h"
+
+typedef enum
+{
+    // Reserve first 100 events for button types and indexes, starting from 0
+    FlippPomodoroAppCustomEventStageSkip = 100,
+    FlippPomodoroAppCustomEventStageComplete, // By Expiration
+    FlippPomodoroAppCustomEventTimerTick,
+    FlippPomodoroAppCustomEventTimerAskHint,
+    FlippPomodoroAppCustomEventStateUpdated,
+    FlippPomodoroAppCustomEventResumeTimer,
+} FlippPomodoroAppCustomEvent;
+
+typedef struct
+{
+    SceneManager *scene_manager;
+    ViewDispatcher *view_dispatcher;
+    Gui *gui;
+    NotificationApp *notification_app;
+    FlippPomodoroTimerView *timer_view;
+    FlippPomodoroInfoView *info_view;
+    FlippPomodoroState *state;
+    FlippPomodoroStatistics *statistics;
+} FlippPomodoroApp;
+
+typedef enum
+{
+    FlippPomodoroAppViewTimer,
+    FlippPomodoroAppViewInfo,
+} FlippPomodoroAppView;

+ 31 - 0
pomodoro/flipp_pomodoro_app_i.h

@@ -0,0 +1,31 @@
+#pragma once
+
+// #define FURI_DEBUG 1
+
+/**
+ * Index of dependencies for the main app
+ */
+
+// Platform Imports
+
+#include <furi.h>
+#include <furi_hal.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
pomodoro/helpers/debug.h

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

+ 49 - 0
pomodoro/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,
+};

+ 14 - 0
pomodoro/helpers/notifications.h

@@ -0,0 +1,14 @@
+#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[] = {
+    [FlippPomodoroStageFocus] = &work_start_notification,
+    [FlippPomodoroStageRest] = &rest_start_notification,
+    [FlippPomodoroStageLongBreak] = &rest_start_notification,
+};

+ 21 - 0
pomodoro/helpers/time.c

@@ -0,0 +1,21 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include "time.h"
+
+const int TIME_SECONDS_IN_MINUTE = 60;
+const int TIME_MINUTES_IN_HOUR = 60;
+
+uint32_t time_now()
+{
+    return furi_hal_rtc_get_timestamp();
+};
+
+TimeDifference time_difference_seconds(uint32_t begin, uint32_t end)
+{
+    const uint32_t duration_seconds = end - begin;
+
+    uint32_t minutes = (duration_seconds / TIME_MINUTES_IN_HOUR) % TIME_MINUTES_IN_HOUR;
+    uint32_t seconds = duration_seconds % TIME_SECONDS_IN_MINUTE;
+
+    return (TimeDifference){.total_seconds = duration_seconds, .minutes = minutes, .seconds = seconds};
+};

+ 25 - 0
pomodoro/helpers/time.h

@@ -0,0 +1,25 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+
+extern const int TIME_SECONDS_IN_MINUTE;
+extern const int TIME_MINUTES_IN_HOUR;
+
+/// @brief Container for a time period
+typedef struct
+{
+    uint8_t seconds;
+    uint8_t minutes;
+    uint32_t total_seconds;
+} TimeDifference;
+
+/// @brief Time by the moment of calling
+/// @return A timestamp(seconds percision)
+uint32_t time_now();
+
+/// @brief Calculates difference between two provided timestamps
+/// @param begin - start timestamp of the period
+/// @param end - end timestamp of the period to measure
+/// @return TimeDifference struct
+TimeDifference time_difference_seconds(uint32_t begin, uint32_t end);

BIN
pomodoro/images/flipp_pomodoro_focus_64/frame_00.png


BIN
pomodoro/images/flipp_pomodoro_focus_64/frame_01.png


+ 1 - 0
pomodoro/images/flipp_pomodoro_focus_64/frame_rate

@@ -0,0 +1 @@
+1

BIN
pomodoro/images/flipp_pomodoro_learn_50x128.png


BIN
pomodoro/images/flipp_pomodoro_rest_64/frame_00.png


BIN
pomodoro/images/flipp_pomodoro_rest_64/frame_01.png


+ 1 - 0
pomodoro/images/flipp_pomodoro_rest_64/frame_rate

@@ -0,0 +1 @@
+1

+ 104 - 0
pomodoro/modules/flipp_pomodoro.c

@@ -0,0 +1,104 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include "../helpers/time.h"
+#include "flipp_pomodoro.h"
+
+PomodoroStage stages_sequence[] = {
+    FlippPomodoroStageFocus,
+    FlippPomodoroStageRest,
+
+    FlippPomodoroStageFocus,
+    FlippPomodoroStageRest,
+
+    FlippPomodoroStageFocus,
+    FlippPomodoroStageRest,
+
+    FlippPomodoroStageFocus,
+    FlippPomodoroStageLongBreak,
+};
+
+char *current_stage_label[] = {
+    [FlippPomodoroStageFocus] = "Focusing...",
+    [FlippPomodoroStageRest] = "Short Break...",
+    [FlippPomodoroStageLongBreak] = "Long Break...",
+};
+
+char *next_stage_label[] = {
+    [FlippPomodoroStageFocus] = "Focus",
+    [FlippPomodoroStageRest] = "Short Break",
+    [FlippPomodoroStageLongBreak] = "Long Break",
+};
+
+PomodoroStage flipp_pomodoro__stage_by_index(int index) {
+    const int one_loop_size = sizeof(stages_sequence);
+    return stages_sequence[index % one_loop_size];
+}
+
+void flipp_pomodoro__toggle_stage(FlippPomodoroState *state)
+{
+    furi_assert(state);
+    state->current_stage_index = state->current_stage_index + 1;
+    state->started_at_timestamp = time_now();
+};
+
+PomodoroStage flipp_pomodoro__get_stage(FlippPomodoroState *state)
+{
+    furi_assert(state);
+    return flipp_pomodoro__stage_by_index(state->current_stage_index);
+};
+
+char *flipp_pomodoro__current_stage_label(FlippPomodoroState *state)
+{
+    furi_assert(state);
+    return current_stage_label[flipp_pomodoro__get_stage(state)];
+};
+
+char *flipp_pomodoro__next_stage_label(FlippPomodoroState *state)
+{
+    furi_assert(state);
+    return next_stage_label[flipp_pomodoro__stage_by_index(state->current_stage_index + 1)];
+};
+
+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[] = {
+        [FlippPomodoroStageFocus] = 25 * TIME_SECONDS_IN_MINUTE,
+        [FlippPomodoroStageRest] = 5 * TIME_SECONDS_IN_MINUTE,
+        [FlippPomodoroStageLongBreak] = 30 * TIME_SECONDS_IN_MINUTE,
+    };
+
+    return stage_duration_seconds_map[flipp_pomodoro__get_stage(state)];
+};
+
+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->current_stage_index = 0;
+    return state;
+};

+ 55 - 0
pomodoro/modules/flipp_pomodoro.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include <furi_hal.h>
+#include "../helpers/time.h"
+
+/// @brief Options of pomodoro stages
+typedef enum
+{
+    FlippPomodoroStageFocus,
+    FlippPomodoroStageRest,
+    FlippPomodoroStageLongBreak,
+} PomodoroStage;
+
+/// @brief State of the pomodoro timer
+typedef struct
+{
+    uint8_t current_stage_index;
+    uint32_t started_at_timestamp;
+} FlippPomodoroState;
+
+/// @brief Generates initial state
+/// @returns A new pre-populated state for pomodoro timer
+FlippPomodoroState *flipp_pomodoro__new();
+
+/// @brief Extract current stage of pomodoro
+/// @param state - pointer to the state of pomorodo
+/// @returns Current stage value
+PomodoroStage flipp_pomodoro__get_stage(FlippPomodoroState *state);
+
+/// @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 currently active stage
+/// @param state - pointer to the state of pomorodo
+/// @returns A string that explains current stage
+char *flipp_pomodoro__current_stage_label(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);

+ 28 - 0
pomodoro/modules/flipp_pomodoro_statistics.c

@@ -0,0 +1,28 @@
+#include "flipp_pomodoro_statistics.h"
+
+FlippPomodoroStatistics *flipp_pomodoro_statistics__new()
+{
+    FlippPomodoroStatistics *statistics = malloc(sizeof(FlippPomodoroStatistics));
+
+    statistics->focus_stages_completed = 0;
+
+    return statistics;
+}
+
+// Return the number of completed focus stages
+uint8_t flipp_pomodoro_statistics__get_focus_stages_completed(FlippPomodoroStatistics *statistics)
+{
+    return statistics->focus_stages_completed;
+}
+
+// Increase the number of completed focus stages by one
+void flipp_pomodoro_statistics__increase_focus_stages_completed(FlippPomodoroStatistics *statistics)
+{
+    statistics->focus_stages_completed++;
+}
+
+void flipp_pomodoro_statistics__destroy(FlippPomodoroStatistics *statistics)
+{
+    furi_assert(statistics);
+    free(statistics);
+};

+ 45 - 0
pomodoro/modules/flipp_pomodoro_statistics.h

@@ -0,0 +1,45 @@
+#pragma once
+#include <furi_hal.h>
+
+/** @brief FlippPomodoroStatistics structure
+ *
+ *  This structure is used to keep track of completed focus stages.
+ */
+typedef struct
+{
+    uint8_t focus_stages_completed;
+} FlippPomodoroStatistics;
+
+/** @brief Allocate and initialize a new FlippPomodoroStatistics
+ *
+ *  This function allocates a new FlippPomodoroStatistics structure, initializes its members
+ *  and returns a pointer to it.
+ *
+ *  @return A pointer to a new FlippPomodoroStatistics structure
+ */
+FlippPomodoroStatistics *flipp_pomodoro_statistics__new();
+
+/** @brief Get the number of completed focus stages
+ *
+ *  This function retrieves the number of completed focus stages in a FlippPomodoroStatistics structure.
+ *
+ *  @param statistics A pointer to a FlippPomodoroStatistics structure
+ *  @return The number of completed focus stages
+ */
+uint8_t flipp_pomodoro_statistics__get_focus_stages_completed(FlippPomodoroStatistics *statistics);
+
+/** @brief Increase the number of completed focus stages
+ *
+ *  This function increases the count of the completed focus stages by one in a FlippPomodoroStatistics structure.
+ *
+ *  @param statistics A pointer to a FlippPomodoroStatistics structure
+ */
+void flipp_pomodoro_statistics__increase_focus_stages_completed(FlippPomodoroStatistics *statistics);
+
+/** @brief Free a FlippPomodoroStatistics structure
+ *
+ *  This function frees the memory used by a FlippPomodoroStatistics structure.
+ *
+ *  @param statistics A pointer to a FlippPomodoroStatistics structure
+ */
+void flipp_pomodoro_statistics__destroy(FlippPomodoroStatistics *state);

+ 0 - 0
pomodoro/scenes/.keep


+ 2 - 0
pomodoro/scenes/config/flipp_pomodoro_scene_config.h

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

+ 30 - 0
pomodoro/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* ctx, 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* ctx) = {
+#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,
+};

+ 29 - 0
pomodoro/scenes/flipp_pomodoro_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+#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 *ctx, 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 *ctx);
+#include "config/flipp_pomodoro_scene_config.h"
+#undef ADD_SCENE

+ 67 - 0
pomodoro/scenes/flipp_pomodoro_scene_info.c

@@ -0,0 +1,67 @@
+#include <furi.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include "flipp_pomodoro_scene.h"
+#include "../flipp_pomodoro_app.h"
+#include "../views/flipp_pomodoro_info_view.h"
+
+enum
+{
+    SceneEventConusmed = true,
+    SceneEventNotConusmed = false
+};
+
+void flipp_pomodoro_scene_info_on_back_to_timer(void *ctx)
+{
+    furi_assert(ctx);
+    FlippPomodoroApp *app = ctx;
+
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher,
+        FlippPomodoroAppCustomEventResumeTimer);
+};
+
+void flipp_pomodoro_scene_info_on_enter(void *ctx)
+{
+    furi_assert(ctx);
+    FlippPomodoroApp *app = ctx;
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlippPomodoroAppViewInfo);
+    flipp_pomodoro_info_view_set_pomodoros_completed(
+        flipp_pomodoro_info_view_get_view(app->info_view),
+        flipp_pomodoro_statistics__get_focus_stages_completed(app->statistics));
+    flipp_pomodoro_info_view_set_mode(flipp_pomodoro_info_view_get_view(app->info_view), FlippPomodoroInfoViewModeStats);
+    flipp_pomodoro_info_view_set_resume_timer_cb(app->info_view, flipp_pomodoro_scene_info_on_back_to_timer, app);
+};
+
+void flipp_pomodoro_scene_info_handle_custom_event(FlippPomodoroApp *app, FlippPomodoroAppCustomEvent custom_event)
+{
+    if (custom_event == FlippPomodoroAppCustomEventResumeTimer)
+    {
+        scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneTimer);
+    }
+};
+
+bool flipp_pomodoro_scene_info_on_event(void *ctx, SceneManagerEvent event)
+{
+    furi_assert(ctx);
+    FlippPomodoroApp *app = ctx;
+
+    switch (event.type)
+    {
+    case SceneManagerEventTypeBack:
+        view_dispatcher_stop(app->view_dispatcher);
+        return SceneEventConusmed;
+    case SceneManagerEventTypeCustom:
+        flipp_pomodoro_scene_info_handle_custom_event(app, event.event);
+        return SceneEventConusmed;
+    default:
+        break;
+    };
+    return SceneEventNotConusmed;
+};
+
+void flipp_pomodoro_scene_info_on_exit(void *ctx)
+{
+    UNUSED(ctx);
+};

+ 179 - 0
pomodoro/scenes/flipp_pomodoro_scene_timer.c

@@ -0,0 +1,179 @@
+#include <furi.h>
+#include <gui/scene_manager.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include "flipp_pomodoro_scene.h"
+#include "../flipp_pomodoro_app.h"
+#include "../views/flipp_pomodoro_timer_view.h"
+
+enum
+{
+    SceneEventConusmed = true,
+    SceneEventNotConusmed = false
+};
+
+static char *work_hints[] = {
+    "Can you explain the problem as if I'm five?",
+    "Expected output vs. reality: what's the difference?",
+    "Ever thought of slicing the problem into bite-sized pieces?",
+    "What's the story when you walk through the code?",
+    "Any error messages gossiping about the issue?",
+    "What tricks have you tried to fix this?",
+    "Did you test the code, or just hoping for the best?",
+    "How's this code mingling with the rest of the app?",
+    "Any sneaky side effects causing mischief?",
+    "What are you assuming, and is it safe to do so?",
+    "Did you remember to invite all the edge cases to the party?",
+    "What happens in the isolation chamber (running code separately)?",
+    "Can you make the issue appear on command?",
+    "What's the scene at the crime spot when the error occurs?",
+    "Did you seek wisdom from the grand oracle (Google)?",
+    "What if you take a different path to solve this?",
+    "Did you take a coffee break to reboot your brain?"};
+
+static char *break_hints[] = {
+    "Time to stretch! Remember, your body isn't made of code.",
+    "Hydrate or diedrate! Grab a glass of water.",
+    "Blink! Your eyes need a break too.",
+    "How about a quick dance-off with your shadow?",
+    "Ever tried chair yoga? Now's the time!",
+    "Time for a quick peek out the window. The outside world still exists!",
+    "Quick, think about kittens! Or puppies! Or baby turtles!",
+    "Time for a laugh. Look up a joke or two!",
+    "Sing a song. Bonus points for making up your own lyrics.",
+    "Do a quick tidy-up. A clean space is a happy space!",
+    "Time to play 'air' musical instrument for a minute.",
+    "How about a quick doodle? Unleash your inner Picasso!",
+    "Practice your superhero pose. Feel the power surge!",
+    "Quick, tell yourself a joke. Don't worry, I won't judge.",
+    "Time to practice your mime skills. Stuck in a box, anyone?",
+    "Ever tried juggling? Now's your chance!",
+    "Do a quick self high-five, you're doing great!"};
+
+static char *random_string_of_list(char **hints, size_t num_hints)
+{
+    int random_index = rand() % num_hints;
+    return hints[random_index];
+}
+
+void flipp_pomodoro_scene_timer_sync_view_state(void *ctx)
+{
+    furi_assert(ctx);
+
+    FlippPomodoroApp *app = ctx;
+
+    flipp_pomodoro_view_timer_set_state(
+        flipp_pomodoro_view_timer_get_view(app->timer_view),
+        app->state);
+};
+
+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_ask_hint(void *ctx)
+{
+    FlippPomodoroApp *app = ctx;
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher,
+        FlippPomodoroAppCustomEventTimerAskHint);
+}
+
+void flipp_pomodoro_scene_timer_on_enter(void *ctx)
+{
+    furi_assert(ctx);
+
+    FlippPomodoroApp *app = ctx;
+
+    if (flipp_pomodoro__is_stage_expired(app->state))
+    {
+        flipp_pomodoro__destroy(app->state);
+        app->state = flipp_pomodoro__new();
+    }
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlippPomodoroAppViewTimer);
+    flipp_pomodoro_scene_timer_sync_view_state(app);
+
+    flipp_pomodoro_view_timer_set_callback_context(app->timer_view, app);
+
+    flipp_pomodoro_view_timer_set_on_ok_cb(
+        app->timer_view,
+        flipp_pomodoro_scene_timer_on_ask_hint);
+
+    flipp_pomodoro_view_timer_set_on_right_cb(
+        app->timer_view,
+        flipp_pomodoro_scene_timer_on_next_stage);
+};
+
+char *flipp_pomodoro_scene_timer_get_contextual_hint(FlippPomodoroApp *app)
+{
+    switch (flipp_pomodoro__get_stage(app->state))
+    {
+    case FlippPomodoroStageFocus:
+        return random_string_of_list(work_hints, sizeof(work_hints) / sizeof(work_hints[0]));
+    case FlippPomodoroStageRest:
+    case FlippPomodoroStageLongBreak:
+        return random_string_of_list(break_hints, sizeof(break_hints) / sizeof(break_hints[0]));
+    default:
+        return "What's up?";
+    }
+}
+
+void flipp_pomodoro_scene_timer_handle_custom_event(FlippPomodoroApp *app, FlippPomodoroAppCustomEvent custom_event)
+{
+    switch (custom_event)
+    {
+    case FlippPomodoroAppCustomEventTimerTick:
+        if (flipp_pomodoro__is_stage_expired(app->state))
+        {
+            view_dispatcher_send_custom_event(
+                app->view_dispatcher,
+                FlippPomodoroAppCustomEventStageComplete);
+        }
+        break;
+    case FlippPomodoroAppCustomEventStateUpdated:
+        flipp_pomodoro_scene_timer_sync_view_state(app);
+        break;
+    case FlippPomodoroAppCustomEventTimerAskHint:
+        flipp_pomodoro_view_timer_display_hint(
+            flipp_pomodoro_view_timer_get_view(app->timer_view),
+            flipp_pomodoro_scene_timer_get_contextual_hint(app));
+        break;
+    default:
+        // optional: code to be executed if custom_event doesn't match any cases
+        break;
+    }
+};
+
+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:
+        scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneInfo);
+        return SceneEventConusmed;
+    default:
+        break;
+    };
+    return SceneEventNotConusmed;
+};
+
+void flipp_pomodoro_scene_timer_on_exit(void *ctx)
+{
+    UNUSED(ctx);
+};

+ 0 - 0
pomodoro/views/.keep


+ 167 - 0
pomodoro/views/flipp_pomodoro_info_view.c

@@ -0,0 +1,167 @@
+
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <gui/view.h>
+#include "flipp_pomodoro_info_view.h"
+// Auto-compiled icons
+#include "flipp_pomodoro_icons.h"
+
+enum
+{
+    ViewInputConsumed = true,
+    ViewInputNotConusmed = false,
+};
+
+struct FlippPomodoroInfoView
+{
+    View *view;
+    FlippPomodoroInfoViewUserActionCb resume_timer_cb;
+    void *user_action_cb_ctx;
+};
+
+typedef struct
+{
+    uint8_t pomodoros_completed;
+    FlippPomodoroInfoViewMode mode;
+} FlippPomodoroInfoViewModel;
+
+static void flipp_pomodoro_info_view_draw_statistics(Canvas *canvas, FlippPomodoroInfoViewModel *model)
+{
+    FuriString *stats_string = furi_string_alloc();
+
+    furi_string_printf(stats_string, "So Long,\nand Thanks for All the Focus...\nand for completing\n\e#%i\e# pomodoro(s)", model->pomodoros_completed);
+    const char *stats_string_formatted = furi_string_get_cstr(stats_string);
+
+    elements_text_box(
+        canvas,
+        0,
+        0,
+        canvas_width(canvas),
+        canvas_height(canvas) - 10,
+        AlignCenter,
+        AlignCenter,
+        stats_string_formatted,
+        true);
+
+    furi_string_free(stats_string);
+
+    elements_button_left(canvas, "Guide");
+}
+
+static void flipp_pomodoro_info_view_draw_about(Canvas *canvas, FlippPomodoroInfoViewModel *model)
+{
+    UNUSED(model);
+    canvas_draw_icon(canvas, 0, 0, &I_flipp_pomodoro_learn_50x128);
+    elements_button_left(canvas, "Stats");
+}
+
+static void flipp_pomodoro_info_view_draw_callback(Canvas *canvas, void *_model)
+{
+    if (!_model)
+    {
+        return;
+    };
+
+    FlippPomodoroInfoViewModel *model = _model;
+
+    canvas_clear(canvas);
+
+    if (model->mode == FlippPomodoroInfoViewModeStats)
+    {
+        flipp_pomodoro_info_view_draw_statistics(canvas, model);
+    }
+    else
+    {
+        flipp_pomodoro_info_view_draw_about(canvas, model);
+    }
+
+    elements_button_right(canvas, "Resume");
+}
+
+void flipp_pomodoro_info_view_set_mode(View *view, FlippPomodoroInfoViewMode desired_mode)
+{
+    with_view_model(
+        view,
+        FlippPomodoroInfoViewModel * model,
+        {
+            model->mode = desired_mode;
+        },
+        false);
+}
+
+void flipp_pomodoro_info_view_toggle_mode(FlippPomodoroInfoView *info_view)
+{
+    with_view_model(
+        flipp_pomodoro_info_view_get_view(info_view),
+        FlippPomodoroInfoViewModel * model,
+        {
+            flipp_pomodoro_info_view_set_mode(
+                flipp_pomodoro_info_view_get_view(info_view),
+                (model->mode == FlippPomodoroInfoViewModeStats) ? FlippPomodoroInfoViewModeAbout : FlippPomodoroInfoViewModeStats);
+        },
+        true);
+}
+
+bool flipp_pomodoro_info_view_input_callback(InputEvent *event, void *ctx)
+{
+    FlippPomodoroInfoView *info_view = ctx;
+
+    if (event->type == InputTypePress)
+    {
+        if (event->key == InputKeyRight && info_view->resume_timer_cb != NULL)
+        {
+            info_view->resume_timer_cb(info_view->user_action_cb_ctx);
+            return ViewInputConsumed;
+        }
+        else if (event->key == InputKeyLeft)
+        {
+            flipp_pomodoro_info_view_toggle_mode(info_view);
+            return ViewInputConsumed;
+        }
+    }
+
+    return ViewInputNotConusmed;
+}
+
+FlippPomodoroInfoView *flipp_pomodoro_info_view_alloc()
+{
+    FlippPomodoroInfoView *info_view = malloc(sizeof(FlippPomodoroInfoView));
+    info_view->view = view_alloc();
+
+    view_allocate_model(flipp_pomodoro_info_view_get_view(info_view), ViewModelTypeLockFree, sizeof(FlippPomodoroInfoViewModel));
+    view_set_context(flipp_pomodoro_info_view_get_view(info_view), info_view);
+    view_set_draw_callback(flipp_pomodoro_info_view_get_view(info_view), flipp_pomodoro_info_view_draw_callback);
+    view_set_input_callback(flipp_pomodoro_info_view_get_view(info_view), flipp_pomodoro_info_view_input_callback);
+
+    return info_view;
+}
+
+View *flipp_pomodoro_info_view_get_view(FlippPomodoroInfoView *info_view)
+{
+    return info_view->view;
+}
+
+void flipp_pomodoro_info_view_free(FlippPomodoroInfoView *info_view)
+{
+    furi_assert(info_view);
+    view_free(info_view->view);
+    free(info_view);
+}
+
+void flipp_pomodoro_info_view_set_pomodoros_completed(View *view, uint8_t pomodoros_completed)
+{
+    with_view_model(
+        view,
+        FlippPomodoroInfoViewModel * model,
+        {
+            model->pomodoros_completed = pomodoros_completed;
+        },
+        false);
+}
+
+void flipp_pomodoro_info_view_set_resume_timer_cb(FlippPomodoroInfoView *info_view, FlippPomodoroInfoViewUserActionCb user_action_cb, void *user_action_cb_ctx)
+{
+    info_view->resume_timer_cb = user_action_cb;
+    info_view->user_action_cb_ctx = user_action_cb_ctx;
+}

+ 69 - 0
pomodoro/views/flipp_pomodoro_info_view.h

@@ -0,0 +1,69 @@
+#pragma once
+
+#include <gui/view.h>
+
+/** @brief Mode types for FlippPomodoroInfoView
+ *
+ *  These are the modes that can be used in the FlippPomodoroInfoView
+ */
+typedef enum
+{
+    FlippPomodoroInfoViewModeStats,
+    FlippPomodoroInfoViewModeAbout,
+} FlippPomodoroInfoViewMode;
+
+/** @brief Forward declaration of the FlippPomodoroInfoView struct */
+typedef struct FlippPomodoroInfoView FlippPomodoroInfoView;
+
+/** @brief User action callback function type
+ *
+ *  Callback functions of this type are called when a user action is performed.
+ */
+typedef void (*FlippPomodoroInfoViewUserActionCb)(void *ctx);
+
+/** @brief Allocate a new FlippPomodoroInfoView
+ *
+ *  Allocates a new FlippPomodoroInfoView and returns a pointer to it.
+ *  @return A pointer to a new FlippPomodoroInfoView
+ */
+FlippPomodoroInfoView *flipp_pomodoro_info_view_alloc();
+
+/** @brief Get the view from a FlippPomodoroInfoView
+ *
+ *  Returns a pointer to the view associated with a FlippPomodoroInfoView.
+ *  @param info_view A pointer to a FlippPomodoroInfoView
+ *  @return A pointer to the view of the FlippPomodoroInfoView
+ */
+View *flipp_pomodoro_info_view_get_view(FlippPomodoroInfoView *info_view);
+
+/** @brief Free a FlippPomodoroInfoView
+ *
+ *  Frees the memory used by a FlippPomodoroInfoView.
+ *  @param info_view A pointer to a FlippPomodoroInfoView
+ */
+void flipp_pomodoro_info_view_free(FlippPomodoroInfoView *info_view);
+
+/** @brief Set the number of completed pomodoros in the view
+ *
+ *  Sets the number of completed pomodoros that should be displayed in the view.
+ *  @param info_view A pointer to the view
+ *  @param pomodoros_completed The number of completed pomodoros
+ */
+void flipp_pomodoro_info_view_set_pomodoros_completed(View *info_view, uint8_t pomodoros_completed);
+
+/** @brief Set the callback function to be called when the timer should be resumed
+ *
+ *  Sets the callback function that will be called when the timer should be resumed.
+ *  @param info_view A pointer to the FlippPomodoroInfoView
+ *  @param user_action_cb The callback function
+ *  @param user_action_cb_ctx The context to be passed to the callback function
+ */
+void flipp_pomodoro_info_view_set_resume_timer_cb(FlippPomodoroInfoView *info_view, FlippPomodoroInfoViewUserActionCb user_action_cb, void *user_action_cb_ctx);
+
+/** @brief Set the mode of the view
+ *
+ *  Sets the mode that should be used in the view.
+ *  @param view A pointer to the view
+ *  @param desired_mode The desired mode
+ */
+void flipp_pomodoro_info_view_set_mode(View *view, FlippPomodoroInfoViewMode desired_mode);

+ 327 - 0
pomodoro/views/flipp_pomodoro_timer_view.c

@@ -0,0 +1,327 @@
+#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;
+    FlippPomodoroTimerViewInputCb ok_cb;
+    void *callback_context;
+};
+
+typedef struct
+{
+    IconAnimation *icon;
+    FlippPomodoroState *state;
+    size_t scroll_counter;
+    char *current_hint;
+} FlippPomodoroTimerViewModel;
+
+static const Icon *stage_background_image[] = {
+    [FlippPomodoroStageFocus] = &A_flipp_pomodoro_focus_64,
+    [FlippPomodoroStageRest] = &A_flipp_pomodoro_rest_64,
+    [FlippPomodoroStageLongBreak] = &A_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 = 15;
+
+    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 draw_str_with_drop_shadow(
+    Canvas *canvas, uint8_t x,
+    uint8_t y,
+    Align horizontal,
+    Align vertical,
+    const char *str)
+{
+    canvas_set_color(canvas, ColorWhite);
+    for (int x_off = -2; x_off <= 2; x_off++)
+    {
+        for (int y_off = -2; y_off <= 2; y_off++)
+        {
+            canvas_draw_str_aligned(
+                canvas,
+                x + x_off,
+                y + y_off,
+                horizontal,
+                vertical,
+                str);
+        }
+    }
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_str_aligned(
+        canvas,
+        x,
+        y,
+        horizontal,
+        vertical,
+        str);
+}
+
+static void flipp_pomodoro_view_timer_draw_current_stage_label(Canvas *canvas, FlippPomodoroState *state)
+{
+    canvas_set_font(canvas, FontPrimary);
+    draw_str_with_drop_shadow(
+        canvas,
+        canvas_width(canvas),
+        0,
+        AlignRight,
+        AlignTop,
+        flipp_pomodoro__current_stage_label(state));
+}
+
+static void flipp_pomodoro_view_timer_draw_hint(Canvas *canvas, FlippPomodoroTimerViewModel *model)
+{
+    size_t MAX_SCROLL_COUNTER = 300;
+    uint8_t SCROLL_DELAY_FRAMES = 3;
+
+    if (model->scroll_counter >= MAX_SCROLL_COUNTER || model->current_hint == NULL)
+    {
+        return;
+    }
+
+    uint8_t hint_width = 90;
+    uint8_t hint_height = 18;
+
+    uint8_t hint_x = canvas_width(canvas) - hint_width - 6;
+    uint8_t hint_y = 35;
+
+    FuriString *displayed_hint_string = furi_string_alloc();
+
+    furi_string_printf(
+        displayed_hint_string,
+        "%s",
+        model->current_hint);
+
+    size_t perfect_duration = furi_string_size(displayed_hint_string) * 1.5;
+
+    if (model->scroll_counter > perfect_duration)
+    {
+        model->scroll_counter = MAX_SCROLL_COUNTER;
+        furi_string_free(displayed_hint_string);
+        return;
+    }
+
+    size_t scroll_offset = (model->scroll_counter < SCROLL_DELAY_FRAMES) ? 0 : model->scroll_counter - SCROLL_DELAY_FRAMES;
+
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_box(canvas, hint_x, hint_y, hint_width + 3, hint_height);
+    canvas_set_color(canvas, ColorBlack);
+
+    elements_bubble(canvas, hint_x, hint_y, hint_width, hint_height);
+
+    elements_scrollable_text_line(
+        canvas,
+        hint_x + 6,
+        hint_y + 12,
+        hint_width - 4,
+        displayed_hint_string,
+        scroll_offset,
+        true);
+    furi_string_free(displayed_hint_string);
+    model->scroll_counter++;
+}
+
+static void flipp_pomodoro_view_timer_draw_callback(Canvas *canvas, void *_model)
+{
+    if (!_model)
+    {
+        return;
+    };
+
+    FlippPomodoroTimerViewModel *model = _model;
+
+    canvas_clear(canvas);
+    if (model->icon)
+    {
+        canvas_draw_icon_animation(canvas, 0, 0, model->icon);
+    }
+
+    flipp_pomodoro_view_timer_draw_countdown(
+        canvas,
+        flipp_pomodoro__stage_remaining_duration(model->state));
+
+    flipp_pomodoro_view_timer_draw_current_stage_label(canvas, model->state);
+
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_set_font(canvas, FontSecondary);
+    elements_button_right(canvas, flipp_pomodoro__next_stage_label(model->state));
+    flipp_pomodoro_view_timer_draw_hint(canvas, model);
+};
+
+bool flipp_pomodoro_view_timer_input_callback(InputEvent *event, void *ctx)
+{
+    furi_assert(ctx);
+    furi_assert(event);
+    FlippPomodoroTimerView *timer = ctx;
+
+    const bool is_press_event = event->type == InputTypePress;
+
+    if (!is_press_event)
+    {
+        return ViewInputNotConusmed;
+    }
+
+    switch (event->key)
+    {
+    case InputKeyRight:
+        timer->right_cb(timer->callback_context);
+        return ViewInputConsumed;
+    case InputKeyOk:
+        timer->ok_cb(timer->callback_context);
+        return ViewInputConsumed;
+    default:
+        return ViewInputNotConusmed;
+    }
+};
+
+View *flipp_pomodoro_view_timer_get_view(FlippPomodoroTimerView *timer)
+{
+    furi_assert(timer);
+    return timer->view;
+};
+
+void flipp_pomodoro_view_timer_display_hint(View *view, char *hint)
+{
+    with_view_model(
+        view,
+        FlippPomodoroTimerViewModel * model,
+        {
+            model->scroll_counter = 0;
+            model->current_hint = hint;
+        },
+        true);
+}
+
+void flipp_pomodoro_view_timer_assign_animation(View *view)
+{
+    with_view_model(
+        view,
+        FlippPomodoroTimerViewModel * model,
+        {
+            if (model->icon)
+            {
+                icon_animation_free(model->icon);
+            }
+            model->icon = icon_animation_alloc(
+                stage_background_image[flipp_pomodoro__get_stage(model->state)]);
+            view_tie_icon_animation(view, model->icon);
+            icon_animation_start(model->icon);
+        },
+        true);
+}
+
+FlippPomodoroTimerView *flipp_pomodoro_view_timer_alloc()
+{
+    FlippPomodoroTimerView *timer = malloc(sizeof(FlippPomodoroTimerView));
+    timer->view = view_alloc();
+
+    view_allocate_model(flipp_pomodoro_view_timer_get_view(timer), 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);
+
+    with_view_model(
+        flipp_pomodoro_view_timer_get_view(timer),
+        FlippPomodoroTimerViewModel * model,
+        {
+            model->scroll_counter = 0;
+        },
+        false);
+
+    return timer;
+};
+
+void flipp_pomodoro_view_timer_set_callback_context(FlippPomodoroTimerView *timer, void *callback_ctx)
+{
+    furi_assert(timer);
+    furi_assert(callback_ctx);
+    timer->callback_context = callback_ctx;
+}
+
+void flipp_pomodoro_view_timer_set_on_right_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb right_cb)
+{
+    furi_assert(timer);
+    furi_assert(right_cb);
+    timer->right_cb = right_cb;
+};
+
+void flipp_pomodoro_view_timer_set_on_ok_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb ok_kb)
+{
+    furi_assert(ok_kb);
+    furi_assert(timer);
+    timer->ok_cb = ok_kb;
+}
+
+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;
+            model->current_hint = NULL;
+        },
+        false);
+    flipp_pomodoro_view_timer_assign_animation(view);
+};
+
+void flipp_pomodoro_view_timer_free(FlippPomodoroTimerView *timer)
+{
+    furi_assert(timer);
+    with_view_model(
+        timer->view,
+        FlippPomodoroTimerViewModel * model,
+        {
+            icon_animation_free(model->icon);
+        },
+        false);
+    view_free(timer->view);
+
+    free(timer);
+};

+ 24 - 0
pomodoro/views/flipp_pomodoro_timer_view.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../modules/flipp_pomodoro.h"
+
+typedef struct FlippPomodoroTimerView FlippPomodoroTimerView;
+
+typedef void (*FlippPomodoroTimerViewInputCb)(void *ctx);
+
+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_callback_context(FlippPomodoroTimerView *timer, void *callback_ctx);
+
+void flipp_pomodoro_view_timer_set_on_right_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb right_cb);
+
+void flipp_pomodoro_view_timer_set_on_ok_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb ok_cb);
+
+void flipp_pomodoro_view_timer_display_hint(View *view, char *hint);