Explorar el Código

API 31 / unzip sources

MX hace 2 años
commit
0a1b42c4d1

+ 11 - 0
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="Misc_Extra",
+    fap_icon_assets="images",
+    fap_icon="flipp_pomodoro_10.png",
+)

BIN
flipp_pomodoro_10.png


+ 100 - 0
flipp_pomodoro_app.c

@@ -0,0 +1,100 @@
+#include "flipp_pomodoro_app_i.h"
+
+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);
+        };
+
+        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();
+    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();
+
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        FlippPomodoroAppViewTimer,
+        flipp_pomodoro_view_timer_get_view(app->timer_view));
+
+    scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneTimer);
+
+    return app;
+};
+
+void flipp_pomodoro_app_free(FlippPomodoroApp* app) {
+    view_dispatcher_remove_view(app->view_dispatcher, FlippPomodoroAppViewTimer);
+    view_dispatcher_free(app->view_dispatcher);
+    scene_manager_free(app->scene_manager);
+    flipp_pomodoro_view_timer_free(app->timer_view);
+    free(app);
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_NOTIFICATION);
+};
+
+int32_t flipp_pomodoro_app(void* p) {
+    UNUSED(p);
+    FlippPomodoroApp* app = flipp_pomodoro_app_alloc();
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    flipp_pomodoro_app_free(app);
+
+    return 0;
+};

+ 32 - 0
flipp_pomodoro_app.h

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

+ 31 - 0
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
helpers/debug.h

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

+ 49 - 0
helpers/notifications.c

@@ -0,0 +1,49 @@
+#include <notification/notification_messages.h>
+
+const NotificationSequence work_start_notification = {
+    &message_display_backlight_on,
+
+    &message_vibro_on,
+
+    &message_note_b5,
+    &message_delay_250,
+
+    &message_note_d5,
+    &message_delay_250,
+
+    &message_sound_off,
+    &message_vibro_off,
+
+    &message_green_255,
+    &message_delay_1000,
+    &message_green_0,
+    &message_delay_250,
+    &message_green_255,
+    &message_delay_1000,
+
+    NULL,
+};
+
+const NotificationSequence rest_start_notification = {
+    &message_display_backlight_on,
+
+    &message_vibro_on,
+
+    &message_note_d5,
+    &message_delay_250,
+
+    &message_note_b5,
+    &message_delay_250,
+
+    &message_sound_off,
+    &message_vibro_off,
+
+    &message_red_255,
+    &message_delay_1000,
+    &message_red_0,
+    &message_delay_250,
+    &message_red_255,
+    &message_delay_1000,
+
+    NULL,
+};

+ 14 - 0
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
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
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);

BIN
images/flipp_pomodoro_focus_64/frame_00.png


BIN
images/flipp_pomodoro_focus_64/frame_01.png


+ 1 - 0
images/flipp_pomodoro_focus_64/frame_rate

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

BIN
images/flipp_pomodoro_rest_64/frame_00.png


BIN
images/flipp_pomodoro_rest_64/frame_01.png


+ 1 - 0
images/flipp_pomodoro_rest_64/frame_rate

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

+ 89 - 0
modules/flipp_pomodoro.c

@@ -0,0 +1,89 @@
+#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] = "Continue focus for:",
+    [FlippPomodoroStageRest] = "Keep rest for:",
+    [FlippPomodoroStageLongBreak] = "Long Break for:",
+};
+
+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)];
+};
+
+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;
+};

+ 54 - 0
modules/flipp_pomodoro.h

@@ -0,0 +1,54 @@
+#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 {
+    PomodoroStage stage;
+    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);

+ 0 - 0
scenes/.keep


+ 1 - 0
scenes/config/flipp_pomodoro_scene_config.h

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

+ 30 - 0
scenes/flipp_pomodoro_scene.c

@@ -0,0 +1,30 @@
+#include "flipp_pomodoro_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const flipp_pomodoro_scene_on_enter_handlers[])(void*) = {
+#include "config/flipp_pomodoro_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const flipp_pomodoro_scene_on_event_handlers[])(void* 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,
+};

+ 27 - 0
scenes/flipp_pomodoro_scene.h

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

+ 71 - 0
scenes/flipp_pomodoro_scene_timer.c

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

+ 0 - 0
views/.keep


+ 195 - 0
views/flipp_pomodoro_timer_view.c

@@ -0,0 +1,195 @@
+#include "flipp_pomodoro_timer_view.h"
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/elements.h>
+#include <gui/view.h>
+#include "../helpers/debug.h"
+#include "../flipp_pomodoro_app.h"
+#include "../modules/flipp_pomodoro.h"
+
+// Auto-compiled icons
+#include "flipp_pomodoro_icons.h"
+
+enum {
+    ViewInputConsumed = true,
+    ViewInputNotConusmed = false,
+};
+
+struct FlippPomodoroTimerView {
+    View* view;
+    FlippPomodoroTimerViewInputCb right_cb;
+    void* right_cb_ctx;
+};
+
+typedef struct {
+    IconAnimation* icon;
+    FlippPomodoroState* state;
+} 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_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));
+};
+
+bool flipp_pomodoro_view_timer_input_callback(InputEvent* event, void* ctx) {
+    furi_assert(ctx);
+    furi_assert(event);
+    FlippPomodoroTimerView* timer = ctx;
+
+    const bool should_trigger_right_event_cb = (event->type == InputTypePress) &&
+                                               (event->key == InputKeyRight) &&
+                                               (timer->right_cb != NULL);
+
+    if(should_trigger_right_event_cb) {
+        furi_assert(timer->right_cb);
+        furi_assert(timer->right_cb_ctx);
+        timer->right_cb(timer->right_cb_ctx);
+        return ViewInputConsumed;
+    };
+
+    return ViewInputNotConusmed;
+};
+
+View* flipp_pomodoro_view_timer_get_view(FlippPomodoroTimerView* timer) {
+    furi_assert(timer);
+    return timer->view;
+};
+
+void flipp_pomodoro_view_timer_assign_animation(View* view) {
+    with_view_model(
+        view,
+        FlippPomodoroTimerViewModel * model,
+        {
+            furi_assert(model->state);
+            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(timer->view, ViewModelTypeLockFree, sizeof(FlippPomodoroTimerViewModel));
+    view_set_context(flipp_pomodoro_view_timer_get_view(timer), timer);
+    view_set_draw_callback(timer->view, flipp_pomodoro_view_timer_draw_callback);
+    view_set_input_callback(timer->view, flipp_pomodoro_view_timer_input_callback);
+
+    return timer;
+};
+
+void flipp_pomodoro_view_timer_set_on_right_cb(
+    FlippPomodoroTimerView* timer,
+    FlippPomodoroTimerViewInputCb right_cb,
+    void* right_cb_ctx) {
+    furi_assert(right_cb);
+    furi_assert(right_cb_ctx);
+    timer->right_cb = right_cb;
+    timer->right_cb_ctx = right_cb_ctx;
+};
+
+void flipp_pomodoro_view_timer_set_state(View* view, FlippPomodoroState* state) {
+    furi_assert(view);
+    furi_assert(state);
+    with_view_model(
+        view, FlippPomodoroTimerViewModel * model, { model->state = state; }, false);
+    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);
+};

+ 21 - 0
views/flipp_pomodoro_timer_view.h

@@ -0,0 +1,21 @@
+#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_on_right_cb(
+    FlippPomodoroTimerView* timer,
+    FlippPomodoroTimerViewInputCb right_cb,
+    void* right_cb_ctx);