├── flipp_pomodoro ├── scenes │ ├── .keep │ ├── config │ │ └── flipp_pomodoro_scene_config.h │ ├── flipp_pomodoro_scene.h │ ├── flipp_pomodoro_scene.c │ ├── flipp_pomodoro_scene_info.c │ ├── flipp_pomodoro_scene_config.c │ └── flipp_pomodoro_scene_timer.c ├── views │ ├── .keep │ ├── flipp_pomodoro_config_view.h │ ├── flipp_pomodoro_timer_view.h │ ├── flipp_pomodoro_info_view.h │ ├── flipp_pomodoro_info_view.c │ ├── flipp_pomodoro_config_view.c │ └── flipp_pomodoro_timer_view.c ├── images │ ├── flipp_pomodoro_focus_64 │ │ ├── frame_rate │ │ ├── frame_00.png │ │ └── frame_01.png │ ├── flipp_pomodoro_rest_64 │ │ ├── frame_rate │ │ ├── frame_00.png │ │ └── frame_01.png │ └── flipp_pomodoro_learn_50x128.png ├── helpers │ ├── debug.h │ ├── time.h │ ├── time.c │ ├── notifications.h │ ├── notification_manager.h │ ├── hints.h │ ├── notifications.c │ └── notification_manager.c ├── flipp_pomodoro_10.png ├── application.fam ├── flipp_pomodoro_app_i.h ├── modules │ ├── flipp_pomodoro_settings.h │ ├── flipp_pomodoro_statistics.c │ ├── flipp_pomodoro_statistics.h │ ├── flipp_pomodoro.h │ ├── flipp_pomodoro_settings.c │ └── flipp_pomodoro.c ├── flipp_pomodoro_app.h └── flipp_pomodoro_app.c ├── docs ├── headline.gif ├── resting.png ├── settings.png ├── working.png └── INSTALATION.md ├── .gitignore ├── .cursorignore ├── .vscode ├── launch.json ├── tasks.json ├── c_cpp_properties.json └── settings.json ├── .gitmodules ├── .github ├── dependabot.yml └── workflows │ └── release.yml ├── tools ├── files-list.sh └── build.sh ├── LICENSE.md ├── README.md └── assets └── graphics-template.pixil /flipp_pomodoro/scenes/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flipp_pomodoro/views/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flipp_pomodoro/images/flipp_pomodoro_focus_64/frame_rate: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /flipp_pomodoro/images/flipp_pomodoro_rest_64/frame_rate: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /docs/headline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/docs/headline.gif -------------------------------------------------------------------------------- /docs/resting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/docs/resting.png -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/docs/settings.png -------------------------------------------------------------------------------- /docs/working.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/docs/working.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .standard-firmware 3 | .unleashed-firmware 4 | .DS_STORE/ 5 | .DS_Store 6 | /.idea -------------------------------------------------------------------------------- /flipp_pomodoro/helpers/debug.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define TAG "FlippPomodoro" -------------------------------------------------------------------------------- /flipp_pomodoro/flipp_pomodoro_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/flipp_pomodoro/flipp_pomodoro_10.png -------------------------------------------------------------------------------- /flipp_pomodoro/images/flipp_pomodoro_learn_50x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/flipp_pomodoro/images/flipp_pomodoro_learn_50x128.png -------------------------------------------------------------------------------- /flipp_pomodoro/images/flipp_pomodoro_rest_64/frame_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/flipp_pomodoro/images/flipp_pomodoro_rest_64/frame_00.png -------------------------------------------------------------------------------- /flipp_pomodoro/images/flipp_pomodoro_rest_64/frame_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/flipp_pomodoro/images/flipp_pomodoro_rest_64/frame_01.png -------------------------------------------------------------------------------- /flipp_pomodoro/images/flipp_pomodoro_focus_64/frame_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/flipp_pomodoro/images/flipp_pomodoro_focus_64/frame_00.png -------------------------------------------------------------------------------- /flipp_pomodoro/images/flipp_pomodoro_focus_64/frame_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Un1q3/flipp_pomodoro/HEAD/flipp_pomodoro/images/flipp_pomodoro_focus_64/frame_01.png -------------------------------------------------------------------------------- /flipp_pomodoro/scenes/config/flipp_pomodoro_scene_config.h: -------------------------------------------------------------------------------- 1 | ADD_SCENE(flipp_pomodoro, info, Info) 2 | ADD_SCENE(flipp_pomodoro, timer, Timer) 3 | ADD_SCENE(flipp_pomodoro, config, Config) 4 | -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- 1 | # Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) 2 | 3 | .git 4 | .github 5 | .cursorignore 6 | .cursorignore 7 | .cursorignore 8 | .standard-firmware 9 | .unleashed-firmware 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".standard-firmware"] 2 | path = .standard-firmware 3 | url = https://github.com/flipperdevices/flipperzero-firmware.git 4 | branch = release 5 | [submodule ".unleashed-firmware"] 6 | path = .unleashed-firmware 7 | url = https://github.com/DarkFlippers/unleashed-firmware.git 8 | branch = release 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gitsubmodule" # Track official and unleashed firmware as a submodule 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /flipp_pomodoro/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="flipp_pomodoro", 3 | name="Flipp Pomodoro", 4 | apptype=FlipperAppType.EXTERNAL, 5 | entry_point="flipp_pomodoro_app", 6 | requires=["gui", "notification", "dolphin"], 7 | stack_size=1 * 1024, 8 | fap_category="Tools", 9 | fap_icon_assets="images", 10 | fap_icon="flipp_pomodoro_10.png", 11 | ) 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build Container", 8 | "type": "shell", 9 | "command": "bash tools/build.sh -f unleashed -i", 10 | "group": "build", 11 | "presentation": { 12 | // Reveal the output only if unrecognized errors occur. 13 | "reveal": "silent" 14 | }, 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /tools/files-list.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if directory is passed as argument 4 | if [ "$#" -ne 1 ]; then 5 | echo "Usage: $0 directory" 6 | exit 1 7 | fi 8 | 9 | # Root directory to start the search 10 | root_dir="$1" 11 | 12 | # Extensions to look for 13 | extensions=("c" "h") 14 | 15 | # Function to process files 16 | process_file() { 17 | file="$1" 18 | echo "# ${file}" 19 | echo "" 20 | cat "${file}" 21 | echo "" 22 | echo "" 23 | } 24 | 25 | export -f process_file 26 | 27 | # Loop over extensions 28 | for ext in "${extensions[@]}"; do 29 | find "$root_dir" -name "*.${ext}" -exec bash -c 'process_file "$0"' {} \; 30 | done 31 | -------------------------------------------------------------------------------- /docs/INSTALATION.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | * Download application file for your firmware. In the release you can find 4 | `flipp_pomodoro_{firmware}_{api_version}.fap` where: 5 | * * `{firmware}` - is type of firmware you use `standard` stands for official one. 6 | * * `{api_version}` - is the version of API that was used for build. There is no easy way to map api version and firmware version. But this app is made the way the latest release is compatable with latest api/firmware. 7 | * Copy `flipp_pomodoro_{firmware}_{api_version}.fap` to SD-card of your flipper `/apps/Misc`. 8 | * Run from `Applications` menu of your flipper. 9 | * Hint: add app to favorites and stay always productive 10 | 11 | So Long, and Thanks for All the Fish! -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Mac", 5 | "includePath": [ 6 | "${workspaceFolder}/flipp_pomodoro/**", 7 | "${workspaceFolder}/.standard-firmware/**" 8 | ], 9 | "defines": [], 10 | "macFrameworkPath": [ 11 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks" 12 | ], 13 | "compilerPath": "/usr/bin/clang", 14 | "cStandard": "c17", 15 | "cppStandard": "c++17", 16 | "intelliSenseMode": "macos-clang-x64" 17 | } 18 | ], 19 | "version": 4 20 | } -------------------------------------------------------------------------------- /flipp_pomodoro/helpers/time.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | extern const int TIME_SECONDS_IN_MINUTE; 7 | extern const int TIME_MINUTES_IN_HOUR; 8 | 9 | /// @brief Container for a time period 10 | typedef struct 11 | { 12 | uint8_t seconds; 13 | uint8_t minutes; 14 | uint32_t total_seconds; 15 | } TimeDifference; 16 | 17 | /// @brief Time by the moment of calling 18 | /// @return A timestamp(seconds percision) 19 | uint32_t time_now(); 20 | 21 | /// @brief Calculates difference between two provided timestamps 22 | /// @param begin - start timestamp of the period 23 | /// @param end - end timestamp of the period to measure 24 | /// @return TimeDifference struct 25 | TimeDifference time_difference_seconds(uint32_t begin, uint32_t end); 26 | -------------------------------------------------------------------------------- /flipp_pomodoro/flipp_pomodoro_app_i.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // #define FURI_DEBUG 1 4 | 5 | /** 6 | * Index of dependencies for the main app 7 | */ 8 | 9 | // Platform Imports 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | // App resource imports 22 | 23 | #include "helpers/time.h" 24 | #include "helpers/notifications.h" 25 | #include "modules/flipp_pomodoro.h" 26 | #include "flipp_pomodoro_app.h" 27 | #include "scenes/flipp_pomodoro_scene.h" 28 | #include "views/flipp_pomodoro_timer_view.h" 29 | 30 | // Auto-compiled icons 31 | #include "flipp_pomodoro_icons.h" 32 | -------------------------------------------------------------------------------- /flipp_pomodoro/modules/flipp_pomodoro_settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | typedef enum { 7 | FlippPomodoroBuzzSlide = 0, 8 | FlippPomodoroBuzzOnce = 1, 9 | FlippPomodoroBuzzAnnoying = 2, 10 | FlippPomodoroBuzzFlash = 3, 11 | FlippPomodoroBuzzVibrate = 4, 12 | FlippPomodoroBuzzSoftBeep = 5, 13 | FlippPomodoroBuzzLoudBeep = 6, 14 | FlippPomodoroBuzzModeCount, 15 | } FlippPomodoroBuzzMode; 16 | 17 | typedef struct { 18 | uint8_t focus_minutes; 19 | uint8_t short_break_minutes; 20 | uint8_t long_break_minutes; 21 | uint8_t buzz_mode; 22 | } FlippPomodoroSettings; 23 | 24 | bool flipp_pomodoro_settings_load(FlippPomodoroSettings* settings); 25 | bool flipp_pomodoro_settings_save(const FlippPomodoroSettings* settings); 26 | void flipp_pomodoro_settings_set_default(FlippPomodoroSettings* settings); 27 | -------------------------------------------------------------------------------- /flipp_pomodoro/helpers/time.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "time.h" 4 | 5 | const int TIME_SECONDS_IN_MINUTE = 60; 6 | const int TIME_MINUTES_IN_HOUR = 60; 7 | 8 | uint32_t time_now() 9 | { 10 | return furi_hal_rtc_get_timestamp(); 11 | }; 12 | 13 | TimeDifference time_difference_seconds(uint32_t begin, uint32_t end) 14 | { 15 | // Clamp negative durations to zero to avoid underflow (e.g., when timer should stop at 00:00) 16 | if(end <= begin) { 17 | return (TimeDifference){.total_seconds = 0, .minutes = 0, .seconds = 0}; 18 | } 19 | 20 | const uint32_t duration_seconds = end - begin; 21 | 22 | uint32_t minutes = (duration_seconds / TIME_SECONDS_IN_MINUTE) % TIME_MINUTES_IN_HOUR; 23 | uint32_t seconds = duration_seconds % TIME_SECONDS_IN_MINUTE; 24 | 25 | return (TimeDifference){.total_seconds = duration_seconds, .minutes = minutes, .seconds = seconds}; 26 | }; 27 | -------------------------------------------------------------------------------- /flipp_pomodoro/modules/flipp_pomodoro_statistics.c: -------------------------------------------------------------------------------- 1 | #include "flipp_pomodoro_statistics.h" 2 | 3 | FlippPomodoroStatistics *flipp_pomodoro_statistics__new() 4 | { 5 | FlippPomodoroStatistics *statistics = malloc(sizeof(FlippPomodoroStatistics)); 6 | 7 | statistics->focus_stages_completed = 0; 8 | 9 | return statistics; 10 | } 11 | 12 | // Return the number of completed focus stages 13 | uint8_t flipp_pomodoro_statistics__get_focus_stages_completed(FlippPomodoroStatistics *statistics) 14 | { 15 | return statistics->focus_stages_completed; 16 | } 17 | 18 | // Increase the number of completed focus stages by one 19 | void flipp_pomodoro_statistics__increase_focus_stages_completed(FlippPomodoroStatistics *statistics) 20 | { 21 | statistics->focus_stages_completed++; 22 | } 23 | 24 | void flipp_pomodoro_statistics__destroy(FlippPomodoroStatistics *statistics) 25 | { 26 | furi_assert(statistics); 27 | free(statistics); 28 | }; 29 | -------------------------------------------------------------------------------- /flipp_pomodoro/views/flipp_pomodoro_config_view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../modules/flipp_pomodoro_settings.h" 5 | 6 | typedef struct FlippPomodoroConfigView FlippPomodoroConfigView; 7 | typedef void (*FlippPomodoroConfigViewActionCb)(void* ctx); 8 | 9 | FlippPomodoroConfigView* flipp_pomodoro_view_config_alloc(); 10 | View* flipp_pomodoro_view_config_get_view(FlippPomodoroConfigView* view); 11 | void flipp_pomodoro_view_config_free(FlippPomodoroConfigView* view); 12 | 13 | // From config file to screen 14 | void flipp_pomodoro_view_config_set_settings( 15 | FlippPomodoroConfigView* view, 16 | const FlippPomodoroSettings* in); 17 | 18 | // Config from ram for save btn 19 | void flipp_pomodoro_view_config_get_settings( 20 | FlippPomodoroConfigView* view, 21 | FlippPomodoroSettings* out); 22 | 23 | // Save 24 | void flipp_pomodoro_view_config_set_on_save_cb( 25 | FlippPomodoroConfigView* view, 26 | FlippPomodoroConfigViewActionCb cb, 27 | void* ctx); 28 | -------------------------------------------------------------------------------- /flipp_pomodoro/scenes/flipp_pomodoro_scene.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // Generate scene id and total number 5 | #define ADD_SCENE(prefix, name, id) FlippPomodoroScene##id, 6 | typedef enum 7 | { 8 | #include "config/flipp_pomodoro_scene_config.h" 9 | FlippPomodoroSceneNum, 10 | } FlippPomodoroScene; 11 | #undef ADD_SCENE 12 | 13 | extern const SceneManagerHandlers flipp_pomodoro_scene_handlers; 14 | 15 | // Generate scene on_enter handlers declaration 16 | #define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void *); 17 | #include "config/flipp_pomodoro_scene_config.h" 18 | #undef ADD_SCENE 19 | 20 | // Generate scene on_event handlers declaration 21 | #define ADD_SCENE(prefix, name, id) \ 22 | bool prefix##_scene_##name##_on_event(void *ctx, SceneManagerEvent event); 23 | #include "config/flipp_pomodoro_scene_config.h" 24 | #undef ADD_SCENE 25 | 26 | // Generate scene on_exit handlers declaration 27 | #define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void *ctx); 28 | #include "config/flipp_pomodoro_scene_config.h" 29 | #undef ADD_SCENE 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License 2 | 3 | Copyright (c) 2022 Mikhail Gubenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /flipp_pomodoro/views/flipp_pomodoro_timer_view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../modules/flipp_pomodoro.h" 5 | 6 | typedef struct FlippPomodoroTimerView FlippPomodoroTimerView; 7 | 8 | typedef void (*FlippPomodoroTimerViewInputCb)(void *ctx); 9 | 10 | FlippPomodoroTimerView *flipp_pomodoro_view_timer_alloc(); 11 | 12 | View *flipp_pomodoro_view_timer_get_view(FlippPomodoroTimerView *timer); 13 | 14 | void flipp_pomodoro_view_timer_free(FlippPomodoroTimerView *timer); 15 | 16 | void flipp_pomodoro_view_timer_set_state(View *view, FlippPomodoroState *state); 17 | 18 | void flipp_pomodoro_view_timer_set_callback_context(FlippPomodoroTimerView *timer, void *callback_ctx); 19 | 20 | void flipp_pomodoro_view_timer_set_on_right_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb right_cb); 21 | 22 | void flipp_pomodoro_view_timer_set_on_ok_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb ok_cb); 23 | 24 | void flipp_pomodoro_view_timer_display_hint(View *view, char *hint); 25 | 26 | void flipp_pomodoro_view_timer_set_on_left_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb left_cb); 27 | -------------------------------------------------------------------------------- /flipp_pomodoro/scenes/flipp_pomodoro_scene.c: -------------------------------------------------------------------------------- 1 | #include "flipp_pomodoro_scene.h" 2 | 3 | // Generate scene on_enter handlers array 4 | #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, 5 | void (*const flipp_pomodoro_scene_on_enter_handlers[])(void*) = { 6 | #include "config/flipp_pomodoro_scene_config.h" 7 | }; 8 | #undef ADD_SCENE 9 | 10 | // Generate scene on_event handlers array 11 | #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, 12 | bool (*const flipp_pomodoro_scene_on_event_handlers[])(void* ctx, SceneManagerEvent event) = { 13 | #include "config/flipp_pomodoro_scene_config.h" 14 | }; 15 | #undef ADD_SCENE 16 | 17 | // Generate scene on_exit handlers array 18 | #define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, 19 | void (*const flipp_pomodoro_scene_on_exit_handlers[])(void* ctx) = { 20 | #include "config/flipp_pomodoro_scene_config.h" 21 | }; 22 | #undef ADD_SCENE 23 | 24 | // Initialize scene handlers configuration structure 25 | const SceneManagerHandlers flipp_pomodoro_scene_handlers = { 26 | .on_enter_handlers = flipp_pomodoro_scene_on_enter_handlers, 27 | .on_event_handlers = flipp_pomodoro_scene_on_event_handlers, 28 | .on_exit_handlers = flipp_pomodoro_scene_on_exit_handlers, 29 | .scene_num = FlippPomodoroSceneNum, 30 | }; -------------------------------------------------------------------------------- /flipp_pomodoro/helpers/notifications.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../modules/flipp_pomodoro.h" 4 | #include 5 | 6 | /// @brief Focus-stage start cue (green + tones + vibro) 7 | extern const NotificationSequence work_start_notification; 8 | /// @brief Short-rest start cue (red + tones + vibro) 9 | extern const NotificationSequence rest_start_notification; 10 | /// @brief Long-break start cue (blue + “dolphin” motif + vibro) 11 | extern const NotificationSequence long_break_start_notification; 12 | 13 | /// @brief Defines a notification sequence that should indicate start of specific pomodoro stage. 14 | extern const NotificationSequence* stage_start_notification_sequence_map[]; 15 | 16 | /// @brief Stops any sound/vibro immediately. 17 | extern const NotificationSequence stop_all_notification; 18 | 19 | /// @brief Flash backlight on (green pulse) 20 | extern const NotificationSequence flash_backlight_on_sequence; 21 | /// @brief Flash backlight off (green off) 22 | extern const NotificationSequence flash_backlight_off_sequence; 23 | /// @brief Single short vibrate 24 | extern const NotificationSequence vibrate_sequence; 25 | /// @brief Soft single beep (D5) 26 | extern const NotificationSequence soft_beep_sequence; 27 | /// @brief Louder triple beep (D5/B5/D5) 28 | extern const NotificationSequence loud_beep_sequence; 29 | -------------------------------------------------------------------------------- /flipp_pomodoro/flipp_pomodoro_app.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "views/flipp_pomodoro_timer_view.h" 10 | #include "views/flipp_pomodoro_info_view.h" 11 | #include "views/flipp_pomodoro_config_view.h" 12 | 13 | #include "modules/flipp_pomodoro.h" 14 | #include "modules/flipp_pomodoro_statistics.h" 15 | #include "modules/flipp_pomodoro_settings.h" 16 | #include "helpers/notification_manager.h" 17 | 18 | typedef enum 19 | { 20 | // Reserve first 100 events for button types and indexes, starting from 0 21 | FlippPomodoroAppCustomEventStageSkip = 100, 22 | FlippPomodoroAppCustomEventStageComplete, // By Expiration 23 | FlippPomodoroAppCustomEventTimerTick, 24 | FlippPomodoroAppCustomEventTimerAskHint, 25 | FlippPomodoroAppCustomEventStateUpdated, 26 | FlippPomodoroAppCustomEventResumeTimer, 27 | } FlippPomodoroAppCustomEvent; 28 | 29 | typedef struct 30 | { 31 | SceneManager *scene_manager; 32 | ViewDispatcher *view_dispatcher; 33 | Gui *gui; 34 | NotificationApp *notification_app; 35 | NotificationManager *notification_manager; 36 | FlippPomodoroTimerView *timer_view; 37 | FlippPomodoroInfoView *info_view; 38 | FlippPomodoroConfigView *config_view; 39 | FlippPomodoroState *state; 40 | FlippPomodoroStatistics *statistics; 41 | uint32_t paused_at_timestamp; 42 | FlippPomodoroSettings settings_before; 43 | } FlippPomodoroApp; 44 | 45 | typedef enum 46 | { 47 | FlippPomodoroAppViewTimer, 48 | FlippPomodoroAppViewInfo, 49 | FlippPomodoroAppViewConfig, 50 | } FlippPomodoroAppView; -------------------------------------------------------------------------------- /flipp_pomodoro/modules/flipp_pomodoro_statistics.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | /** @brief FlippPomodoroStatistics structure 5 | * 6 | * This structure is used to keep track of completed focus stages. 7 | */ 8 | typedef struct 9 | { 10 | uint8_t focus_stages_completed; 11 | } FlippPomodoroStatistics; 12 | 13 | /** @brief Allocate and initialize a new FlippPomodoroStatistics 14 | * 15 | * This function allocates a new FlippPomodoroStatistics structure, initializes its members 16 | * and returns a pointer to it. 17 | * 18 | * @return A pointer to a new FlippPomodoroStatistics structure 19 | */ 20 | FlippPomodoroStatistics *flipp_pomodoro_statistics__new(); 21 | 22 | /** @brief Get the number of completed focus stages 23 | * 24 | * This function retrieves the number of completed focus stages in a FlippPomodoroStatistics structure. 25 | * 26 | * @param statistics A pointer to a FlippPomodoroStatistics structure 27 | * @return The number of completed focus stages 28 | */ 29 | uint8_t flipp_pomodoro_statistics__get_focus_stages_completed(FlippPomodoroStatistics *statistics); 30 | 31 | /** @brief Increase the number of completed focus stages 32 | * 33 | * This function increases the count of the completed focus stages by one in a FlippPomodoroStatistics structure. 34 | * 35 | * @param statistics A pointer to a FlippPomodoroStatistics structure 36 | */ 37 | void flipp_pomodoro_statistics__increase_focus_stages_completed(FlippPomodoroStatistics *statistics); 38 | 39 | /** @brief Free a FlippPomodoroStatistics structure 40 | * 41 | * This function frees the memory used by a FlippPomodoroStatistics structure. 42 | * 43 | * @param statistics A pointer to a FlippPomodoroStatistics structure 44 | */ 45 | void flipp_pomodoro_statistics__destroy(FlippPomodoroStatistics *state); -------------------------------------------------------------------------------- /flipp_pomodoro/helpers/notification_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../modules/flipp_pomodoro.h" 4 | #include "../modules/flipp_pomodoro_settings.h" 5 | #include 6 | #include 7 | 8 | /** 9 | * @brief Opaque notification manager state 10 | */ 11 | typedef struct NotificationManager NotificationManager; 12 | 13 | /** 14 | * @brief Create a new notification manager instance 15 | * @return Pointer to the notification manager 16 | */ 17 | NotificationManager* notification_manager_alloc(void); 18 | 19 | /** 20 | * @brief Free the notification manager 21 | * @param manager Pointer to the notification manager 22 | */ 23 | void notification_manager_free(NotificationManager* manager); 24 | 25 | /** 26 | * @brief Reset notification state when entering a scene 27 | * @param manager Pointer to the notification manager 28 | */ 29 | void notification_manager_reset(NotificationManager* manager); 30 | 31 | /** 32 | * @brief Stop all active notifications 33 | * @param manager Pointer to the notification manager 34 | */ 35 | void notification_manager_stop(NotificationManager* manager); 36 | 37 | /** 38 | * @brief Handle timer tick when stage is expired 39 | * @param manager Pointer to the notification manager 40 | * @param next_stage The next pomodoro stage to notify about 41 | * @param buzz_mode Notification mode to use 42 | * @return true if stage complete event should be sent, false otherwise 43 | */ 44 | bool notification_manager_handle_expired_stage( 45 | NotificationManager* manager, 46 | PomodoroStage next_stage, 47 | FlippPomodoroBuzzMode buzz_mode); 48 | 49 | /** 50 | * @brief Reset notification flags when stage is not expired 51 | * @param manager Pointer to the notification manager 52 | */ 53 | void notification_manager_reset_flags(NotificationManager* manager); 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | name: Bump and Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Cancel Previous Runs 13 | uses: styfle/cancel-workflow-action@0.12.1 14 | with: 15 | access_token: ${{ github.token }} 16 | 17 | - uses: actions/checkout@v6 18 | 19 | - name: Build Application 20 | run: | 21 | bash tools/build.sh -f unleashed 22 | bash tools/build.sh 23 | 24 | - name: Bump Version and Push Tag 25 | id: tag_version 26 | uses: mathieudutour/github-tag-action@v6.2 27 | with: 28 | release_branches: master 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | 32 | - name: Create a GitHub Release 33 | uses: ncipollo/release-action@v1 34 | with: 35 | tag: ${{ steps.tag_version.outputs.new_tag }} 36 | name: ${{ steps.tag_version.outputs.new_tag }} 37 | body: | 38 | # Installation 39 | 40 | * Download application file for your firmware. In the release you can find 41 | `flipp_pomodoro_{firmware}_{api_version}.fap` where: 42 | * * `{firmware}` - is type of firmware you use `standard` stands for official one. 43 | * * `{api_version}` - is the version of API that was used for build. There is no easy way to map api version and firmware version. But this app is made the way the latest release is compatable with latest api/firmware. 44 | * Copy `flipp_pomodoro_{firmware}_{api_version}.fap` to SD-card of your flipper `/apps/Tools`. 45 | * Run from `Applications` menu of your flipper. 46 | * Hint: add app to favorites and stay always productive 47 | 48 | So Long, and Thanks for All the Fish! 🐬 49 | 50 | # Change Detail 51 | ${{ steps.tag_version.outputs.changelog }} 52 | artifacts: "dist/**" 53 | -------------------------------------------------------------------------------- /tools/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | application_name="flipp_pomodoro" 4 | repo_root=$(dirname $0)/.. 5 | 6 | mkdir -p ${repo_root}/dist 7 | 8 | # Fetch all firmwares submodules 9 | git submodule update --init --recursive 10 | 11 | # Set default build mode 12 | build_mode="standard" 13 | is_run=false 14 | 15 | # Use getopts to parse command-line options and assign their values to variables 16 | while getopts "f:i" opt; do 17 | case $opt in 18 | f) 19 | build_mode=$OPTARG 20 | ;; 21 | i) 22 | is_run=true 23 | ;; 24 | \?) 25 | echo "Invalid option: -$OPTARG" >&2 26 | exit 1 27 | ;; 28 | :) 29 | echo "Option -$OPTARG requires an argument." >&2 30 | exit 1 31 | ;; 32 | esac 33 | done 34 | 35 | cd "${repo_root}/.${build_mode}-firmware" 36 | 37 | 38 | # Define the possible file paths 39 | file_path1="firmware/targets/f7/api_symbols.csv" 40 | file_path2="targets/f7/api_symbols.csv" 41 | 42 | # Function to extract the API version from a CSV file 43 | extract_api_version() { 44 | local file_path=$1 45 | local api_version=$(awk -F',' 'NR == 2 {print $3}' "$file_path") 46 | echo "$api_version" 47 | } 48 | 49 | # Try to extract from the first file path 50 | api_version=$(extract_api_version "$file_path1") 51 | 52 | # If the first attempt fails, try the second file path 53 | if [ -z "$api_version" ]; then 54 | api_version=$(extract_api_version "$file_path2") 55 | fi 56 | 57 | # Check if the API version is still not found 58 | if [ -z "$api_version" ]; then 59 | echo "Error: API version not found in either file path." 60 | else 61 | echo "API version: $api_version" 62 | fi 63 | 64 | app_suffix="${build_mode}_${api_version}" 65 | 66 | export FBT_NO_SYNC=1 67 | 68 | rm -rf applications_user/$application_name 69 | rm -rf build/f7-firmware-D/.extapps 70 | 71 | cp -r ../$application_name/. applications_user/$application_name 72 | 73 | if $is_run; then 74 | ./fbt launch_app APPSRC=$application_name 75 | else 76 | ./fbt "fap_${application_name}" 77 | fi 78 | 79 | cp "build/f7-firmware-D/.extapps/${application_name}.fap" "../dist/${application_name}_${app_suffix}.fap" 80 | -------------------------------------------------------------------------------- /flipp_pomodoro/scenes/flipp_pomodoro_scene_info.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "flipp_pomodoro_scene.h" 5 | #include "../flipp_pomodoro_app.h" 6 | #include "../views/flipp_pomodoro_info_view.h" 7 | 8 | enum 9 | { 10 | SceneEventConusmed = true, 11 | SceneEventNotConusmed = false 12 | }; 13 | 14 | void flipp_pomodoro_scene_info_on_back_to_timer(void *ctx) 15 | { 16 | furi_assert(ctx); 17 | FlippPomodoroApp *app = ctx; 18 | 19 | view_dispatcher_send_custom_event( 20 | app->view_dispatcher, 21 | FlippPomodoroAppCustomEventResumeTimer); 22 | }; 23 | 24 | void flipp_pomodoro_scene_info_on_enter(void *ctx) 25 | { 26 | furi_assert(ctx); 27 | FlippPomodoroApp *app = ctx; 28 | 29 | view_dispatcher_switch_to_view(app->view_dispatcher, FlippPomodoroAppViewInfo); 30 | flipp_pomodoro_info_view_set_pomodoros_completed( 31 | flipp_pomodoro_info_view_get_view(app->info_view), 32 | flipp_pomodoro_statistics__get_focus_stages_completed(app->statistics)); 33 | flipp_pomodoro_info_view_set_mode(flipp_pomodoro_info_view_get_view(app->info_view), FlippPomodoroInfoViewModeStats); 34 | flipp_pomodoro_info_view_set_resume_timer_cb(app->info_view, flipp_pomodoro_scene_info_on_back_to_timer, app); 35 | }; 36 | 37 | void flipp_pomodoro_scene_info_handle_custom_event(FlippPomodoroApp *app, FlippPomodoroAppCustomEvent custom_event) 38 | { 39 | if (custom_event == FlippPomodoroAppCustomEventResumeTimer) 40 | { 41 | scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneTimer); 42 | } 43 | }; 44 | 45 | bool flipp_pomodoro_scene_info_on_event(void *ctx, SceneManagerEvent event) 46 | { 47 | furi_assert(ctx); 48 | FlippPomodoroApp *app = ctx; 49 | 50 | switch (event.type) 51 | { 52 | case SceneManagerEventTypeBack: 53 | view_dispatcher_stop(app->view_dispatcher); 54 | return SceneEventConusmed; 55 | case SceneManagerEventTypeCustom: 56 | flipp_pomodoro_scene_info_handle_custom_event(app, event.event); 57 | return SceneEventConusmed; 58 | default: 59 | break; 60 | }; 61 | return SceneEventNotConusmed; 62 | }; 63 | 64 | void flipp_pomodoro_scene_info_on_exit(void *ctx) 65 | { 66 | UNUSED(ctx); 67 | }; -------------------------------------------------------------------------------- /flipp_pomodoro/scenes/flipp_pomodoro_scene_config.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../flipp_pomodoro_app.h" 3 | #include "flipp_pomodoro_scene.h" 4 | #include "../helpers/time.h" 5 | #include "../modules/flipp_pomodoro_settings.h" 6 | #include "../views/flipp_pomodoro_config_view.h" 7 | #include "../modules/flipp_pomodoro.h" 8 | #include 9 | 10 | static void flipp_pomodoro_scene_config_on_save(void* ctx) { 11 | FlippPomodoroApp* app = ctx; 12 | 13 | FlippPomodoroSettings s_now; 14 | flipp_pomodoro_view_config_get_settings(app->config_view, &s_now); 15 | flipp_pomodoro_settings_save(&s_now); 16 | // immediately apply to timers 17 | flipp_pomodoro__apply_settings(&s_now); 18 | 19 | scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneTimer); 20 | } 21 | 22 | void flipp_pomodoro_scene_config_on_enter(void* ctx) { 23 | FlippPomodoroApp* app = ctx; 24 | app->paused_at_timestamp = time_now(); 25 | 26 | FlippPomodoroSettings s; 27 | flipp_pomodoro_settings_load(&s); 28 | app->settings_before = s; 29 | 30 | // ram data to view 31 | flipp_pomodoro_view_config_set_settings(app->config_view, &s); 32 | 33 | // Save at center 34 | flipp_pomodoro_view_config_set_on_save_cb(app->config_view, flipp_pomodoro_scene_config_on_save, app); 35 | 36 | view_dispatcher_switch_to_view(app->view_dispatcher, FlippPomodoroAppViewConfig); 37 | } 38 | 39 | bool flipp_pomodoro_scene_config_on_event(void* ctx, SceneManagerEvent event) { 40 | FlippPomodoroApp* app = ctx; 41 | if(event.type == SceneManagerEventTypeBack) { 42 | // when Back -> nothing to save 43 | scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneTimer); 44 | return true; 45 | } 46 | return false; 47 | } 48 | 49 | void flipp_pomodoro_scene_config_on_exit(void* ctx) { 50 | FlippPomodoroApp* app = ctx; 51 | 52 | FlippPomodoroSettings now; 53 | flipp_pomodoro_settings_load(&now); 54 | 55 | bool changed = memcmp(&now, &app->settings_before, sizeof(FlippPomodoroSettings)) != 0; 56 | 57 | // settings from file 58 | flipp_pomodoro__apply_settings(&now); 59 | 60 | if(changed) { 61 | flipp_pomodoro__destroy(app->state); 62 | app->state = flipp_pomodoro__new(); 63 | } else { 64 | uint32_t paused = time_now() - app->paused_at_timestamp; 65 | app->state->started_at_timestamp += paused; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /flipp_pomodoro/modules/flipp_pomodoro.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../helpers/time.h" 5 | #include "flipp_pomodoro_settings.h" 6 | 7 | /// @brief Options of pomodoro stages 8 | typedef enum 9 | { 10 | FlippPomodoroStageFocus, 11 | FlippPomodoroStageRest, 12 | FlippPomodoroStageLongBreak, 13 | } PomodoroStage; 14 | 15 | /// @brief State of the pomodoro timer 16 | typedef struct 17 | { 18 | uint8_t current_stage_index; 19 | uint32_t started_at_timestamp; 20 | } FlippPomodoroState; 21 | 22 | /// @brief Generates initial state 23 | /// @returns A new pre-populated state for pomodoro timer 24 | FlippPomodoroState *flipp_pomodoro__new(); 25 | 26 | /// @brief Extract current stage of pomodoro 27 | /// @param state - pointer to the state of pomorodo 28 | /// @returns Current stage value 29 | PomodoroStage flipp_pomodoro__get_stage(FlippPomodoroState *state); 30 | 31 | /// @brief Get stage by index 32 | /// @param index - stage index in the sequence 33 | /// @returns Stage value at the given index 34 | PomodoroStage flipp_pomodoro__stage_by_index(int index); 35 | 36 | /// @brief Destroys state of timer and it's dependencies 37 | void flipp_pomodoro__destroy(FlippPomodoroState *state); 38 | 39 | /// @brief Get remaining stage time. 40 | /// @param state - pointer to the state of pomorodo 41 | /// @returns Time difference to the end of current stage 42 | TimeDifference flipp_pomodoro__stage_remaining_duration(FlippPomodoroState *state); 43 | 44 | /// @brief Label of currently active stage 45 | /// @param state - pointer to the state of pomorodo 46 | /// @returns A string that explains current stage 47 | char *flipp_pomodoro__current_stage_label(FlippPomodoroState *state); 48 | 49 | /// @brief Label of transition to the next stage 50 | /// @param state - pointer to the state of pomorodo. 51 | /// @returns string with the label of the "skipp" button 52 | char *flipp_pomodoro__next_stage_label(FlippPomodoroState *state); 53 | 54 | /// @brief Check if current stage is expired 55 | /// @param state - pointer to the state of pomorodo. 56 | /// @returns expriations status - true means stage is expired 57 | bool flipp_pomodoro__is_stage_expired(FlippPomodoroState *state); 58 | 59 | /// @brief Rotate stage of the timer 60 | /// @param state - pointer to the state of pomorodo. 61 | void flipp_pomodoro__toggle_stage(FlippPomodoroState *state); 62 | 63 | const char* flipp_pomodoro__settings_button_label(); 64 | 65 | /// @brief Apply settings (minutes) to active durations (no persistence) 66 | void flipp_pomodoro__apply_settings(const FlippPomodoroSettings* settings); 67 | -------------------------------------------------------------------------------- /flipp_pomodoro/views/flipp_pomodoro_info_view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | /** @brief Mode types for FlippPomodoroInfoView 6 | * 7 | * These are the modes that can be used in the FlippPomodoroInfoView 8 | */ 9 | typedef enum 10 | { 11 | FlippPomodoroInfoViewModeStats, 12 | FlippPomodoroInfoViewModeAbout, 13 | } FlippPomodoroInfoViewMode; 14 | 15 | /** @brief Forward declaration of the FlippPomodoroInfoView struct */ 16 | typedef struct FlippPomodoroInfoView FlippPomodoroInfoView; 17 | 18 | /** @brief User action callback function type 19 | * 20 | * Callback functions of this type are called when a user action is performed. 21 | */ 22 | typedef void (*FlippPomodoroInfoViewUserActionCb)(void *ctx); 23 | 24 | /** @brief Allocate a new FlippPomodoroInfoView 25 | * 26 | * Allocates a new FlippPomodoroInfoView and returns a pointer to it. 27 | * @return A pointer to a new FlippPomodoroInfoView 28 | */ 29 | FlippPomodoroInfoView *flipp_pomodoro_info_view_alloc(); 30 | 31 | /** @brief Get the view from a FlippPomodoroInfoView 32 | * 33 | * Returns a pointer to the view associated with a FlippPomodoroInfoView. 34 | * @param info_view A pointer to a FlippPomodoroInfoView 35 | * @return A pointer to the view of the FlippPomodoroInfoView 36 | */ 37 | View *flipp_pomodoro_info_view_get_view(FlippPomodoroInfoView *info_view); 38 | 39 | /** @brief Free a FlippPomodoroInfoView 40 | * 41 | * Frees the memory used by a FlippPomodoroInfoView. 42 | * @param info_view A pointer to a FlippPomodoroInfoView 43 | */ 44 | void flipp_pomodoro_info_view_free(FlippPomodoroInfoView *info_view); 45 | 46 | /** @brief Set the number of completed pomodoros in the view 47 | * 48 | * Sets the number of completed pomodoros that should be displayed in the view. 49 | * @param info_view A pointer to the view 50 | * @param pomodoros_completed The number of completed pomodoros 51 | */ 52 | void flipp_pomodoro_info_view_set_pomodoros_completed(View *info_view, uint8_t pomodoros_completed); 53 | 54 | /** @brief Set the callback function to be called when the timer should be resumed 55 | * 56 | * Sets the callback function that will be called when the timer should be resumed. 57 | * @param info_view A pointer to the FlippPomodoroInfoView 58 | * @param user_action_cb The callback function 59 | * @param user_action_cb_ctx The context to be passed to the callback function 60 | */ 61 | void flipp_pomodoro_info_view_set_resume_timer_cb(FlippPomodoroInfoView *info_view, FlippPomodoroInfoViewUserActionCb user_action_cb, void *user_action_cb_ctx); 62 | 63 | /** @brief Set the mode of the view 64 | * 65 | * Sets the mode that should be used in the view. 66 | * @param view A pointer to the view 67 | * @param desired_mode The desired mode 68 | */ 69 | void flipp_pomodoro_info_view_set_mode(View *view, FlippPomodoroInfoViewMode desired_mode); 70 | -------------------------------------------------------------------------------- /flipp_pomodoro/modules/flipp_pomodoro_settings.c: -------------------------------------------------------------------------------- 1 | #include "flipp_pomodoro_settings.h" 2 | #include 3 | #include 4 | #include 5 | 6 | #define SETTINGS_DIR "/ext/apps_data/pomodoro_timer" 7 | #define SETTINGS_PATH SETTINGS_DIR "/pomodoro_timer.conf" 8 | 9 | typedef struct { 10 | uint8_t focus_minutes; 11 | uint8_t short_break_minutes; 12 | uint8_t long_break_minutes; 13 | } FlippPomodoroSettingsV1; 14 | 15 | void flipp_pomodoro_settings_set_default(FlippPomodoroSettings* settings) { 16 | settings->focus_minutes = 25; 17 | settings->short_break_minutes = 5; 18 | settings->long_break_minutes = 30; 19 | settings->buzz_mode = FlippPomodoroBuzzOnce; 20 | } 21 | 22 | static bool flipp_pomodoro_settings_try_load_from(File* file, const char* path, FlippPomodoroSettings* settings) { 23 | if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { 24 | return false; 25 | } 26 | uint8_t buf[sizeof(FlippPomodoroSettings)] = {0}; 27 | uint32_t n = storage_file_read(file, buf, sizeof(FlippPomodoroSettings)); 28 | bool ok = false; 29 | if(n == sizeof(FlippPomodoroSettings)) { 30 | memcpy(settings, buf, sizeof(FlippPomodoroSettings)); 31 | ok = true; 32 | } else if(n == sizeof(FlippPomodoroSettingsV1)) { 33 | const FlippPomodoroSettingsV1* v1 = (const FlippPomodoroSettingsV1*)buf; 34 | settings->focus_minutes = v1->focus_minutes; 35 | settings->short_break_minutes = v1->short_break_minutes; 36 | settings->long_break_minutes = v1->long_break_minutes; 37 | settings->buzz_mode = FlippPomodoroBuzzOnce; // upgrade by default 38 | ok = true; 39 | } 40 | storage_file_close(file); 41 | return ok; 42 | } 43 | 44 | bool flipp_pomodoro_settings_load(FlippPomodoroSettings* settings) { 45 | Storage* storage = furi_record_open("storage"); 46 | File* file = storage_file_alloc(storage); 47 | 48 | bool ok = flipp_pomodoro_settings_try_load_from(file, SETTINGS_PATH, settings); 49 | 50 | storage_file_free(file); 51 | furi_record_close("storage"); 52 | 53 | if(!ok) { 54 | flipp_pomodoro_settings_set_default(settings); 55 | } 56 | return ok; 57 | } 58 | 59 | bool flipp_pomodoro_settings_save(const FlippPomodoroSettings* settings) { 60 | Storage* storage = furi_record_open("storage"); 61 | File* file = storage_file_alloc(storage); 62 | 63 | // ensure the dir exist 64 | storage_common_mkdir(storage, SETTINGS_DIR); 65 | 66 | bool ok = false; 67 | if(storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { 68 | if(storage_file_write(file, settings, sizeof(FlippPomodoroSettings)) == sizeof(FlippPomodoroSettings)) { 69 | ok = true; 70 | } 71 | storage_file_close(file); 72 | } 73 | storage_file_free(file); 74 | furi_record_close("storage"); 75 | return ok; 76 | } 77 | -------------------------------------------------------------------------------- /flipp_pomodoro/helpers/hints.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Hints for focus stage 4 | static char* work_hints[] = { 5 | "Can you explain the problem as if I'm five?", 6 | "Expected output vs. reality: what's the difference?", 7 | "Ever thought of slicing the problem into bite-sized pieces?", 8 | "What's the story when you walk through the code?", 9 | "Any error messages gossiping about the issue?", 10 | "What tricks have you tried to fix this?", 11 | "Did you test the code, or just hoping for the best?", 12 | "How's this code mingling with the rest of the app?", 13 | "Any sneaky side effects causing mischief?", 14 | "What are you assuming, and is it safe to do so?", 15 | "Did you remember to invite all the edge cases to the party?", 16 | "What happens in the isolation chamber (running code separately)?", 17 | "Can you make the issue appear on command?", 18 | "What's the scene at the crime spot when the error occurs?", 19 | "Did you seek wisdom from the grand oracle (Google)?", 20 | "What if you take a different path to solve this?", 21 | "Did you take a coffee break to reboot your brain?", 22 | "What part of the code do you trust the least?", 23 | "Did the bug appear after a recent change?", 24 | "What would happen if you removed this block entirely?", 25 | "Is the code doing too much in one place?", 26 | "If this were someone else's bug, how would you debug it?" 27 | }; 28 | 29 | // Hints for break stages 30 | static char* break_hints[] = { 31 | "Time to stretch! Remember, your body isn't made of code.", 32 | "Hydrate or diedrate! Grab a glass of water.", 33 | "Blink! Your eyes need a break too.", 34 | "How about a quick dance-off with your shadow?", 35 | "Ever tried chair yoga? Now's the time!", 36 | "Time for a quick peek out the window. The outside world still exists!", 37 | "Quick, think about kittens! Or puppies! Or baby turtles!", 38 | "Time for a laugh. Look up a joke or two!", 39 | "Sing a song. Bonus points for making up your own lyrics.", 40 | "Do a quick tidy-up. A clean space is a happy space!", 41 | "Time to play 'air' musical instrument for a minute.", 42 | "How about a quick doodle? Unleash your inner Picasso!", 43 | "Practice your superhero pose. Feel the power surge!", 44 | "Quick, tell yourself a joke. Don't worry, I won't judge.", 45 | "Time to practice your mime skills. Stuck in a box, anyone?", 46 | "Ever tried juggling? Now's your chance!", 47 | "Do a quick self high-five, you're doing great!", 48 | "Close your eyes for 30 seconds and just breathe.", 49 | "Step away from the screen — literally 10 steps back.", 50 | "Name 3 things you can hear right now. Ground yourself.", 51 | "Imagine finishing this task and how it’ll feel.", 52 | "Stand up and shake your hands like you're drying them." 53 | }; 54 | 55 | #define WORK_HINTS_COUNT (sizeof(work_hints) / sizeof(work_hints[0])) 56 | #define BREAK_HINTS_COUNT (sizeof(break_hints) / sizeof(break_hints[0])) 57 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "input.h": "c", 4 | "furi_hal.h": "c", 5 | "gui.h": "c", 6 | "locale": "c", 7 | "string": "c", 8 | "__bit_reference": "c", 9 | "__config": "c", 10 | "__debug": "c", 11 | "__hash_table": "c", 12 | "__locale": "c", 13 | "__mutex_base": "c", 14 | "__node_handle": "c", 15 | "__split_buffer": "c", 16 | "__tree": "c", 17 | "__tuple": "c", 18 | "any": "c", 19 | "array": "c", 20 | "atomic": "c", 21 | "bit": "c", 22 | "charconv": "c", 23 | "chrono": "c", 24 | "cmath": "c", 25 | "compare": "c", 26 | "complex": "c", 27 | "concepts": "c", 28 | "cstdarg": "c", 29 | "cstddef": "c", 30 | "deque": "c", 31 | "exception": "c", 32 | "__memory": "c", 33 | "coroutine": "c", 34 | "functional": "c", 35 | "iterator": "c", 36 | "memory_resource": "c", 37 | "type_traits": "c", 38 | "utility": "c", 39 | "filesystem": "c", 40 | "forward_list": "c", 41 | "future": "c", 42 | "ios": "c", 43 | "istream": "c", 44 | "limits": "c", 45 | "list": "c", 46 | "map": "c", 47 | "memory": "c", 48 | "mutex": "c", 49 | "new": "c", 50 | "numeric": "c", 51 | "optional": "c", 52 | "ostream": "c", 53 | "random": "c", 54 | "ratio": "c", 55 | "regex": "c", 56 | "scoped_allocator": "c", 57 | "set": "c", 58 | "span": "c", 59 | "streambuf": "c", 60 | "string_view": "c", 61 | "system_error": "c", 62 | "thread": "c", 63 | "tuple": "c", 64 | "typeinfo": "c", 65 | "unordered_map": "c", 66 | "valarray": "c", 67 | "variant": "c", 68 | "vector": "c", 69 | "queue": "c", 70 | "semaphore": "c", 71 | "*.tcc": "c", 72 | "bitset": "c", 73 | "algorithm": "c", 74 | "rope": "c", 75 | "slist": "c", 76 | "fstream": "c", 77 | "iosfwd": "c", 78 | "ranges": "c", 79 | "shared_mutex": "c", 80 | "sstream": "c", 81 | "stop_token": "c", 82 | "typeindex": "c", 83 | "*.def": "c", 84 | "*.inc": "c", 85 | "elements.h": "c", 86 | "stdint.h": "c", 87 | "timer.h": "c", 88 | "check.h": "c", 89 | "time.h": "c", 90 | "notification_messages.h": "c", 91 | "dolphin.h": "c", 92 | "string.h": "c", 93 | "infrared_i.h": "c", 94 | "furi.h": "c", 95 | "stream_buffer.h": "c", 96 | "flipp_pomodoro_icons.h": "c", 97 | "notification.h": "c", 98 | "stdbool.h": "c", 99 | "cli.h": "c", 100 | "flipp_pomodoro.h": "c", 101 | "stddef.h": "c", 102 | "view_port.h": "c", 103 | "assets_icons.h": "c", 104 | "flipp_pomodoro_app_i.h": "c", 105 | "stdlib.h": "c", 106 | "notifications.h": "c", 107 | "infrared_scene_config.h": "c", 108 | "dap_scene_config.h": "c", 109 | "dap_scene.h": "c", 110 | "flipp_pomodoro_scene.h": "c", 111 | "dap_gui_custom_event.h": "c", 112 | "flipp_pomodoro_app.h": "c", 113 | "lfrfid_debug_scene_config.h": "c", 114 | "flipp_pomodoro_scene_config.h": "c", 115 | "lfrfid_debug_view_tune.h": "c", 116 | "view.h": "c", 117 | "u2f_view.h": "c", 118 | "view_dispatcher.h": "c", 119 | "flipp_pomodoro_timer_view.h": "c", 120 | "log.h": "c", 121 | "mutex.h": "c", 122 | "m-array.h": "c", 123 | "cwctype": "c", 124 | "dolphin_deed.h": "c", 125 | "flipp_pomodoro_info_view.h": "c", 126 | "scene_manager.h": "c", 127 | "unordered_set": "c", 128 | "canvas.h": "c", 129 | "icon_i.h": "c", 130 | "icon_animation_i.h": "c", 131 | "icon.h": "c", 132 | "flipp_pomodoro_settings.h": "c", 133 | "flipp_pomodoro_config_view.h": "c", 134 | "furi_hal_resources.h": "c" 135 | }, 136 | "chatgpt-helper.model": "gpt-4" 137 | } -------------------------------------------------------------------------------- /flipp_pomodoro/helpers/notifications.c: -------------------------------------------------------------------------------- 1 | #include "notifications.h" 2 | 3 | const NotificationSequence stop_all_notification = { 4 | &message_sound_off, 5 | &message_vibro_off, 6 | 7 | &message_red_0, 8 | &message_green_0, 9 | &message_blue_0, 10 | 11 | NULL, 12 | }; 13 | 14 | const NotificationSequence flash_backlight_on_sequence = { 15 | &message_display_backlight_on, 16 | &message_green_255, 17 | NULL, 18 | }; 19 | 20 | const NotificationSequence flash_backlight_off_sequence = { 21 | &message_display_backlight_off, 22 | &message_green_0, 23 | NULL, 24 | }; 25 | 26 | const NotificationSequence vibrate_sequence = { 27 | &message_vibro_on, 28 | &message_delay_250, 29 | &message_vibro_off, 30 | NULL, 31 | }; 32 | 33 | const NotificationSequence soft_beep_sequence = { 34 | &message_display_backlight_on, 35 | &message_note_d5, 36 | &message_delay_250, 37 | &message_sound_off, 38 | NULL, 39 | }; 40 | 41 | const NotificationSequence loud_beep_sequence = { 42 | &message_display_backlight_on, 43 | &message_note_d5, 44 | &message_delay_250, 45 | &message_note_b5, 46 | &message_delay_250, 47 | &message_note_d5, 48 | &message_delay_250, 49 | &message_sound_off, 50 | NULL, 51 | }; 52 | 53 | const NotificationSequence work_start_notification = { 54 | &message_display_backlight_on, 55 | 56 | &message_vibro_on, 57 | 58 | &message_note_b5, 59 | &message_delay_250, 60 | &message_delay_100, 61 | 62 | &message_note_d5, 63 | &message_delay_250, 64 | &message_delay_100, 65 | 66 | &message_sound_off, 67 | &message_vibro_off, 68 | 69 | &message_green_255, 70 | &message_delay_1000, 71 | &message_green_0, 72 | &message_delay_250, 73 | &message_green_255, 74 | &message_delay_1000, 75 | 76 | 77 | NULL, 78 | }; 79 | 80 | const NotificationSequence rest_start_notification = { 81 | &message_display_backlight_on, 82 | 83 | &message_vibro_on, 84 | 85 | &message_note_d5, 86 | &message_delay_250, 87 | 88 | &message_note_b5, 89 | &message_delay_250, 90 | 91 | &message_sound_off, 92 | &message_vibro_off, 93 | 94 | &message_red_255, 95 | &message_delay_1000, 96 | &message_red_0, 97 | &message_delay_250, 98 | &message_red_255, 99 | &message_delay_1000, 100 | 101 | NULL, 102 | }; 103 | 104 | 105 | // Dolphin laughing sound 106 | const NotificationSequence long_break_start_notification = { 107 | &message_display_backlight_on, 108 | 109 | &message_vibro_on, 110 | 111 | &message_note_d3, 112 | &message_delay_50, 113 | &message_note_d5, 114 | &message_delay_100, 115 | &message_note_d7, 116 | &message_delay_250, 117 | &message_sound_off, 118 | &message_delay_250, 119 | 120 | &message_note_d3, 121 | &message_delay_100, 122 | &message_note_d5, 123 | &message_delay_100, 124 | &message_sound_off, 125 | &message_delay_50, 126 | 127 | &message_note_d3, 128 | &message_delay_100, 129 | &message_note_d5, 130 | &message_delay_100, 131 | &message_sound_off, 132 | &message_delay_50, 133 | 134 | &message_note_d3, 135 | &message_delay_100, 136 | &message_note_d5, 137 | &message_delay_100, 138 | &message_sound_off, 139 | &message_delay_50, 140 | 141 | &message_note_d3, 142 | &message_delay_100, 143 | &message_note_d5, 144 | &message_delay_100, 145 | &message_sound_off, 146 | &message_delay_50, 147 | 148 | &message_note_d3, 149 | &message_delay_100, 150 | &message_note_d5, 151 | &message_delay_250, 152 | &message_note_d7, 153 | &message_delay_500, 154 | 155 | &message_sound_off, 156 | &message_vibro_off, 157 | 158 | &message_blue_255, 159 | &message_delay_1000, 160 | &message_blue_0, 161 | &message_delay_250, 162 | &message_blue_255, 163 | &message_delay_1000, 164 | 165 | NULL, 166 | }; 167 | 168 | const NotificationSequence* stage_start_notification_sequence_map[] = { 169 | [FlippPomodoroStageFocus] = &work_start_notification, 170 | [FlippPomodoroStageRest] = &rest_start_notification, 171 | [FlippPomodoroStageLongBreak] = &long_break_start_notification, 172 | }; 173 | -------------------------------------------------------------------------------- /flipp_pomodoro/modules/flipp_pomodoro.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../helpers/time.h" 4 | #include "flipp_pomodoro.h" 5 | #include "flipp_pomodoro_settings.h" 6 | 7 | PomodoroStage stages_sequence[] = { 8 | FlippPomodoroStageFocus, 9 | FlippPomodoroStageRest, 10 | 11 | FlippPomodoroStageFocus, 12 | FlippPomodoroStageRest, 13 | 14 | FlippPomodoroStageFocus, 15 | FlippPomodoroStageRest, 16 | 17 | FlippPomodoroStageFocus, 18 | FlippPomodoroStageLongBreak, 19 | }; 20 | 21 | char *current_stage_label[] = { 22 | [FlippPomodoroStageFocus] = "Focusing...", 23 | [FlippPomodoroStageRest] = "Short Break...", 24 | [FlippPomodoroStageLongBreak] = "Long Break...", 25 | }; 26 | 27 | char *next_stage_label[] = { 28 | [FlippPomodoroStageFocus] = "Focus", 29 | [FlippPomodoroStageRest] = "Short Break", 30 | [FlippPomodoroStageLongBreak] = "Long Break", 31 | }; 32 | 33 | static uint8_t s_focus_min = 25; 34 | static uint8_t s_short_break_min = 5; 35 | static uint8_t s_long_break_min = 30; 36 | 37 | static void flipp_pomodoro__load_settings_or_default(void) { 38 | FlippPomodoroSettings s; 39 | flipp_pomodoro_settings_load(&s); 40 | s_focus_min = s.focus_minutes; 41 | s_short_break_min = s.short_break_minutes; 42 | s_long_break_min = s.long_break_minutes; 43 | } 44 | 45 | void flipp_pomodoro__apply_settings(const FlippPomodoroSettings* s) { 46 | if(!s) return; 47 | s_focus_min = s->focus_minutes; 48 | s_short_break_min = s->short_break_minutes; 49 | s_long_break_min = s->long_break_minutes; 50 | } 51 | 52 | PomodoroStage flipp_pomodoro__stage_by_index(int index) { 53 | const int one_loop_size = (int)(sizeof(stages_sequence) / sizeof(stages_sequence[0])); 54 | return stages_sequence[index % one_loop_size]; 55 | } 56 | 57 | void flipp_pomodoro__toggle_stage(FlippPomodoroState *state) 58 | { 59 | furi_assert(state); 60 | state->current_stage_index = state->current_stage_index + 1; 61 | state->started_at_timestamp = time_now(); 62 | }; 63 | 64 | PomodoroStage flipp_pomodoro__get_stage(FlippPomodoroState *state) 65 | { 66 | furi_assert(state); 67 | return flipp_pomodoro__stage_by_index(state->current_stage_index); 68 | }; 69 | 70 | char *flipp_pomodoro__current_stage_label(FlippPomodoroState *state) 71 | { 72 | furi_assert(state); 73 | return current_stage_label[flipp_pomodoro__get_stage(state)]; 74 | }; 75 | 76 | char *flipp_pomodoro__next_stage_label(FlippPomodoroState *state) 77 | { 78 | furi_assert(state); 79 | return next_stage_label[flipp_pomodoro__stage_by_index(state->current_stage_index + 1)]; 80 | }; 81 | 82 | void flipp_pomodoro__destroy(FlippPomodoroState *state) 83 | { 84 | furi_assert(state); 85 | free(state); 86 | }; 87 | 88 | uint32_t flipp_pomodoro__current_stage_total_duration(FlippPomodoroState *state) 89 | { 90 | const int32_t stage_duration_seconds_map[] = { 91 | [FlippPomodoroStageFocus] = s_focus_min * TIME_SECONDS_IN_MINUTE, 92 | [FlippPomodoroStageRest] = s_short_break_min * TIME_SECONDS_IN_MINUTE, 93 | [FlippPomodoroStageLongBreak] = s_long_break_min * TIME_SECONDS_IN_MINUTE, 94 | }; 95 | 96 | return stage_duration_seconds_map[flipp_pomodoro__get_stage(state)]; 97 | }; 98 | 99 | uint32_t flipp_pomodoro__stage_expires_timestamp(FlippPomodoroState *state) 100 | { 101 | return state->started_at_timestamp + flipp_pomodoro__current_stage_total_duration(state); 102 | }; 103 | 104 | TimeDifference flipp_pomodoro__stage_remaining_duration(FlippPomodoroState *state) 105 | { 106 | const uint32_t stage_ends_at = flipp_pomodoro__stage_expires_timestamp(state); 107 | return time_difference_seconds(time_now(), stage_ends_at); 108 | }; 109 | 110 | bool flipp_pomodoro__is_stage_expired(FlippPomodoroState *state) 111 | { 112 | const uint32_t expired_by = flipp_pomodoro__stage_expires_timestamp(state); 113 | // precise response (there was an unclear bug with "eating" a second) 114 | return time_now() >= expired_by; 115 | }; 116 | 117 | FlippPomodoroState *flipp_pomodoro__new() 118 | { 119 | // ensure durations reflect settings file (or defaults) 120 | flipp_pomodoro__load_settings_or_default(); 121 | 122 | FlippPomodoroState *state = malloc(sizeof(FlippPomodoroState)); 123 | const uint32_t now = time_now(); 124 | state->started_at_timestamp = now; 125 | state->current_stage_index = 0; 126 | return state; 127 | }; 128 | 129 | const char* flipp_pomodoro__settings_button_label() { 130 | return "Settings"; 131 | } 132 | -------------------------------------------------------------------------------- /flipp_pomodoro/views/flipp_pomodoro_info_view.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "flipp_pomodoro_info_view.h" 7 | // Auto-compiled icons 8 | #include "flipp_pomodoro_icons.h" 9 | 10 | enum 11 | { 12 | ViewInputConsumed = true, 13 | ViewInputNotConusmed = false, 14 | }; 15 | 16 | struct FlippPomodoroInfoView 17 | { 18 | View *view; 19 | FlippPomodoroInfoViewUserActionCb resume_timer_cb; 20 | void *user_action_cb_ctx; 21 | }; 22 | 23 | typedef struct 24 | { 25 | uint8_t pomodoros_completed; 26 | FlippPomodoroInfoViewMode mode; 27 | } FlippPomodoroInfoViewModel; 28 | 29 | static void flipp_pomodoro_info_view_draw_statistics(Canvas *canvas, FlippPomodoroInfoViewModel *model) 30 | { 31 | FuriString *stats_string = furi_string_alloc(); 32 | 33 | 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); 34 | const char *stats_string_formatted = furi_string_get_cstr(stats_string); 35 | 36 | elements_text_box( 37 | canvas, 38 | 0, 39 | 0, 40 | canvas_width(canvas), 41 | canvas_height(canvas) - 10, 42 | AlignCenter, 43 | AlignCenter, 44 | stats_string_formatted, 45 | true); 46 | 47 | furi_string_free(stats_string); 48 | 49 | elements_button_left(canvas, "Guide"); 50 | } 51 | 52 | static void flipp_pomodoro_info_view_draw_about(Canvas *canvas, FlippPomodoroInfoViewModel *model) 53 | { 54 | UNUSED(model); 55 | canvas_draw_icon(canvas, 0, 0, &I_flipp_pomodoro_learn_50x128); 56 | elements_button_left(canvas, "Stats"); 57 | } 58 | 59 | static void flipp_pomodoro_info_view_draw_callback(Canvas *canvas, void *_model) 60 | { 61 | if (!_model) 62 | { 63 | return; 64 | }; 65 | 66 | FlippPomodoroInfoViewModel *model = _model; 67 | 68 | canvas_clear(canvas); 69 | 70 | if (model->mode == FlippPomodoroInfoViewModeStats) 71 | { 72 | flipp_pomodoro_info_view_draw_statistics(canvas, model); 73 | } 74 | else 75 | { 76 | flipp_pomodoro_info_view_draw_about(canvas, model); 77 | } 78 | 79 | elements_button_right(canvas, "Resume"); 80 | } 81 | 82 | void flipp_pomodoro_info_view_set_mode(View *view, FlippPomodoroInfoViewMode desired_mode) 83 | { 84 | with_view_model( 85 | view, 86 | FlippPomodoroInfoViewModel * model, 87 | { 88 | model->mode = desired_mode; 89 | }, 90 | false); 91 | } 92 | 93 | void flipp_pomodoro_info_view_toggle_mode(FlippPomodoroInfoView *info_view) 94 | { 95 | with_view_model( 96 | flipp_pomodoro_info_view_get_view(info_view), 97 | FlippPomodoroInfoViewModel * model, 98 | { 99 | flipp_pomodoro_info_view_set_mode( 100 | flipp_pomodoro_info_view_get_view(info_view), 101 | (model->mode == FlippPomodoroInfoViewModeStats) ? FlippPomodoroInfoViewModeAbout : FlippPomodoroInfoViewModeStats); 102 | }, 103 | true); 104 | } 105 | 106 | bool flipp_pomodoro_info_view_input_callback(InputEvent *event, void *ctx) 107 | { 108 | FlippPomodoroInfoView *info_view = ctx; 109 | 110 | if (event->type == InputTypePress) 111 | { 112 | if (event->key == InputKeyRight && info_view->resume_timer_cb != NULL) 113 | { 114 | info_view->resume_timer_cb(info_view->user_action_cb_ctx); 115 | return ViewInputConsumed; 116 | } 117 | else if (event->key == InputKeyLeft) 118 | { 119 | flipp_pomodoro_info_view_toggle_mode(info_view); 120 | return ViewInputConsumed; 121 | } 122 | } 123 | 124 | return ViewInputNotConusmed; 125 | } 126 | 127 | FlippPomodoroInfoView *flipp_pomodoro_info_view_alloc() 128 | { 129 | FlippPomodoroInfoView *info_view = malloc(sizeof(FlippPomodoroInfoView)); 130 | info_view->view = view_alloc(); 131 | 132 | view_allocate_model(flipp_pomodoro_info_view_get_view(info_view), ViewModelTypeLockFree, sizeof(FlippPomodoroInfoViewModel)); 133 | view_set_context(flipp_pomodoro_info_view_get_view(info_view), info_view); 134 | view_set_draw_callback(flipp_pomodoro_info_view_get_view(info_view), flipp_pomodoro_info_view_draw_callback); 135 | view_set_input_callback(flipp_pomodoro_info_view_get_view(info_view), flipp_pomodoro_info_view_input_callback); 136 | 137 | return info_view; 138 | } 139 | 140 | View *flipp_pomodoro_info_view_get_view(FlippPomodoroInfoView *info_view) 141 | { 142 | return info_view->view; 143 | } 144 | 145 | void flipp_pomodoro_info_view_free(FlippPomodoroInfoView *info_view) 146 | { 147 | furi_assert(info_view); 148 | view_free(info_view->view); 149 | free(info_view); 150 | } 151 | 152 | void flipp_pomodoro_info_view_set_pomodoros_completed(View *view, uint8_t pomodoros_completed) 153 | { 154 | with_view_model( 155 | view, 156 | FlippPomodoroInfoViewModel * model, 157 | { 158 | model->pomodoros_completed = pomodoros_completed; 159 | }, 160 | false); 161 | } 162 | 163 | void flipp_pomodoro_info_view_set_resume_timer_cb(FlippPomodoroInfoView *info_view, FlippPomodoroInfoViewUserActionCb user_action_cb, void *user_action_cb_ctx) 164 | { 165 | info_view->resume_timer_cb = user_action_cb; 166 | info_view->user_action_cb_ctx = user_action_cb_ctx; 167 | } 168 | -------------------------------------------------------------------------------- /flipp_pomodoro/flipp_pomodoro_app.c: -------------------------------------------------------------------------------- 1 | #include "flipp_pomodoro_app_i.h" 2 | 3 | #define TAG "FlippPomodoro" 4 | 5 | enum 6 | { 7 | CustomEventConsumed = true, 8 | CustomEventNotConsumed = false, 9 | }; 10 | 11 | static bool flipp_pomodoro_app_back_event_callback(void *ctx) 12 | { 13 | furi_assert(ctx); 14 | FlippPomodoroApp *app = ctx; 15 | return scene_manager_handle_back_event(app->scene_manager); 16 | }; 17 | 18 | static void flipp_pomodoro_app_tick_event_callback(void *ctx) 19 | { 20 | furi_assert(ctx); 21 | FlippPomodoroApp *app = ctx; 22 | 23 | scene_manager_handle_custom_event(app->scene_manager, FlippPomodoroAppCustomEventTimerTick); 24 | }; 25 | 26 | static bool flipp_pomodoro_app_custom_event_callback(void *ctx, uint32_t event) 27 | { 28 | furi_assert(ctx); 29 | FlippPomodoroApp *app = ctx; 30 | 31 | switch (event) 32 | { 33 | case FlippPomodoroAppCustomEventStageSkip: 34 | flipp_pomodoro__toggle_stage(app->state); 35 | view_dispatcher_send_custom_event( 36 | app->view_dispatcher, 37 | FlippPomodoroAppCustomEventStateUpdated); 38 | return CustomEventConsumed; 39 | case FlippPomodoroAppCustomEventStageComplete: 40 | if (flipp_pomodoro__get_stage(app->state) == FlippPomodoroStageFocus) 41 | { 42 | // REGISTER a deed on work stage complete to get an acheivement 43 | dolphin_deed(DolphinDeedPluginGameWin); 44 | FURI_LOG_I(TAG, "Focus stage reward added"); 45 | 46 | flipp_pomodoro_statistics__increase_focus_stages_completed(app->statistics); 47 | }; 48 | 49 | flipp_pomodoro__toggle_stage(app->state); 50 | 51 | PomodoroStage next_stage = flipp_pomodoro__get_stage(app->state); 52 | FlippPomodoroSettings settings; 53 | flipp_pomodoro_settings_load(&settings); 54 | // Keep flash mode completely silent regardless of target stage. 55 | bool skip_beep = (settings.buzz_mode == FlippPomodoroBuzzFlash); 56 | 57 | if(!skip_beep) { 58 | notification_message(app->notification_app, stage_start_notification_sequence_map[next_stage]); 59 | } 60 | view_dispatcher_send_custom_event( 61 | app->view_dispatcher, 62 | FlippPomodoroAppCustomEventStateUpdated); 63 | return CustomEventConsumed; 64 | default: 65 | break; 66 | } 67 | return scene_manager_handle_custom_event(app->scene_manager, event); 68 | }; 69 | 70 | FlippPomodoroApp *flipp_pomodoro_app_alloc() 71 | { 72 | FlippPomodoroApp *app = malloc(sizeof(FlippPomodoroApp)); 73 | app->state = flipp_pomodoro__new(); 74 | 75 | app->scene_manager = scene_manager_alloc(&flipp_pomodoro_scene_handlers, app); 76 | app->gui = furi_record_open(RECORD_GUI); 77 | app->notification_app = furi_record_open(RECORD_NOTIFICATION); 78 | app->notification_manager = notification_manager_alloc(); 79 | 80 | app->view_dispatcher = view_dispatcher_alloc(); 81 | app->statistics = flipp_pomodoro_statistics__new(); 82 | 83 | view_dispatcher_set_event_callback_context(app->view_dispatcher, app); 84 | view_dispatcher_set_custom_event_callback(app->view_dispatcher, flipp_pomodoro_app_custom_event_callback); 85 | view_dispatcher_set_tick_event_callback(app->view_dispatcher, flipp_pomodoro_app_tick_event_callback, 1000); 86 | view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); 87 | view_dispatcher_set_navigation_event_callback(app->view_dispatcher, flipp_pomodoro_app_back_event_callback); 88 | 89 | app->config_view = flipp_pomodoro_view_config_alloc(); 90 | app->timer_view = flipp_pomodoro_view_timer_alloc(); 91 | app->info_view = flipp_pomodoro_info_view_alloc(); 92 | 93 | view_dispatcher_add_view( 94 | app->view_dispatcher, 95 | FlippPomodoroAppViewTimer, 96 | flipp_pomodoro_view_timer_get_view(app->timer_view)); 97 | 98 | view_dispatcher_add_view( 99 | app->view_dispatcher, 100 | FlippPomodoroAppViewInfo, 101 | flipp_pomodoro_info_view_get_view(app->info_view)); 102 | 103 | view_dispatcher_add_view( 104 | app->view_dispatcher, 105 | FlippPomodoroAppViewConfig, 106 | flipp_pomodoro_view_config_get_view(app->config_view)); 107 | 108 | scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneTimer); 109 | FURI_LOG_I(TAG, "Alloc complete"); 110 | return app; 111 | }; 112 | 113 | void flipp_pomodoro_app_free(FlippPomodoroApp *app) 114 | { 115 | view_dispatcher_remove_view(app->view_dispatcher, FlippPomodoroAppViewTimer); 116 | view_dispatcher_remove_view(app->view_dispatcher, FlippPomodoroAppViewInfo); 117 | view_dispatcher_remove_view(app->view_dispatcher, FlippPomodoroAppViewConfig); 118 | view_dispatcher_free(app->view_dispatcher); 119 | scene_manager_free(app->scene_manager); 120 | flipp_pomodoro_view_timer_free(app->timer_view); 121 | flipp_pomodoro_view_config_free(app->config_view); 122 | flipp_pomodoro_info_view_free(app->info_view); 123 | flipp_pomodoro_statistics__destroy(app->statistics); 124 | flipp_pomodoro__destroy(app->state); 125 | notification_manager_free(app->notification_manager); 126 | free(app); 127 | furi_record_close(RECORD_GUI); 128 | furi_record_close(RECORD_NOTIFICATION); 129 | }; 130 | 131 | 132 | int32_t flipp_pomodoro_app(void *p) 133 | { 134 | UNUSED(p); 135 | FURI_LOG_I(TAG, "Initial"); 136 | FlippPomodoroApp *app = flipp_pomodoro_app_alloc(); 137 | 138 | FURI_LOG_I(TAG, "Run deed added"); 139 | dolphin_deed(DolphinDeedPluginGameStart); 140 | 141 | view_dispatcher_run(app->view_dispatcher); 142 | 143 | flipp_pomodoro_app_free(app); 144 | 145 | return 0; 146 | }; 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flipp Pomodoro 2 | ![Banner Image](docs/headline.gif) 3 | 4 | >Boost Your Productivity with the Pomodoro Timer for Flipper Zero! Don't let your flipper get bored, let him help you instead. 5 | 6 | --- 7 | * [Install](#install) 8 | * [Guide](#guide) 9 | * [Contributing](#contributing) 10 | * [Development](#development) 11 | + [Track](#track) 12 | + [Build and Package](#build-and-package) 13 | 14 | ## Install 15 | [![Total Downloads](https://img.shields.io/github/downloads/Th3Un1q3/flipp_pomodoro/total?style=for-the-badge)](https://github.com/Th3Un1q3/flipp_pomodoro/releases/latest) 16 | 17 | [![Latest Release Date](https://img.shields.io/github/release-date/Th3Un1q3/flipp_pomodoro?label=Latest%20release%20&style=for-the-badge) ![Latest Release](https://img.shields.io/github/v/release/Th3Un1q3/flipp_pomodoro?label=Latest%20version&style=for-the-badge) ![GitHub release (latest by date)](https://img.shields.io/github/downloads/Th3Un1q3/flipp_pomodoro/latest/total?label=Latest%20downloaded&style=for-the-badge)](https://github.com/Th3Un1q3/flipp_pomodoro/releases/latest) 18 | 19 | ### From Repository 20 | 21 | Download from [releases](https://github.com/Th3Un1q3/flipp_pomodoro/releases/latest) and follow instructions there. 22 | 23 | ### From Application Marketplace 24 | 25 | - Using `Flipper` Mobile App 26 | - Open `Hub` tab, then open `Apps` 27 | - Find and install `Flipp Pomodoro` 28 | 29 | ### Bundled into Firmware 30 | 31 | `Flipp Pomodoro` is prepacked in [unleased firmware](https://github.com/DarkFlippers/unleashed-firmware/releases) releases suffixed with `e`. Once you have the firmware installed, you can find `Flipp Pomodoro` among `Tools`. 32 | 33 | ## Guide 34 | 35 | [About Pomodoro Technique](https://francescocirillo.com/products/the-pomodoro-technique) 36 | 37 | ### `Focus` Phase 38 | 39 | ![Working Screen](docs/working.png) 40 | 41 | Do the following: 42 | * Check your past notes 43 | * Choose a task and note it down 44 | * Work on the task until the Flipp Pomodoro rings 45 | * If task is done during `Focus` phase 46 | * Review completed work 47 | * Plan a task to focus on next 48 | * Reflect what have you have learned 49 | * If task is not complete by the end of the `Focus` phase 50 | * Place a bookmark and return to it during the next cycle 51 | * Think how to define task the way it would fit into a single phase 52 | * If there is some distraction(remember: many things can wait 25 minutes) 53 | * Incomming message - take a note and answer during `Rest` phase 54 | * Genius idea - note a hint down, plan as a task or return to it when work is done 55 | * Everything unavoidable - feel free to leave the cycle, once it's mitigated you have task to work on 56 | 57 | > Hint: By completing `Focus` phase your flipper gains good mood boost 58 | 59 | ### `Rest` Phase 60 | ![Resting Screen](docs/resting.png) 61 | 62 | Do the following: 63 | * Take a walk around or do a little stretch 64 | * Take some fresh air 65 | * Refill your drink 66 | * Answer pending messages 67 | * Talk to a colleague 68 | 69 | ### `Settings` 70 | ![Settings Screen](docs/settings.png) 71 | 72 | #### About the Menu 73 | All settings are persistent — they are saved automatically and reloaded when the app restarts. 74 | You can exit the settings menu without saving changes if needed. 75 | Entering the settings menu will pause any active timer — use this intentionally, as excessive use breaks the spirit of the Pomodoro technique. 76 | 77 | #### Available Settings 78 | You can configure: 79 | 80 | - Focus time 81 | - Short break 82 | - Long break 83 | - Notification mode (`Buzz Mode`) 84 | 85 | #### Buzz Modes 86 | - **Slide** — Notifies you and immediately starts the next timer. 87 | - **Once** — Notifies you once, then waits for manual confirmation. 88 | - **Naggy** — Sends 10 consecutive alerts until you press the center button, open the settings menu, or start a new timer manually. 89 | - **Flash** — Keeps toggling the backlight/LED without playing any sound until you acknowledge the stage change. 90 | - **Vibrate** — Emits repeating, silent vibration bursts and flashes the screen until you acknowledge the stage change. 91 | - **Beep soft** — Plays a gentle repeating chime pattern so you can catch the change without startling everyone around you. 92 | - **Beep loud** — Uses a longer, high-contrast beep pattern to make sure you notice the finished stage even in noisy environments. 93 | 94 | 95 | ## Contributing 96 | 97 | [![GitHub Discussions](https://img.shields.io/github/discussions/Th3Un1q3/flipp_pomodoro?style=for-the-badge)](https://github.com/Th3Un1q3/flipp_pomodoro/discussions) 98 | 99 | I welcome contributions to this project! If you're interested in helping out, here are a few ways to get started: 100 | - Join [discussions](https://github.com/Th3Un1q3/flipp_pomodoro/discussions) to ask questions and share ideas with other contributors. 101 | - If you've found a bug or have an idea for a new feature, please open an issue on [issue tracker](https://github.com/Th3Un1q3/flipp_pomodoro/issues). Before opening a new issue, please search the existing issues to see if someone has already reported the problem. 102 | - If you're ready to start contributing code, please [fork](https://github.com/Th3Un1q3/flipp_pomodoro/fork) this GitHub repository and submit a [pull request](https://github.com/Th3Un1q3/flipp_pomodoro/pulls). 103 | 104 | ## Development 105 | 106 | ![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/m/Th3Un1q3/flipp_pomodoro?style=for-the-badge) 107 | 108 | ### Track 109 | You can follow project development on the [project board](https://github.com/users/Th3Un1q3/projects/1). 110 | 111 | 112 | ### Build and Package 113 | Build application commands 114 | ```shell 115 | # For standard(official) firmware 116 | bash tools/build.sh 117 | 118 | # For unleashed firmware 119 | bash tools/build.sh -f unleashed 120 | 121 | # While flipper connected via USB and serial port is not bussy 122 | # Build, run on flipper and keep the app in `Tools` directory 123 | bash tools/build.sh -f unleashed -i 124 | ``` 125 | 126 | To try the app on real hardware: 127 | - Connect your Flipper Zero over USB and make sure no other process is using the serial port. 128 | - Run `bash tools/build.sh -i` (add `-f unleashed` if you're targeting the unleashed firmware). 129 | - Once flashing completes, open `Tools → Flipp Pomodoro` on the device to start the timer. 130 | 131 | ### Developer Environment 132 | 133 | If you want to build with [ufbt](https://github.com/flipperdevices/flipperzero-ufbt) without touching your global Python installation: 134 | 135 | ```shell 136 | # create (once) and activate the virtualenv 137 | python3 -m venv .venv_ufbt 138 | source .venv_ufbt/bin/activate 139 | 140 | # install ufbt locally 141 | python -m pip install --upgrade ufbt 142 | 143 | # build the app 144 | bash tools/build.sh 145 | 146 | # when you're done 147 | deactivate 148 | ``` 149 | -------------------------------------------------------------------------------- /flipp_pomodoro/scenes/flipp_pomodoro_scene_timer.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "flipp_pomodoro_scene.h" 6 | #include "../flipp_pomodoro_app.h" 7 | #include "../views/flipp_pomodoro_timer_view.h" 8 | #include "../modules/flipp_pomodoro_settings.h" 9 | #include "../modules/flipp_pomodoro.h" 10 | #include "../helpers/notification_manager.h" 11 | #include "../helpers/time.h" 12 | #include "../helpers/hints.h" 13 | 14 | enum 15 | { 16 | SceneEventConusmed = true, 17 | SceneEventNotConusmed = false 18 | }; 19 | 20 | static char *random_string_of_list(char **hints, size_t num_hints) 21 | { 22 | int random_index = rand() % num_hints; 23 | return hints[random_index]; 24 | } 25 | 26 | void flipp_pomodoro_scene_timer_sync_view_state(void *ctx) 27 | { 28 | furi_assert(ctx); 29 | 30 | FlippPomodoroApp *app = ctx; 31 | 32 | flipp_pomodoro_view_timer_set_state( 33 | flipp_pomodoro_view_timer_get_view(app->timer_view), 34 | app->state); 35 | }; 36 | 37 | void flipp_pomodoro_scene_timer_on_next_stage(void* ctx) { 38 | furi_assert(ctx); 39 | FlippPomodoroApp* app = ctx; 40 | notification_manager_stop(app->notification_manager); 41 | const bool expired = flipp_pomodoro__is_stage_expired(app->state); 42 | view_dispatcher_send_custom_event( 43 | app->view_dispatcher, 44 | expired ? FlippPomodoroAppCustomEventStageComplete 45 | : FlippPomodoroAppCustomEventStageSkip); 46 | } 47 | 48 | void flipp_pomodoro_scene_timer_on_left(void* ctx) { 49 | FlippPomodoroApp* app = ctx; 50 | notification_manager_stop(app->notification_manager); 51 | scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneConfig); 52 | } 53 | 54 | void flipp_pomodoro_scene_timer_on_ask_hint(void *ctx) 55 | { 56 | FlippPomodoroApp *app = ctx; 57 | notification_manager_stop(app->notification_manager); 58 | view_dispatcher_send_custom_event( 59 | app->view_dispatcher, 60 | FlippPomodoroAppCustomEventTimerAskHint); 61 | } 62 | 63 | void flipp_pomodoro_scene_timer_on_enter(void *ctx) 64 | { 65 | furi_assert(ctx); 66 | 67 | FlippPomodoroApp *app = ctx; 68 | notification_manager_reset(app->notification_manager); 69 | if (flipp_pomodoro__is_stage_expired(app->state)) 70 | { 71 | FlippPomodoroSettings s; 72 | flipp_pomodoro_settings_load(&s); 73 | if(s.buzz_mode == FlippPomodoroBuzzSlide) { 74 | flipp_pomodoro__destroy(app->state); 75 | app->state = flipp_pomodoro__new(); 76 | } else { 77 | notification_manager_stop(app->notification_manager); 78 | PomodoroStage next_stage = flipp_pomodoro__stage_by_index(app->state->current_stage_index + 1); 79 | notification_manager_handle_expired_stage( 80 | app->notification_manager, 81 | next_stage, 82 | s.buzz_mode); 83 | } 84 | } 85 | 86 | view_dispatcher_switch_to_view(app->view_dispatcher, FlippPomodoroAppViewTimer); 87 | 88 | flipp_pomodoro_scene_timer_sync_view_state(app); 89 | 90 | flipp_pomodoro_view_timer_set_callback_context(app->timer_view, app); 91 | 92 | flipp_pomodoro_view_timer_set_on_ok_cb( 93 | app->timer_view, 94 | flipp_pomodoro_scene_timer_on_ask_hint); 95 | 96 | flipp_pomodoro_view_timer_set_on_right_cb( 97 | app->timer_view, 98 | flipp_pomodoro_scene_timer_on_next_stage); 99 | 100 | flipp_pomodoro_view_timer_set_on_left_cb( 101 | app->timer_view, 102 | flipp_pomodoro_scene_timer_on_left); 103 | }; 104 | 105 | char *flipp_pomodoro_scene_timer_get_contextual_hint(FlippPomodoroApp *app) 106 | { 107 | switch (flipp_pomodoro__get_stage(app->state)) 108 | { 109 | case FlippPomodoroStageFocus: 110 | return random_string_of_list(work_hints, sizeof(work_hints) / sizeof(work_hints[0])); 111 | case FlippPomodoroStageRest: 112 | case FlippPomodoroStageLongBreak: 113 | return random_string_of_list(break_hints, BREAK_HINTS_COUNT); 114 | default: 115 | return "What's up?"; 116 | } 117 | } 118 | 119 | void flipp_pomodoro_scene_timer_handle_custom_event(FlippPomodoroApp *app, FlippPomodoroAppCustomEvent custom_event) 120 | { 121 | switch (custom_event) 122 | { 123 | case FlippPomodoroAppCustomEventTimerTick: { 124 | if (!flipp_pomodoro__is_stage_expired(app->state)) { 125 | notification_manager_reset_flags(app->notification_manager); 126 | break; 127 | } 128 | FlippPomodoroSettings s; 129 | flipp_pomodoro_settings_load(&s); 130 | 131 | PomodoroStage next_stage = flipp_pomodoro__stage_by_index(app->state->current_stage_index + 1); 132 | 133 | bool should_send_complete = notification_manager_handle_expired_stage( 134 | app->notification_manager, 135 | next_stage, 136 | s.buzz_mode); 137 | if(should_send_complete) { 138 | view_dispatcher_send_custom_event( 139 | app->view_dispatcher, 140 | FlippPomodoroAppCustomEventStageComplete); 141 | } 142 | break; 143 | } 144 | case FlippPomodoroAppCustomEventStateUpdated: 145 | flipp_pomodoro_scene_timer_sync_view_state(app); 146 | flipp_pomodoro_view_timer_set_on_ok_cb( 147 | app->timer_view, 148 | flipp_pomodoro_scene_timer_on_ask_hint); 149 | notification_manager_reset_flags(app->notification_manager); 150 | notification_manager_stop(app->notification_manager); 151 | break; 152 | case FlippPomodoroAppCustomEventTimerAskHint: 153 | flipp_pomodoro_view_timer_display_hint( 154 | flipp_pomodoro_view_timer_get_view(app->timer_view), 155 | flipp_pomodoro_scene_timer_get_contextual_hint(app)); 156 | break; 157 | default: 158 | break; 159 | } 160 | }; 161 | 162 | bool flipp_pomodoro_scene_timer_on_event(void *ctx, SceneManagerEvent event) 163 | { 164 | furi_assert(ctx); 165 | FlippPomodoroApp *app = ctx; 166 | 167 | switch (event.type) 168 | { 169 | case SceneManagerEventTypeCustom: 170 | flipp_pomodoro_scene_timer_handle_custom_event( 171 | app, 172 | event.event); 173 | return SceneEventConusmed; 174 | case SceneManagerEventTypeBack: 175 | notification_manager_stop(app->notification_manager); 176 | scene_manager_next_scene(app->scene_manager, FlippPomodoroSceneInfo); 177 | return SceneEventConusmed; 178 | default: 179 | break; 180 | }; 181 | return SceneEventNotConusmed; 182 | }; 183 | 184 | void flipp_pomodoro_scene_timer_on_exit(void *ctx) 185 | { 186 | furi_assert(ctx); 187 | FlippPomodoroApp *app = ctx; 188 | notification_manager_stop(app->notification_manager); 189 | }; 190 | -------------------------------------------------------------------------------- /flipp_pomodoro/helpers/notification_manager.c: -------------------------------------------------------------------------------- 1 | #include "notification_manager.h" 2 | #include "notifications.h" 3 | #include "../helpers/time.h" 4 | #include 5 | #include 6 | 7 | // Notification behavior constants 8 | #define ANNOYING_MODE_REPEAT_COUNT 10 9 | 10 | // Handler return value signals 11 | #define SIGNAL_SEND_STAGE_COMPLETE_EVENT true 12 | #define SIGNAL_NO_STAGE_COMPLETE_EVENT false 13 | 14 | struct NotificationManager { 15 | bool stage_complete_sent; 16 | bool notification_started; 17 | uint8_t notification_repeats_left; 18 | uint32_t notification_cooldown_until; 19 | bool flash_backlight_on; 20 | }; 21 | 22 | NotificationManager* notification_manager_alloc(void) { 23 | NotificationManager* manager = malloc(sizeof(NotificationManager)); 24 | furi_assert(manager); 25 | notification_manager_reset(manager); 26 | return manager; 27 | } 28 | 29 | void notification_manager_free(NotificationManager* manager) { 30 | furi_assert(manager); 31 | free(manager); 32 | } 33 | 34 | void notification_manager_reset(NotificationManager* manager) { 35 | furi_assert(manager); 36 | manager->stage_complete_sent = false; 37 | manager->notification_started = false; 38 | manager->notification_repeats_left = 0; 39 | manager->notification_cooldown_until = 0; 40 | manager->flash_backlight_on = false; 41 | } 42 | 43 | void notification_manager_reset_flags(NotificationManager* manager) { 44 | furi_assert(manager); 45 | notification_manager_reset(manager); 46 | } 47 | 48 | static void send_notification_sequence(const NotificationSequence* sequence) { 49 | if(!sequence) { 50 | return; 51 | } 52 | NotificationApp* notification_app = furi_record_open(RECORD_NOTIFICATION); 53 | notification_message(notification_app, sequence); 54 | furi_record_close(RECORD_NOTIFICATION); 55 | } 56 | 57 | static void stop_all_notifications(void) { 58 | NotificationApp* notification_app = furi_record_open(RECORD_NOTIFICATION); 59 | notification_message(notification_app, &stop_all_notification); 60 | furi_record_close(RECORD_NOTIFICATION); 61 | } 62 | 63 | void notification_manager_stop(NotificationManager* manager) { 64 | furi_assert(manager); 65 | manager->notification_repeats_left = 0; 66 | manager->notification_cooldown_until = time_now() + 1; 67 | manager->notification_started = false; 68 | manager->flash_backlight_on = false; 69 | stop_all_notifications(); 70 | } 71 | 72 | static void toggle_flash_pattern(NotificationManager* manager) { 73 | send_notification_sequence(manager->flash_backlight_on ? &flash_backlight_off_sequence : &flash_backlight_on_sequence); 74 | manager->flash_backlight_on = !manager->flash_backlight_on; 75 | } 76 | 77 | static bool is_cooldown_active(NotificationManager* manager) { 78 | const uint32_t current_time = time_now(); 79 | return current_time < manager->notification_cooldown_until; 80 | } 81 | 82 | static void set_cooldown(NotificationManager* manager, uint32_t cooldown_seconds) { 83 | const uint32_t current_time = time_now(); 84 | manager->notification_started = true; 85 | manager->notification_cooldown_until = current_time + cooldown_seconds; 86 | } 87 | 88 | static void notify_next_stage(NotificationManager* manager, PomodoroStage next_stage) { 89 | if(is_cooldown_active(manager)) { 90 | return; 91 | } 92 | 93 | const NotificationSequence* notification_sequence = stage_start_notification_sequence_map[next_stage]; 94 | 95 | send_notification_sequence(notification_sequence); 96 | manager->notification_cooldown_until = time_now() + 3; 97 | } 98 | 99 | static bool handle_buzz_slide(NotificationManager* manager) { 100 | if(!manager->stage_complete_sent) { 101 | manager->stage_complete_sent = true; 102 | return SIGNAL_SEND_STAGE_COMPLETE_EVENT; 103 | } 104 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 105 | } 106 | 107 | static bool handle_buzz_once(NotificationManager* manager, PomodoroStage next_stage) { 108 | if(!manager->notification_started) { 109 | manager->notification_started = true; 110 | notify_next_stage(manager, next_stage); 111 | } 112 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 113 | } 114 | 115 | static bool handle_buzz_annoying(NotificationManager* manager, PomodoroStage next_stage) { 116 | if(!manager->notification_started) { 117 | manager->notification_started = true; 118 | manager->notification_repeats_left = ANNOYING_MODE_REPEAT_COUNT; 119 | } 120 | 121 | if(manager->notification_repeats_left > 0) { 122 | notify_next_stage(manager, next_stage); 123 | manager->notification_repeats_left--; 124 | } 125 | 126 | if(manager->notification_repeats_left <= 0) { 127 | stop_all_notifications(); 128 | } 129 | 130 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 131 | } 132 | 133 | static bool handle_buzz_flash(NotificationManager* manager) { 134 | if(is_cooldown_active(manager)) { 135 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 136 | } 137 | toggle_flash_pattern(manager); 138 | set_cooldown(manager, 1); 139 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 140 | } 141 | 142 | static bool handle_buzz_vibrate(NotificationManager* manager) { 143 | if(is_cooldown_active(manager)) { 144 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 145 | } 146 | send_notification_sequence(&vibrate_sequence); 147 | toggle_flash_pattern(manager); 148 | set_cooldown(manager, 1); 149 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 150 | } 151 | 152 | static bool handle_buzz_soft_beep(NotificationManager* manager) { 153 | if(is_cooldown_active(manager)) { 154 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 155 | } 156 | send_notification_sequence(&soft_beep_sequence); 157 | set_cooldown(manager, 2); 158 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 159 | } 160 | 161 | static bool handle_buzz_loud_beep(NotificationManager* manager) { 162 | if(is_cooldown_active(manager)) { 163 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 164 | } 165 | send_notification_sequence(&loud_beep_sequence); 166 | set_cooldown(manager, 3); 167 | return SIGNAL_NO_STAGE_COMPLETE_EVENT; 168 | } 169 | 170 | bool notification_manager_handle_expired_stage( 171 | NotificationManager* manager, 172 | PomodoroStage next_stage, 173 | FlippPomodoroBuzzMode buzz_mode) { 174 | 175 | furi_assert(manager); 176 | 177 | switch(buzz_mode) { 178 | case FlippPomodoroBuzzSlide: 179 | return handle_buzz_slide(manager); 180 | case FlippPomodoroBuzzOnce: 181 | return handle_buzz_once(manager, next_stage); 182 | case FlippPomodoroBuzzAnnoying: 183 | return handle_buzz_annoying(manager, next_stage); 184 | case FlippPomodoroBuzzFlash: 185 | return handle_buzz_flash(manager); 186 | case FlippPomodoroBuzzVibrate: 187 | return handle_buzz_vibrate(manager); 188 | case FlippPomodoroBuzzSoftBeep: 189 | return handle_buzz_soft_beep(manager); 190 | case FlippPomodoroBuzzLoudBeep: 191 | return handle_buzz_loud_beep(manager); 192 | default: 193 | return handle_buzz_once(manager, next_stage); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /flipp_pomodoro/views/flipp_pomodoro_config_view.c: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "flipp_pomodoro_config_view.h" 4 | #include "../modules/flipp_pomodoro_settings.h" 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | 14 | 15 | const uint8_t _I_back_10px_0[] = {0x00,0x00,0x00,0x10,0x00,0x38,0x00,0x7c,0x00,0xfe,0x00,0x38,0x00,0x38,0x00,0xf8,0x01,0xf8,0x01,0x00,0x00,}; 16 | const uint8_t* const _I_back_10px[] = {_I_back_10px_0}; 17 | const Icon I_back_10px = {.width=10,.height=10,.frame_count=1,.frame_rate=0,.frames=_I_back_10px}; 18 | 19 | typedef struct { 20 | uint8_t selected; // index of the selected row: 0=focus, 1=short, 2=long, 3=buzz mode 21 | uint8_t durations[3]; // minutes for the first 3 rows (displayed as number + " min") 22 | uint8_t buzz_mode; // notification mode 23 | } FlippPomodoroConfigViewModel; 24 | 25 | struct FlippPomodoroConfigView { 26 | View* view; 27 | FlippPomodoroConfigViewActionCb on_save_cb; 28 | void* cb_ctx; 29 | }; 30 | 31 | static const char* buzz_mode_to_str(uint8_t m) { 32 | switch(m) { 33 | case FlippPomodoroBuzzSlide: return "Slide"; 34 | case FlippPomodoroBuzzOnce: return "Once"; 35 | case FlippPomodoroBuzzAnnoying: return "Naggy"; 36 | case FlippPomodoroBuzzFlash: return "Flash"; 37 | case FlippPomodoroBuzzVibrate: return "Vibrate"; 38 | case FlippPomodoroBuzzSoftBeep: return "Beep soft"; 39 | case FlippPomodoroBuzzLoudBeep: return "Beep loud"; 40 | default: return "?"; 41 | } 42 | } 43 | 44 | static uint8_t buzz_mode_sanitise(uint8_t mode) { 45 | if(mode >= FlippPomodoroBuzzModeCount) { 46 | return FlippPomodoroBuzzOnce; 47 | } 48 | return mode; 49 | } 50 | 51 | static uint8_t buzz_mode_next(uint8_t mode) { 52 | return (mode + 1) % FlippPomodoroBuzzModeCount; 53 | } 54 | 55 | static uint8_t buzz_mode_prev(uint8_t mode) { 56 | return (mode + FlippPomodoroBuzzModeCount - 1) % FlippPomodoroBuzzModeCount; 57 | } 58 | 59 | 60 | void elements_button_back(Canvas* canvas, const char* str) { 61 | furi_check(canvas); 62 | 63 | const size_t button_height = 12; 64 | const size_t vertical_offset = 3; 65 | const size_t horizontal_offset = 3; 66 | const size_t string_width = canvas_string_width(canvas, str); 67 | const Icon* icon = &I_back_10px; 68 | const int32_t icon_h_offset = 3; 69 | const int32_t icon_width_with_offset = icon->width + icon_h_offset; 70 | const int32_t icon_v_offset = icon->height + 1; 71 | const size_t button_width = string_width + horizontal_offset * 2 + icon_width_with_offset; 72 | 73 | const int32_t x = canvas_width(canvas); 74 | const int32_t y = canvas_height(canvas); 75 | 76 | canvas_draw_box(canvas, x - button_width, y - button_height, button_width, button_height); 77 | canvas_draw_line(canvas, x - button_width - 1, y, x - button_width - 1, y - button_height + 0); 78 | canvas_draw_line(canvas, x - button_width - 2, y, x - button_width - 2, y - button_height + 1); 79 | canvas_draw_line(canvas, x - button_width - 3, y, x - button_width - 3, y - button_height + 2); 80 | 81 | canvas_invert_color(canvas); 82 | canvas_draw_str(canvas, x - button_width + horizontal_offset, y - vertical_offset, str); 83 | canvas_draw_icon( 84 | canvas, x - horizontal_offset - icon->width, y - icon_v_offset, icon); 85 | canvas_invert_color(canvas); 86 | } 87 | 88 | static void config_draw_callback(Canvas* canvas, void* ctx) { 89 | FlippPomodoroConfigViewModel* model = ctx; 90 | canvas_clear(canvas); // clear the screen 91 | 92 | const char* labels[4] = {"Focus:", "Short Break:", "Long Break:", "Buzz Mode:"}; // labels on the left (x_label) 93 | 94 | // Vertical layout of text: 95 | const uint8_t y_base = 10; // base Y for the first row 96 | const uint8_t row_h = 12; // vertical spacing between rows 97 | 98 | for(uint8_t i = 0; i < 4; i++) { // 4 UI rows 99 | bool sel = (i == model->selected); // currently selected row 100 | 101 | // Horizontal coordinates of columns: 102 | uint8_t x_label = 5 + 2; // X of label (left of selection box starting at x=5) 103 | uint8_t y = y_base + row_h * i; // Y of the current row 104 | uint8_t x_left = 70; // X for left arrow "<" 105 | uint8_t x_val = 78; // X for value (minutes or buzz mode) 106 | uint8_t x_right = 118; // X for right arrow ">" 107 | 108 | if(sel) { 109 | canvas_set_color(canvas, ColorBlack); 110 | canvas_draw_box(canvas, 5, y - 9, 118, 12); // selection box: (x=5, y=y-9, w=118, h=12) 111 | canvas_set_color(canvas, ColorWhite); // inverted text inside selection 112 | } else { 113 | canvas_set_color(canvas, ColorBlack); // normal text color 114 | } 115 | 116 | canvas_set_font(canvas, FontPrimary); // font: primary (FontPrimary) 117 | canvas_draw_str(canvas, x_label, y, labels[i]); // draw label at (x_label, y) 118 | canvas_draw_str(canvas, x_left, y, "<"); // left arrow at (x_left, y) 119 | 120 | if(i < 3) { 121 | char val_buf[8]; 122 | snprintf(val_buf, sizeof(val_buf), "%2u min", model->durations[i]); // format value as minutes 123 | canvas_draw_str(canvas, x_val, y, val_buf); // draw value at (x_val, y) 124 | } else { 125 | canvas_draw_str(canvas, x_val, y, buzz_mode_to_str(model->buzz_mode)); // draw buzz mode at (x_val, y) 126 | } 127 | 128 | canvas_draw_str(canvas, x_right, y, ">"); // right arrow at (x_right, y) 129 | } 130 | 131 | canvas_set_font(canvas, FontSecondary); 132 | canvas_set_color(canvas, ColorBlack); 133 | elements_button_center(canvas, "Save"); // soft-button centered on the bottom bar 134 | elements_button_back(canvas, "Back"); // soft-button on the right of the bottom bar 135 | } 136 | 137 | static bool config_input_callback(InputEvent* event, void* ctx) { 138 | FlippPomodoroConfigView* self = ctx; 139 | bool handled = false; 140 | 141 | if((event->type == InputTypePress) || (event->type == InputTypeRepeat)) { 142 | with_view_model(self->view, FlippPomodoroConfigViewModel * model, { 143 | switch(event->key) { 144 | case InputKeyUp: 145 | if(model->selected > 0) model->selected--; // move up (0..3) 146 | handled = true; 147 | break; 148 | case InputKeyDown: 149 | if(model->selected < 3) model->selected++; // move down (0..3) 150 | handled = true; 151 | break; 152 | case InputKeyRight: 153 | if(model->selected < 3) { 154 | if(model->durations[model->selected] < 99) model->durations[model->selected]++; // increment minutes 155 | } else { 156 | model->buzz_mode = buzz_mode_next(model->buzz_mode); // next buzz mode 157 | } 158 | handled = true; 159 | break; 160 | case InputKeyLeft: 161 | if(model->selected < 3) { 162 | if(model->durations[model->selected] > 1) model->durations[model->selected]--; // decrement minutes 163 | } else { 164 | model->buzz_mode = buzz_mode_prev(model->buzz_mode); // previous mode (circular) 165 | } 166 | handled = true; 167 | break; 168 | case InputKeyOk: 169 | if(self->on_save_cb) self->on_save_cb(self->cb_ctx); // confirmation (Save) 170 | handled = true; 171 | break; 172 | default: 173 | break; 174 | } 175 | }, true); 176 | } 177 | 178 | return handled; 179 | } 180 | 181 | FlippPomodoroConfigView* flipp_pomodoro_view_config_alloc() { 182 | FlippPomodoroConfigView* config = malloc(sizeof(FlippPomodoroConfigView)); 183 | config->view = view_alloc(); 184 | config->on_save_cb = NULL; 185 | config->cb_ctx = NULL; 186 | 187 | view_allocate_model(config->view, ViewModelTypeLockFree, sizeof(FlippPomodoroConfigViewModel)); 188 | view_set_context(config->view, config); 189 | view_set_draw_callback(config->view, config_draw_callback); 190 | view_set_input_callback(config->view, config_input_callback); 191 | 192 | with_view_model(config->view, FlippPomodoroConfigViewModel * model, { 193 | model->selected = 0; // first row (Focus) is selected by default 194 | model->durations[0] = 0; // default minutes 195 | model->durations[1] = 0; 196 | model->durations[2] = 0; 197 | model->buzz_mode = FlippPomodoroBuzzOnce; // default buzz mode 198 | }, false); 199 | 200 | return config; 201 | } 202 | 203 | View* flipp_pomodoro_view_config_get_view(FlippPomodoroConfigView* config) { 204 | return config->view; 205 | } 206 | 207 | void flipp_pomodoro_view_config_free(FlippPomodoroConfigView* config) { 208 | view_free(config->view); 209 | free(config); 210 | } 211 | 212 | void flipp_pomodoro_view_config_set_on_save_cb( 213 | FlippPomodoroConfigView* view, 214 | FlippPomodoroConfigViewActionCb cb, 215 | void* ctx 216 | ) { 217 | view->on_save_cb = cb; 218 | view->cb_ctx = ctx; 219 | } 220 | 221 | void flipp_pomodoro_view_config_set_settings( 222 | FlippPomodoroConfigView* view, 223 | const FlippPomodoroSettings* in 224 | ) { 225 | furi_assert(in); 226 | with_view_model(view->view, FlippPomodoroConfigViewModel * model, { 227 | model->durations[0] = in->focus_minutes; // load minutes into model 228 | model->durations[1] = in->short_break_minutes; 229 | model->durations[2] = in->long_break_minutes; 230 | model->buzz_mode = buzz_mode_sanitise(in->buzz_mode); // load buzz mode 231 | }, true); 232 | } 233 | 234 | void flipp_pomodoro_view_config_get_settings( 235 | FlippPomodoroConfigView* view, 236 | FlippPomodoroSettings* out 237 | ) { 238 | furi_assert(out); 239 | with_view_model(view->view, FlippPomodoroConfigViewModel * model, { 240 | out->focus_minutes = model->durations[0]; // read minutes from model 241 | out->short_break_minutes = model->durations[1]; 242 | out->long_break_minutes = model->durations[2]; 243 | out->buzz_mode = model->buzz_mode; // read buzz mode 244 | }, false); 245 | } 246 | -------------------------------------------------------------------------------- /flipp_pomodoro/views/flipp_pomodoro_timer_view.c: -------------------------------------------------------------------------------- 1 | #include "flipp_pomodoro_timer_view.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "../helpers/debug.h" 7 | #include "../flipp_pomodoro_app.h" 8 | #include "../modules/flipp_pomodoro.h" 9 | 10 | // Auto-compiled icons 11 | #include "flipp_pomodoro_icons.h" 12 | 13 | enum 14 | { 15 | ViewInputConsumed = true, 16 | ViewInputNotConusmed = false, 17 | }; 18 | 19 | struct FlippPomodoroTimerView 20 | { 21 | View *view; 22 | FlippPomodoroTimerViewInputCb right_cb; 23 | FlippPomodoroTimerViewInputCb left_cb; 24 | FlippPomodoroTimerViewInputCb ok_cb; 25 | void *callback_context; 26 | }; 27 | 28 | typedef struct 29 | { 30 | IconAnimation *icon; 31 | FlippPomodoroState *state; 32 | size_t scroll_counter; 33 | char *current_hint; 34 | uint32_t hint_open_timestamp; 35 | } FlippPomodoroTimerViewModel; 36 | 37 | static const Icon *stage_background_image[] = { 38 | [FlippPomodoroStageFocus] = &A_flipp_pomodoro_focus_64, 39 | [FlippPomodoroStageRest] = &A_flipp_pomodoro_rest_64, 40 | [FlippPomodoroStageLongBreak] = &A_flipp_pomodoro_rest_64, 41 | }; 42 | 43 | static void flipp_pomodoro_view_timer_draw_countdown(Canvas *canvas, TimeDifference remaining_time) 44 | { 45 | canvas_set_font(canvas, FontBigNumbers); 46 | const uint8_t right_border_margin = 1; 47 | 48 | const uint8_t countdown_box_height = canvas_height(canvas) * 0.4; 49 | const uint8_t countdown_box_width = canvas_width(canvas) * 0.5; 50 | const uint8_t countdown_box_x = canvas_width(canvas) - countdown_box_width - right_border_margin; 51 | const uint8_t countdown_box_y = 15; 52 | 53 | elements_bold_rounded_frame(canvas, 54 | countdown_box_x, 55 | countdown_box_y, 56 | countdown_box_width, 57 | countdown_box_height); 58 | 59 | FuriString *timer_string = furi_string_alloc(); 60 | furi_string_printf(timer_string, "%02u:%02u", remaining_time.minutes, remaining_time.seconds); 61 | const char *remaining_stage_time_string = furi_string_get_cstr(timer_string); 62 | canvas_draw_str_aligned( 63 | canvas, 64 | countdown_box_x + (countdown_box_width / 2), 65 | countdown_box_y + (countdown_box_height / 2), 66 | AlignCenter, 67 | AlignCenter, 68 | remaining_stage_time_string); 69 | 70 | furi_string_free(timer_string); 71 | }; 72 | 73 | static void draw_str_with_drop_shadow( 74 | Canvas *canvas, uint8_t x, 75 | uint8_t y, 76 | Align horizontal, 77 | Align vertical, 78 | const char *str) 79 | { 80 | canvas_set_color(canvas, ColorWhite); 81 | for (int x_off = -2; x_off <= 2; x_off++) 82 | { 83 | for (int y_off = -2; y_off <= 2; y_off++) 84 | { 85 | canvas_draw_str_aligned( 86 | canvas, 87 | x + x_off, 88 | y + y_off, 89 | horizontal, 90 | vertical, 91 | str); 92 | } 93 | } 94 | canvas_set_color(canvas, ColorBlack); 95 | canvas_draw_str_aligned( 96 | canvas, 97 | x, 98 | y, 99 | horizontal, 100 | vertical, 101 | str); 102 | } 103 | 104 | static void flipp_pomodoro_view_timer_draw_current_stage_label(Canvas *canvas, FlippPomodoroTimerViewModel *model) 105 | { 106 | canvas_set_font(canvas, FontPrimary); 107 | const char* label = flipp_pomodoro__current_stage_label(model->state); 108 | draw_str_with_drop_shadow( 109 | canvas, 110 | canvas_width(canvas), 111 | 0, 112 | AlignRight, 113 | AlignTop, 114 | label); 115 | } 116 | 117 | static void flipp_pomodoro_view_timer_draw_hint(Canvas *canvas, FlippPomodoroTimerViewModel *model) 118 | { 119 | uint8_t FRAMES_PER_SECOND = 3; 120 | uint8_t HINT_MAX_DURATION_SECONDS = 30; 121 | uint8_t SCROLL_DELAY_SECONDS = 2; 122 | 123 | uint8_t hint_duration = time_now() - model->hint_open_timestamp; 124 | 125 | if (hint_duration > HINT_MAX_DURATION_SECONDS || model->current_hint == NULL) 126 | { 127 | return; 128 | } 129 | 130 | uint8_t hint_width = 90; 131 | uint8_t hint_height = 18; 132 | 133 | uint8_t hint_x = canvas_width(canvas) - hint_width - 6; 134 | uint8_t hint_y = 35; 135 | 136 | FuriString *displayed_hint_string = furi_string_alloc(); 137 | 138 | furi_string_printf( 139 | displayed_hint_string, 140 | "%s", 141 | model->current_hint); 142 | 143 | 144 | size_t scroll_offset_base_max = (hint_duration < SCROLL_DELAY_SECONDS) ? 0 : ((hint_duration - SCROLL_DELAY_SECONDS) * FRAMES_PER_SECOND); 145 | 146 | size_t scroll_offset = (scroll_offset_base_max <= model->scroll_counter ? scroll_offset_base_max : model->scroll_counter + 1); 147 | 148 | canvas_set_color(canvas, ColorWhite); 149 | canvas_draw_box(canvas, hint_x, hint_y, hint_width + 3, hint_height); 150 | canvas_set_color(canvas, ColorBlack); 151 | 152 | elements_bubble(canvas, hint_x, hint_y, hint_width, hint_height); 153 | 154 | elements_scrollable_text_line( 155 | canvas, 156 | hint_x + 6, 157 | hint_y + 12, 158 | hint_width - 4, 159 | displayed_hint_string, 160 | scroll_offset, 161 | true); 162 | furi_string_free(displayed_hint_string); 163 | model->scroll_counter = scroll_offset; 164 | } 165 | 166 | static void flipp_pomodoro_view_timer_draw_callback(Canvas *canvas, void *_model) 167 | { 168 | if (!_model) 169 | { 170 | return; 171 | }; 172 | 173 | FlippPomodoroTimerViewModel *model = _model; 174 | 175 | canvas_clear(canvas); 176 | if (model->icon) 177 | { 178 | canvas_draw_icon_animation(canvas, 0, 0, model->icon); 179 | } 180 | 181 | flipp_pomodoro_view_timer_draw_countdown( 182 | canvas, 183 | flipp_pomodoro__stage_remaining_duration(model->state)); 184 | 185 | flipp_pomodoro_view_timer_draw_current_stage_label(canvas, model); 186 | 187 | canvas_set_color(canvas, ColorBlack); 188 | 189 | canvas_set_font(canvas, FontSecondary); 190 | elements_button_right(canvas, flipp_pomodoro__next_stage_label(model->state)); 191 | flipp_pomodoro_view_timer_draw_hint(canvas, model); 192 | elements_button_left(canvas, flipp_pomodoro__settings_button_label()); 193 | }; 194 | 195 | bool flipp_pomodoro_view_timer_input_callback(InputEvent *event, void *ctx) 196 | { 197 | furi_assert(ctx); 198 | furi_assert(event); 199 | FlippPomodoroTimerView *timer = ctx; 200 | 201 | const bool is_press_event = event->type == InputTypePress; 202 | 203 | if (!is_press_event) 204 | { 205 | return ViewInputNotConusmed; 206 | } 207 | 208 | switch (event->key) 209 | { 210 | case InputKeyRight: 211 | timer->right_cb(timer->callback_context); 212 | return ViewInputConsumed; 213 | case InputKeyLeft: 214 | timer->left_cb(timer->callback_context); 215 | return ViewInputConsumed; 216 | case InputKeyOk: 217 | timer->ok_cb(timer->callback_context); 218 | return ViewInputConsumed; 219 | default: 220 | return ViewInputNotConusmed; 221 | } 222 | }; 223 | 224 | View *flipp_pomodoro_view_timer_get_view(FlippPomodoroTimerView *timer) 225 | { 226 | furi_assert(timer); 227 | return timer->view; 228 | }; 229 | 230 | void flipp_pomodoro_view_timer_display_hint(View *view, char *hint) 231 | { 232 | with_view_model( 233 | view, 234 | FlippPomodoroTimerViewModel * model, 235 | { 236 | model->scroll_counter = 0; 237 | model->current_hint = hint; 238 | model->hint_open_timestamp = time_now(); 239 | }, 240 | true); 241 | } 242 | 243 | void flipp_pomodoro_view_timer_assign_animation(View *view) 244 | { 245 | with_view_model( 246 | view, 247 | FlippPomodoroTimerViewModel * model, 248 | { 249 | if (model->icon) 250 | { 251 | icon_animation_free(model->icon); 252 | } 253 | model->icon = icon_animation_alloc( 254 | stage_background_image[flipp_pomodoro__get_stage(model->state)]); 255 | view_tie_icon_animation(view, model->icon); 256 | icon_animation_start(model->icon); 257 | }, 258 | true); 259 | } 260 | 261 | FlippPomodoroTimerView *flipp_pomodoro_view_timer_alloc() 262 | { 263 | FlippPomodoroTimerView *timer = malloc(sizeof(FlippPomodoroTimerView)); 264 | timer->view = view_alloc(); 265 | 266 | view_allocate_model(flipp_pomodoro_view_timer_get_view(timer), ViewModelTypeLockFree, sizeof(FlippPomodoroTimerViewModel)); 267 | 268 | view_set_context(flipp_pomodoro_view_timer_get_view(timer), timer); 269 | view_set_draw_callback(timer->view, flipp_pomodoro_view_timer_draw_callback); 270 | view_set_input_callback(timer->view, flipp_pomodoro_view_timer_input_callback); 271 | 272 | with_view_model( 273 | flipp_pomodoro_view_timer_get_view(timer), 274 | FlippPomodoroTimerViewModel * model, 275 | { 276 | model->scroll_counter = 0; 277 | }, 278 | false); 279 | 280 | return timer; 281 | }; 282 | 283 | void flipp_pomodoro_view_timer_set_callback_context(FlippPomodoroTimerView *timer, void *callback_ctx) 284 | { 285 | furi_assert(timer); 286 | furi_assert(callback_ctx); 287 | timer->callback_context = callback_ctx; 288 | } 289 | 290 | void flipp_pomodoro_view_timer_set_on_right_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb right_cb) 291 | { 292 | furi_assert(timer); 293 | furi_assert(right_cb); 294 | timer->right_cb = right_cb; 295 | }; 296 | 297 | void flipp_pomodoro_view_timer_set_on_ok_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb ok_kb) 298 | { 299 | furi_assert(ok_kb); 300 | furi_assert(timer); 301 | timer->ok_cb = ok_kb; 302 | } 303 | 304 | void flipp_pomodoro_view_timer_set_state(View *view, FlippPomodoroState *state) 305 | { 306 | furi_assert(view); 307 | furi_assert(state); 308 | with_view_model( 309 | view, 310 | FlippPomodoroTimerViewModel * model, 311 | { 312 | model->state = state; 313 | model->current_hint = NULL; 314 | }, 315 | false); 316 | flipp_pomodoro_view_timer_assign_animation(view); 317 | }; 318 | 319 | void flipp_pomodoro_view_timer_free(FlippPomodoroTimerView *timer) 320 | { 321 | furi_assert(timer); 322 | with_view_model( 323 | timer->view, 324 | FlippPomodoroTimerViewModel * model, 325 | { 326 | icon_animation_free(model->icon); 327 | }, 328 | false); 329 | view_free(timer->view); 330 | 331 | free(timer); 332 | }; 333 | 334 | void flipp_pomodoro_view_timer_set_on_left_cb(FlippPomodoroTimerView *timer, FlippPomodoroTimerViewInputCb left_cb) { 335 | furi_assert(timer); 336 | furi_assert(left_cb); 337 | timer->left_cb = left_cb; 338 | } 339 | -------------------------------------------------------------------------------- /assets/graphics-template.pixil: -------------------------------------------------------------------------------- 1 | {"application":"pixil","version":"2.6.1","website":"pixilart.com","author":"https://www.pixilart.com","contact":"support@pixilart.com","width":"64","height":"64","colors":{"default":["000000","ffffff","f44336","E91E63","9C27B0","673AB7","3F51B5","2196F3","03A9F4","00BCD4","009688","4CAF50","8BC34A","CDDC39","FFEB3B","FFC107","FF9800","FF5722","795548","9E9E9E","607D8B","ffebee","ffcdd2","ef9a9a","e57373","ef5350","e53935","d32f2f","c62828","b71c1c","ff8a80","ff5252","ff1744","d50000","fce4ec","f8bbd0","f48fb1","f06292","ec407a","e91e63","d81b60","c2185b","ad1457","880e4f","ff80ab","ff4081","f50057","c51162","f3e5f5","e1bee7","ce93d8","ba68c8","ab47bc","9c27b0","8e24aa","7b1fa2","6a1b9a","4a148c","ea80fc","e040fb","d500f9","aa00ff","ede7f6","d1c4e9","b39ddb","9575cd","7e57c2","673ab7","5e35b1","512da8","4527a0","311b92","b388ff","7c4dff","651fff","6200ea","e8eaf6","c5cae9","9fa8da","7986cb","5c6bc0","3f51b5","3949ab","303f9f","283593","1a237e","8c9eff","536dfe","3d5afe","304ffe","e3f2fd","bbdefb","90caf9","64b5f6","42a5f5","2196f3","1e88e5","1976d2","1565c0","0d47a1","82b1ff","448aff","2979ff","2962ff","e1f5fe","b3e5fc","81d4fa","4fc3f7","29b6f6","03a9f4","039be5","0288d1","0277bd","01579b","80d8ff","40c4ff","00b0ff","0091ea","e0f7fa","b2ebf2","80deea","4dd0e1","26c6da","00bcd4","00acc1","0097a7","00838f","006064","84ffff","18ffff","00e5ff","00b8d4","e0f2f1","b2dfdb","80cbc4","4db6ac","26a69a","009688","00897b","00796b","00695c","004d40","a7ffeb","64ffda","1de9b6","00bfa5","e8f5e9","c8e6c9","a5d6a7","81c784","66bb6a","4caf50","43a047","388e3c","2e7d32","1b5e20","b9f6ca","69f0ae","00e676","00c853","f1f8e9","dcedc8","c5e1a5","aed581","9ccc65","8bc34a","7cb342","689f38","558b2f","33691e","ccff90","b2ff59","76ff03","64dd17","f9fbe7","f0f4c3","e6ee9c","dce775","d4e157","cddc39","c0ca33","afb42b","9e9d24","827717","f4ff81","eeff41","c6ff00","aeea00","fffde7","fff9c4","fff59d","fff176","ffee58","ffeb3b","fdd835","fbc02d","f9a825","f57f17","ffff8d","ffff00","ffea00","ffd600","fff8e1","ffecb3","ffe082","ffd54f","ffca28","ffc107","ffb300","ffa000","ff8f00","ff6f00","ffe57f","ffd740","ffc400","ffab00","fff3e0","ffe0b2","ffcc80","ffb74d","ffa726","ff9800","fb8c00","f57c00","ef6c00","e65100","ffd180","ffab40","ff9100","ff6d00","fbe9e7","ffccbc","ffab91","ff8a65","ff7043","ff5722","f4511e","e64a19","d84315","bf360c","ff9e80","ff6e40","ff3d00","dd2c00","efebe9","d7ccc8","bcaaa4","a1887f","8d6e63","795548","6d4c41","5d4037","4e342e","3e2723","fafafa","f5f5f5","eeeeee","e0e0e0","bdbdbd","9e9e9e","757575","616161","424242","212121","eceff1","cfd8dc","b0bec5","90a4ae","78909c","607d8b","546e7a","455a64","37474f","263238"],"simple":["ffffff","d4d4d4","a1a1a1","787878","545454","303030","000000","edc5c5","e68383","ff0000","de2424","ad3636","823737","592b2b","f5d2ee","eb8dd7","f700b9","bf1f97","9c277f","732761","4f2445","e2bcf7","bf79e8","9d00ff","8330ba","6d3096","502c69","351b47","c5c3f0","736feb","0905f7","2e2eb0","2d2d80","252554","090936","c7e2ed","6ac3e6","00bbff","279ac4","347c96","2d5b6b","103947","bbf0d9","6febb3","00ff88","2eb878","349166","2b694c","0c3d25","c2edc0","76ed70","0dff00","36c72c","408c3b","315c2e","144511","d6edbb","b5eb73","8cff00","89c93a","6f8f44","4b632a","2a400c","f1f2bf","eef069","ffff00","baba30","91913f","5e5e2b","3b3b09","ffdeb8","f2ae61","ff8400","c48037","85623d","573e25","3d2309","fcbbae","ff8066","ff2b00","cc553d","9c5b4e","61372e","36130b"],"common":["000000","FFFFFF","7F7F7F","a1a1a1","C3C3C3","c40424","880015","B97A57","dba88c","ED1C24","f75b63","f26f9b","FF7F27","f7ab79","FFC90E","FFF200","cfc532","EFE4B0","1ee656","0c6624","22B14C","B5E61D","5487ff","00A2E8","99D9EA","3F48CC","7f86e3","7092BE","720899","cd55cf","A349A4","C8BFE7","ffffff","030003","f7f7f7","fcfcfc"],"skin tones":["ffe0bd","ffdbac","ffcd94","eac086","e0ac69","f1c27d","ffad60","c68642","8d5524","896347","765339","613D24","4C2D17","391E0B","351606","2D1304","180A01","090300"]},"frames":[{"name":"","speed":100,"layers":[{"id":0,"src":"data:image/pngp98kjasdnasd983/24kasdjasdbase64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABKVJREFUeF7tW9GS4zAIa///o/cmuXOGUEkI2+324fZlZ1sHgxACJ9nn4/H4eWz6+fn5a+r5fE5bnLVxXHfsq34jpw5PlwFgTo/PHTQyaDNAzICwDMDY9AgyB9xhArs22neAHGtcJpwAOMgxekWnUBYrWjp2Z8vKAeF5RL/i5M6goy3ErA6jXCa0GTAMI2dXmOQmYYYN0q8OA2LwzCjSgk7tjgAt+ja6DbJ37uVqAAreCVbRNjqlQBo20PpOWUBQGQNyDTlZVK1LMSbaRjZih0BgOCCw/W8MYMLhBI/WuEGza1G9KzAqP6E/OwYhtfFsH69UPJZfFNAOCJcGVBft+F5NhRWFO4Jb2RrAXaDNMmAmswiE4Qhqr1kbVOvNjGBJy2BOjcK0pTTaUqR47jAMjAx69mMKhC4D2KZZnWfKJncAxLJq/w4IpQYwxHP23OCZDrBxmgXjdBe7JTMGKPSjkKB5QW2emZFBYWBGf6J9xAiWIKQpVANU9lUtjt7dFckYCLPBQEAJeVH7oE830FwGMKSdmszIx7/VCRD1d0Z/Brjym2qAo7YMYZRJJYixBHLAHdAVCzKjSgY4AKjMO/TvKnymf5wflAij6yJYUAPYZpmuA1lVb1X2qxsqqhPkUqrKINs61yMNqABAKt9RfgSYEjgEQkd7WNeAGoDoHzPNnMlrVOZz62TBZIYxPxATUcuDApoZ4ALA1NsJ3HEOiZryjQkmAnt8dsTwUgKz9HdOYRU4rMXFICrdUToAu4FiAKv1XdlHgLAOlDOZQcnBZdsI3FIDPg2AI2xViboMuMrgWxnAapoBMMMASwOQ8r6zBBh1qxJAg1EW2wjS1zKAtcgKgBkGfJ0GzIpizizqSFnPrr+/TQM+yQBLA9AJrdKFqt+737uKHwUz+4ZmiJvA/vYkqMBwZgI3GRkkSwSPRc7Bp3sQchhAazbd2bkC+fd6DGIAmzDPz3/rNFiB8BH6o7PAyHr1EBJlqPOIqgOAmkjR2UWNwZEh57Xj6XAeGpynMO4trCpYRweqQ1pOHOomL8EfDJgFIG7InIv1yYJU+lGNw5VIMjbfwEEAIDQ7zqh5PQOxelc4Z1rRHw1I8P0ABUAFzqAZO5UpJuR5XmW4k33ly1kCqnUwbaici9epmyWjBHK5VPbV47TIChTbzbfxjpADgtuaMmiVCM4+G1T0d2eTaw5QrUZ1CCQ0FequICLqOp+5wZ9+xkGI9XYmLCzTsZYrMGIJVNexWs5dqNrzpQRQINUjbyUsKijUBfJ6xJB3BP/CgCweWZgcJqA1XQ3o7MNEtNrziq16OlzVU7fduY4pgUMqP+uHfEcI1RbrFp26c0BgwKNymQ2elgBSfQeMii2zgaugV4K3ABhtjulBrkEkclXgXRuOaFZ7lhrAlNihehWQcq56xLaDZbINVsjtdqDaL5ejk4COzakXJWNZVBnrOMPWvhP0aQCyNuzOzGp/d4FfAmCAEKfGW301Xp1lAa+qfAXEMgCqHDpiiE6Eu1mFwNgCwDC8o1Z32KiyvtQFHOPdPt1d7/jgrtnKADU7rPR+N5iZdW8HYMapT17zH4DuP0x8Mjuf2OsPcqIgHLHYYvgAAAAASUVORK5CYII=","edit":false,"name":"Background_work","opacity":"1","active":false,"unqid":"yyl3ww","options":{"blend":"source-over","locked":false,"filter":{"brightness":"100%","contrast":"100%","grayscale":"0%","blur":0,"dropshadow_x":0,"dropshadow_y":0,"dropshadow_blur":0,"dropshadow_alpha":1}}},{"id":1,"src":"data:image/pngp98kjasdnasd983/24kasdjasdbase64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAcdJREFUeF7tW8sSwiAQk///6Do61qlVyD5w2Gi8eABqk03CorVt27ZdiF+ttcsNwv7uhdKYCTiDPpNhIaUcAT0QEXA3NSASShCQlTGS/ej6ywlAFULgvONvtlmZAQg8GveC/zR/mQIQuG/ZopQCzhXpgUZkWZXw6TrLFLDftDf1Z5Hx/PyVGYAqh/b53nqPkpYrwKoEZBcP6OO1yhAwUgMKxMw4BQEvFXv0/lb5Q3IqZwACiexzHi/VB6AAROOwskApFLuAlwRESmkFWPsBRIp3vEQIepsb7/zhDrM6BKNgouve+onVBBy/tIiCsq4reRbwdnjRrOiRVCIDvMGF+gO0GxzJKEcAunlEltUOFH1AT+5ZEkorIAuu1x732uJyFkAERMZHtvoLAo5bbck+IFLVyBG59FngWySgXeGnLBDZNX6KgIiKRECFw1CkcrPWSAFSAPEjMmiLs9iE1gIzwN87RFmA1AJSgMXghjm0FpACDI/AGQTAH4JZJdBawFJdyxx6Av5aAVnw1I3QDPDUBFj8bZlDnwEWkKM5IkCHIdLDUFb6FD+OzgKpDBgwoBBUCCoEuf84mQ1KZYAyQBmgDKD+97hCMMnAFXmfi18H390RAAAAAElFTkSuQmCC","edit":false,"name":"rest_background","opacity":"1","active":true,"unqid":"r5qv0h","options":{"blend":"source-over","locked":false,"filter":{"brightness":"100%","contrast":"100%","grayscale":"0%","blur":0,"dropshadow_x":0,"dropshadow_y":0,"dropshadow_blur":0,"dropshadow_alpha":1}}},{"id":2,"src":"data:image/pngp98kjasdnasd983/24kasdjasdbase64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAArlJREFUeF7tmu2ShSAIhuv+L9qd3HQ4CgKmSJP7byfryMPLh9R5fPzv/Lj9xwawFfBxAjsEPi6AnQR3COwQ+DiBHQIKAQRi7ashUpuvjA0Bt/88q0e8Cki52WjlZexlGGU0pZrzn0Yi9QoQeZMhhJCM7jG+hHIrwz2EuMHRxicYb4CQJdsrey6JeocQAcwy/g1KOKH8OW/2XvesgqwAqXE9iRKUSndJMSpAajyW6SW3uwaQcsAoCJRCvIbBIwXAJAcTKdVHeITwGEBpLGN8ZtaruNH3kQCQHp9sjbnEWF73pAQUQMuLkqQHveQ9HCoA3DmAu96qFElVKV8cx/qZ5A8ATMqYwRoIcK3Ho3MGoDHq8rJmPZEDKrGMTnCS55kASMDShso8AuYI5p1iBUBSxrSHJxj7gmGKKQSRAqCEoQFcRYAxz60FKvEBgElekQNmlNbo1ZMktAoQm8pGj+4TkLAwU4G4D3jqWQoqkhBjNEgy+Ig10ztBqeHLc0BL9lT50nhAWAlMvR9/7MlARApA0jStOiBNBSDx+irpJ+cNAYDNBKgySR2W7m5QKqph64YA4FrdRveXXsOZZf3KARY5gKsEq7xvlgQhACRclnnfHACWFFd63xSAR+PNAFBD09XenwoAOzt4i/+pAKh6nxsQZEA4rLgrHiQaiCieRy716P1KAdLWVQuEmCwvLX/NVlhyeNFAKMF6SH7sWWCUGhqjNb8KaHVuGs+nMwI3O7ScAHWdBZ6oQRJOoCCYq0J1GsQqFzfulgAApfFHfFq19axXAcB+gCrnCYwGQBl69/9TVfEYgOCsr/7ktlDEXADg297ujbYgXNe4MGHunwsADkR6YlyiAC42W4BmD0vZEOBadqrEtV6gIo0Q96neNBWwADjvtVTTeIU2zSBuv119gPahMIkRCtkAeqGOvu8PNvQkX4Cf258AAAAASUVORK5CYII=","edit":false,"name":"scene","opacity":"1","active":false,"unqid":"ggx4ui","options":{"blend":"source-over","locked":false,"filter":{"brightness":"100%","contrast":"100%","grayscale":"0%","blur":0,"dropshadow_x":0,"dropshadow_y":0,"dropshadow_blur":0,"dropshadow_alpha":1}}},{"id":3,"src":"data:image/pngp98kjasdnasd983/24kasdjasdbase64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAqBJREFUeF7tm9uWwyAIRev/f3S6YjVDFAQxKk6cx2lubM4BNK37vPzPvTz+zwawFfByAtsCLxfALoKcBQ5CIdx5ywgLBpIFexx4/M5l8S8L5HzwGKU7qIgL+XQ/Gtc1lkl9eFBV0FSQQRlLqeFRACeY1SA8DmA1CF0ArARhA9BUfkmlB63SdFHsrQDvBgmwWcd0A7BKHegKYAUIXQEkI7NJK9wAIDP+RzEdezvDa53XsDogXQDOB8SCpf5fKloxeBC4v7ZFCB5AmvkURC0ESkkmAYCV3C2pLRCWUkAKIJUtpFKjBKIGpM6ZXhjhWj6zdYsKsEKYRf/bWJkK4QIAMx8ftAUAtIGgYE6DgBbB84G1HSGVPjfizi6MtzZIZR7KmQJTOtcyBG8BybDTY05IwEyxwWWBCAHr4S3ZTbsIZq+ZNsgsUFJDTRuUBG5hsXQrgi1WwHwu6QQzs+8TICoAXBVDPpeoZXbwXQBIsm5B+jFnKgVg2ZUGHoMPg9eUyn+rT1oLcCtIyjURXnilpjDXs6eoFACzKH0cuEscluDTs6+uAZICl7ZBK5LPFmRaC2gyH4agcwPGRPbVCqgN3mr2uwIAxc7fRwpt9HHqIlia/OC8b2HYKUF9FEBWYMJOsyXPDy2C1rMfvem/3yNZCNX409rAQw5msQ3W9nYORjoaW7UB+mqsVQ0QZtLyzXUDtAi2qqG0fQaUYwIG2QVqVndU9S+2n79hcCoItg1iUytnkRoFzbYIC4AbeODncGOVg8Rcd5gqVAAEa311Wx09OzwKIO4TtMwVwwFgr8c18oWbJJLVLrf9PmoBRbbBUgUvvTesmACp3yLEWw+pAyoLlDpDYQYYEhA3oXZfDG0A9JetbCqgVjL/7XiTWRkJeQMYSdvivb4wHLxQEL841wAAAABJRU5ErkJggg==","edit":false,"name":"dolphin backup","opacity":"1","active":true,"unqid":"ggx4ui","options":{"blend":"source-over","locked":false,"filter":{"brightness":"100%","contrast":"100%","grayscale":"0%","blur":0,"dropshadow_x":0,"dropshadow_y":0,"dropshadow_blur":0,"dropshadow_alpha":1}}},{"id":4,"src":"data:image/pngp98kjasdnasd983/24kasdjasdbase64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAYRJREFUeF7tmOEOwyAIhOf7P7SLS0ycQyki1I7b387G+7gDa3oF/6Xg+l8AAAcEJ4AIBDcAmiAigAgEJ4AIBDcApgAigAgEJ4AIBDcApgAigAgEJ4AIBDcApgAigAgEJ4AIBDcApgAigAgEJ4AIBDfA7VMgKwugdrD6BQoBOWed/pTE2/9ZIH6DQnC/VA1AuhcK2F0A3MWPYAGA1EYb/n9M9YsWbwccJR4AnB1gWn2qw18Zs14RMBdfxK5AeDyAIrqtdAvhFAeYVb8X/2lqg9PhCIaHA0wAUOJnAMozCoI1ABPxVWgV1MKQ9gFLAGbi28NYFdzCaJ9zfeDxAIZnfKI5ekbAvPqjHtD2gd4VXgDMxdeqUyPvhDHoBmBW7VHX7yOzuwe4iqecQNy6TD9g/wLAaP5zE2D31+At1dfeT6w6gLzNvEJcu+Hd6ykA7FUtOU7kN7Tk0XS3QO59BcCX4NUqzuby7LDCbXDluURD2hlc7kwu7dAr4rkPImoMspZf3cgT1r0BUbC/5qun8cAAAAAASUVORK5CYII=","edit":false,"name":"foreground_work","opacity":"1","active":false,"unqid":"e4kst","options":{"blend":"source-over","locked":false,"filter":{"brightness":"100%","contrast":"100%","grayscale":"0%","blur":0,"dropshadow_x":0,"dropshadow_y":0,"dropshadow_blur":0,"dropshadow_alpha":1}}},{"id":5,"src":"data:image/pngp98kjasdnasd983/24kasdjasdbase64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAcZJREFUeF7tmOGSwyAIhM37P3RuvCl31Coq0swq9E9n2ti4H7tIeiXnr8u5/hQAwgHOCUQEnBsgmmBEICLgnEBEwLkB4hSICEQEnBOICDg3QJwCEYGIgHMCEQHnBohTYOcI3CmtF3BXACbic/x3BWDWunYBUFbclQNaYk0goDlgVKyJeNQeQOL4O98rF78MAs0BZXPLAmviy8/VTREJQK3yeX+tz9Wi+UIkAHxfpejad8cCkLJf9q0jewDPd8/+RwMgi/OYmg9Eu/SAVk842gF0ArRmAHLIEgQ0B2gyfxSAXNVWzqWjUX0kojmgBCBNgiZ7N/kRNf7PhbURt+eIpdsjAiiPvdazAB+T1RAQAfSmPdNegAhg1AEmj/NoAKRTgJ/7JuLNfkQdwPZC6dHYdN+IDiAXkNDecLTEHxmA9GeIWSNEAkAzgKaiah3qhZpddtbc9z3P4Lo+JExpmrr4C6Lf/urSAuDrXkCGdQ1f+GXxf41vBkKl+imvn4EABYA2PwKhEC8NT2LtoAD82uBVQQlCFs8qXRudh2cFKAAj4qmc4YD/rL9ZfOseoG203DkzDXA4J9qNPbiODxBTsZ66+EFBj93KPYAfRRqhQe+3M0UAAAAASUVORK5CYII=","edit":false,"name":"Layer 5","opacity":"1","active":true,"unqid":"n79l4g","options":{"blend":"source-over","locked":false,"filter":{"brightness":"100%","contrast":"100%","grayscale":"0%","blur":0,"dropshadow_x":0,"dropshadow_y":0,"dropshadow_blur":0,"dropshadow_alpha":1}}},{"id":6,"src":"data:image/pngp98kjasdnasd983/24kasdjasdbase64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAQpJREFUeF7tlVsKwzAMBKX7H9rFgUIeciM5BZfu+CeQKLZ22bHcxJeL6zcMIAHiDoCAeAC4BEEABMQdAAHxADAFQAAExB0AAfEAMAVAAATEHQAB8QAwBUAABMQdAAHxADAFQAAExB0AAfEAMAVAAATEHQAB8QAwBUAABMQdAAHxADAFQAAExB0AAfEAfGUKtBsTfzplleaGQltr5u62f75N6e8TK1WU2KdcEh0cCu3iRmskPtvNSpO6AQdln4RGgp6KX22St6pisy3u0ZrYKqs/VTeTpIsBmU0ioZn/zipWGHbu8zEC+8uuKmjGtEwUKn1MIXB3GfbvGXGVRjPCi5NnK78koHLQP9S+AO/GRTrQGImwAAAAAElFTkSuQmCC","edit":false,"name":"foreground_rest","opacity":"1","active":true,"unqid":"e4kst","options":{"blend":"source-over","locked":false,"filter":{"brightness":"100%","contrast":"100%","grayscale":"0%","blur":0,"dropshadow_x":0,"dropshadow_y":0,"dropshadow_blur":0,"dropshadow_alpha":1}}}],"active":true,"selectedLayer":3,"unqid":"9k4so9","preview":"data:image/pngp98kjasdnasd983/24kasdjasdbase64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAA+xJREFUeF7VWtGOwzAI2/7/o3fqdKkYBWyTdO32cjqtJcEY7LR7vl6v1+OHP8/n87GlMP6qqTx/GQCftAeDAeV2AGRJdJLb2IBAuAUAszRGtK/iXw4AqhBKTv3+0DZXzgCUPPpeTT66/jIGoOTOaotbMcBXJEsagcUyIYpTMmC7IfqstA7q1F8FxshrByBKNkvUX7sSEAs40vms8gqTthK/neBMEgOQ2RjW0aEZgJizV/jfKaZgrVSB1fT0SWTxVbA+WLYSAMZ5sQML0Rt9zzrHU2TwLCZktEbJVvs5BYBvMgG1BWLczwJgQUYMqEA4FQBFXdBUP6utbgGAmpx6/SUMYOdAN5nufQf7vVoGM39fVmH2sRYwO5UpOq0FBgPG4qxLZGcBuo4txAcAynkAyos5SM08tETrRGcH1hm+izRaYFZP/UaGAthzAtO3aPMIEGaNgxVGpzs5aHCM7rCgq+9ov/b7/TTokfU9i4KuYsCqCmdM8nkcABiVikxMBwTfCjZBdjAiUND3VVulDJgFgFEBBVB2qmdgpDPOPhBZOQvs8GM8AKpiNO0rfa+uh0Mwqv6oKKKtBRFdy8ZkwOmqWPhMMNp4VVE1aZXOh8GVOL+OarxnwEylOn2sGK5uZRnW7EboQxeTR+FZW7AL2aGYSSybbAf0dDhaJ4iS7C6M2scCEtHY7ssXi2FvOYQ3ANiJrQ4tJm4Fqr/fgoOKxTLztNMgw5aomtkgq8wMm2x03XIAmKojJpXOzSgAAzICpwVAtDCbuJK87/3qf5QoHIJqAOQa0wXRq6p/FWJek13GAFTFsNdMYuVUdjIcKUQlqXIhKRfkoqrId69Hzk6N+5UhGNlcRbLsLMlk0DJwFoTWEGRppgzGKCmfXPS/Au5XGdDV7SzpLN6tGGCVYQwvdYPeHEU2WI0JrTBLafU6daNRy6iMkPfYUQF2kQ4AXvYqBqjxwxmw4jdCme6rJ7UOA9hiQCe4Ak27iKoAkbnKZLATGwKw1F25A8tYnGHEN6Tvo1DRDJhlQ3Z/pBJV+0ROcFb3D0YtG4IzNGMArOJ7NxgdjGZ7f9wPnaCtGktlBoB9A+4tcuUII0YwbbXcB0SgRHKlVCmLycSYAQEygNmArWbnLXCkAOy6HjgVjKUAzCiJ0jZebrNnBgyI4ctRBcUudaN3A+qEz+aTAmbIADWpakorsZT2ySRVVa8DAGjAMbrNUC9yjdV92csTzxoZgHEWYCXuYCQmf+LGgsUwqfpxB2WFZzejzA52LeU6FqQP9nkn2Alip7+y4SsA8/mFvxFSkvAeQLmXAVuJ12njpT7Ayg+T3FkMYNbeC+eHYAfxX77nD9QjOpXFm1yHAAAAAElFTkSuQmCC","width":"64","height":"64"}],"currentFrame":0,"name":"Untitled","preview":"data:image/pngp98kjasdnasd983/24kasdjasdbase64,iVBORw0KGgoAAAANSUhEUgAAAEAA/sfR5H8Fkddasdmnacvx/AABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAA+xJREFUeF7VWtGOwzAI2/7/o3fqdKkYBWyTdO32cjqtJcEY7LR7vl6v1+OHP8/n87GlMP6qqTx/GQCftAeDAeV2AGRJdJLb2IBAuAUAszRGtK/iXw4AqhBKTv3+0DZXzgCUPPpeTT66/jIGoOTOaotbMcBXJEsagcUyIYpTMmC7IfqstA7q1F8FxshrByBKNkvUX7sSEAs40vms8gqTthK/neBMEgOQ2RjW0aEZgJizV/jfKaZgrVSB1fT0SWTxVbA+WLYSAMZ5sQML0Rt9zzrHU2TwLCZktEbJVvs5BYBvMgG1BWLczwJgQUYMqEA4FQBFXdBUP6utbgGAmpx6/SUMYOdAN5nufQf7vVoGM39fVmH2sRYwO5UpOq0FBgPG4qxLZGcBuo4txAcAynkAyos5SM08tETrRGcH1hm+izRaYFZP/UaGAthzAtO3aPMIEGaNgxVGpzs5aHCM7rCgq+9ov/b7/TTokfU9i4KuYsCqCmdM8nkcABiVikxMBwTfCjZBdjAiUND3VVulDJgFgFEBBVB2qmdgpDPOPhBZOQvs8GM8AKpiNO0rfa+uh0Mwqv6oKKKtBRFdy8ZkwOmqWPhMMNp4VVE1aZXOh8GVOL+OarxnwEylOn2sGK5uZRnW7EboQxeTR+FZW7AL2aGYSSybbAf0dDhaJ4iS7C6M2scCEtHY7ssXi2FvOYQ3ANiJrQ4tJm4Fqr/fgoOKxTLztNMgw5aomtkgq8wMm2x03XIAmKojJpXOzSgAAzICpwVAtDCbuJK87/3qf5QoHIJqAOQa0wXRq6p/FWJek13GAFTFsNdMYuVUdjIcKUQlqXIhKRfkoqrId69Hzk6N+5UhGNlcRbLsLMlk0DJwFoTWEGRppgzGKCmfXPS/Au5XGdDV7SzpLN6tGGCVYQwvdYPeHEU2WI0JrTBLafU6daNRy6iMkPfYUQF2kQ4AXvYqBqjxwxmw4jdCme6rJ7UOA9hiQCe4Ak27iKoAkbnKZLATGwKw1F25A8tYnGHEN6Tvo1DRDJhlQ3Z/pBJV+0ROcFb3D0YtG4IzNGMArOJ7NxgdjGZ7f9wPnaCtGktlBoB9A+4tcuUII0YwbbXcB0SgRHKlVCmLycSYAQEygNmArWbnLXCkAOy6HjgVjKUAzCiJ0jZebrNnBgyI4ctRBcUudaN3A+qEz+aTAmbIADWpakorsZT2ySRVVa8DAGjAMbrNUC9yjdV92csTzxoZgHEWYCXuYCQmf+LGgsUwqfpxB2WFZzejzA52LeU6FqQP9nkn2Alip7+y4SsA8/mFvxFSkvAeQLmXAVuJ12njpT7Ayg+T3FkMYNbeC+eHYAfxX77nD9QjOpXFm1yHAAAAAElFTkSuQmCC","palette_id":false} --------------------------------------------------------------------------------