瀏覽代碼

Add pomodoro from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: pomodoro
git-subtree-mainline: 1ad06cacbed8a5d721b0297f67f81915ed9c31cb
git-subtree-split: b4087037bc46fba226c789d77b701ba43adbbff0
Willy-JL 2 年之前
父節點
當前提交
09c58ac21c
共有 35 個文件被更改,包括 1358 次插入0 次删除
  1. 1 0
      pomodoro/.gitsubtree
  2. 15 0
      pomodoro/application.fam
  3. 二進制
      pomodoro/flipp_pomodoro_10.png
  4. 121 0
      pomodoro/flipp_pomodoro_app.c
  5. 39 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. 20 0
      pomodoro/helpers/time.c
  11. 24 0
      pomodoro/helpers/time.h
  12. 二進制
      pomodoro/images/flipp_pomodoro_focus_64/frame_00.png
  13. 二進制
      pomodoro/images/flipp_pomodoro_focus_64/frame_01.png
  14. 1 0
      pomodoro/images/flipp_pomodoro_focus_64/frame_rate
  15. 二進制
      pomodoro/images/flipp_pomodoro_learn_50x128.png
  16. 二進制
      pomodoro/images/flipp_pomodoro_rest_64/frame_00.png
  17. 二進制
      pomodoro/images/flipp_pomodoro_rest_64/frame_01.png
  18. 1 0
      pomodoro/images/flipp_pomodoro_rest_64/frame_rate
  19. 二進制
      pomodoro/img/1.png
  20. 二進制
      pomodoro/img/2.png
  21. 94 0
      pomodoro/modules/flipp_pomodoro.c
  22. 53 0
      pomodoro/modules/flipp_pomodoro.h
  23. 26 0
      pomodoro/modules/flipp_pomodoro_statistics.c
  24. 45 0
      pomodoro/modules/flipp_pomodoro_statistics.h
  25. 0 0
      pomodoro/scenes/.keep
  26. 2 0
      pomodoro/scenes/config/flipp_pomodoro_scene_config.h
  27. 30 0
      pomodoro/scenes/flipp_pomodoro_scene.c
  28. 28 0
      pomodoro/scenes/flipp_pomodoro_scene.h
  29. 59 0
      pomodoro/scenes/flipp_pomodoro_scene_info.c
  30. 154 0
      pomodoro/scenes/flipp_pomodoro_scene_timer.c
  31. 0 0
      pomodoro/views/.keep
  32. 152 0
      pomodoro/views/flipp_pomodoro_info_view.c
  33. 71 0
      pomodoro/views/flipp_pomodoro_info_view.h
  34. 293 0
      pomodoro/views/flipp_pomodoro_timer_view.c
  35. 30 0
      pomodoro/views/flipp_pomodoro_timer_view.h

+ 1 - 0
pomodoro/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev apps_source_code/flipp_pomodoro

+ 15 - 0
pomodoro/application.fam

@@ -0,0 +1,15 @@
+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="Tools",
+    fap_icon_assets="images",
+    fap_icon="flipp_pomodoro_10.png",
+    fap_author="@Th3Un1q3",
+    fap_weburl="https://github.com/Th3Un1q3/flipp_pomodoro",
+    fap_version="1.3",
+    fap_description="Boost Your Productivity with the Pomodoro Timer",
+)

二進制
pomodoro/flipp_pomodoro_10.png


+ 121 - 0
pomodoro/flipp_pomodoro_app.c

@@ -0,0 +1,121 @@
+#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;
+};

+ 39 - 0
pomodoro/flipp_pomodoro_app.h

@@ -0,0 +1,39 @@
+#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,
+};

+ 20 - 0
pomodoro/helpers/time.c

@@ -0,0 +1,20 @@
+#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};
+};

+ 24 - 0
pomodoro/helpers/time.h

@@ -0,0 +1,24 @@
+#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);

二進制
pomodoro/images/flipp_pomodoro_focus_64/frame_00.png


二進制
pomodoro/images/flipp_pomodoro_focus_64/frame_01.png


+ 1 - 0
pomodoro/images/flipp_pomodoro_focus_64/frame_rate

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

二進制
pomodoro/images/flipp_pomodoro_learn_50x128.png


二進制
pomodoro/images/flipp_pomodoro_rest_64/frame_00.png


二進制
pomodoro/images/flipp_pomodoro_rest_64/frame_01.png


+ 1 - 0
pomodoro/images/flipp_pomodoro_rest_64/frame_rate

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

二進制
pomodoro/img/1.png


二進制
pomodoro/img/2.png


+ 94 - 0
pomodoro/modules/flipp_pomodoro.c

@@ -0,0 +1,94 @@
+#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;
+};

+ 53 - 0
pomodoro/modules/flipp_pomodoro.h

@@ -0,0 +1,53 @@
+#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);

+ 26 - 0
pomodoro/modules/flipp_pomodoro_statistics.c

@@ -0,0 +1,26 @@
+#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,
+};

+ 28 - 0
pomodoro/scenes/flipp_pomodoro_scene.h

@@ -0,0 +1,28 @@
+#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

+ 59 - 0
pomodoro/scenes/flipp_pomodoro_scene_info.c

@@ -0,0 +1,59 @@
+#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);
+};

+ 154 - 0
pomodoro/scenes/flipp_pomodoro_scene_timer.c

@@ -0,0 +1,154 @@
+#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


+ 152 - 0
pomodoro/views/flipp_pomodoro_info_view.c

@@ -0,0 +1,152 @@
+
+#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;
+}

+ 71 - 0
pomodoro/views/flipp_pomodoro_info_view.h

@@ -0,0 +1,71 @@
+#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);

+ 293 - 0
pomodoro/views/flipp_pomodoro_timer_view.c

@@ -0,0 +1,293 @@
+#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);
+};

+ 30 - 0
pomodoro/views/flipp_pomodoro_timer_view.h

@@ -0,0 +1,30 @@
+#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);