Przeglądaj źródła

add new apps and update readme

MX 2 lat temu
rodzic
commit
670d4c459b
27 zmienionych plików z 1410 dodań i 1 usunięć
  1. 3 1
      ReadMe.md
  2. 16 0
      non_catalog_apps/calendar/README.md
  3. 13 0
      non_catalog_apps/calendar/application.fam
  4. 119 0
      non_catalog_apps/calendar/calendar.c
  5. 27 0
      non_catalog_apps/calendar/calendar.h
  6. 6 0
      non_catalog_apps/calendar/helpers/calendar_event.h
  7. 8 0
      non_catalog_apps/calendar/helpers/variable_shared_context.h
  8. BIN
      non_catalog_apps/calendar/icon.png
  9. 30 0
      non_catalog_apps/calendar/scenes/calendar_scene.c
  10. 27 0
      non_catalog_apps/calendar/scenes/calendar_scene.h
  11. 3 0
      non_catalog_apps/calendar/scenes/calendar_scene_config.h
  12. 29 0
      non_catalog_apps/calendar/scenes/calendar_scene_month_browser.c
  13. 37 0
      non_catalog_apps/calendar/scenes/calendar_scene_month_picker.c
  14. 37 0
      non_catalog_apps/calendar/scenes/calendar_scene_year_picker.c
  15. 122 0
      non_catalog_apps/calendar/views/calendar_month_browser.c
  16. 13 0
      non_catalog_apps/calendar/views/calendar_month_browser.h
  17. 157 0
      non_catalog_apps/calendar/views/calendar_month_picker.c
  18. 17 0
      non_catalog_apps/calendar/views/calendar_month_picker.h
  19. 167 0
      non_catalog_apps/calendar/views/calendar_year_picker.c
  20. 17 0
      non_catalog_apps/calendar/views/calendar_year_picker.h
  21. 41 0
      non_catalog_apps/sudoku/.github/workflows/build.yml
  22. 4 0
      non_catalog_apps/sudoku/.gitignore
  23. 1 0
      non_catalog_apps/sudoku/README.md
  24. 19 0
      non_catalog_apps/sudoku/application.fam
  25. 0 0
      non_catalog_apps/sudoku/images/.gitkeep
  26. 497 0
      non_catalog_apps/sudoku/sudoku.c
  27. BIN
      non_catalog_apps/sudoku/sudoku.png

+ 3 - 1
ReadMe.md

@@ -13,7 +13,7 @@ Apps contains changes needed to compile them on latest firmware, fixes has been
 
 The Flipper and its community wouldn't be as rich as it is without your contributions and support. Thank you for all you have done.
 
-### Apps checked & updated at `13 Aug 21:57 GMT +3`
+### Apps checked & updated at `17 Aug 02:19 GMT +3`
 
 
 # Default pack
@@ -109,6 +109,7 @@ Games:
 - [Video Poker (by PixlEmly)](https://github.com/PixlEmly/flipperzero-firmware-testing/blob/420/applications/VideoPoker/poker.c)
 - [Yatzee (by emfleak)](https://github.com/emfleak/flipperzero-yatzee)
 - [Secret Toggle (by nostrumuva)](https://github.com/nostrumuva/secret_toggle)
+- [Sudoku Game (by profelis)](https://github.com/profelis/fz-sudoku)
 
 ## GPIO
 - [Air Mouse (by ginkage)](https://github.com/ginkage/FlippAirMouse/)
@@ -200,6 +201,7 @@ Games:
 - [QR Code (by bmatcuk)](https://github.com/bmatcuk/flipperzero-qrcode)
 - [Resistance calculator (by instantiator)](https://github.com/instantiator/flipper-zero-experimental-apps)
 - [VB Lab Migration Assistant (by GMMan (cyanic))](https://github.com/GMMan/flipperzero-vb-migrate)
+- [Simple calendar app (by Adiras)](https://github.com/Adiras/flipperzero-calendar)
 
 ## USB
 - [USB HID Autofire (by pbek)](https://github.com/pbek/usb_hid_autofire)

+ 16 - 0
non_catalog_apps/calendar/README.md

@@ -0,0 +1,16 @@
+# Flipper Zero calendar application
+
+- [Flipper Zero Official Website](https://flipperzero.one). A simple way to explain to your friends what Flipper Zero can do.
+- [Flipper Zero Firmware Update](https://update.flipperzero.one). Improvements for your dolphin: latest firmware releases, upgrade tools for PC and mobile devices.
+- [User Documentation](https://docs.flipperzero.one). Learn more about your dolphin: specs, usage guides, and anything you want to ask.
+
+## How to set up and build the application
+
+Make sure you have enough space on SD card and clone the source code inside `applications_user` folder:
+
+```shell
+git clone https://github.com/Adiras/flipperzero-calendar.git
+```
+
+- To launch app on Flipper, run `./fbt launch APPSRC=applications_user\flipperzero-calendar`
+- To build app without uploading it to Flipper, use `./fbt build APPSRC=applications_user\flipperzero-calendar`

+ 13 - 0
non_catalog_apps/calendar/application.fam

@@ -0,0 +1,13 @@
+App(
+    appid="calendar",
+    apptype=FlipperAppType.EXTERNAL,
+    name="Calendar",
+    entry_point="calendar_app",
+    stack_size=1 * 1024,
+    fap_icon="icon.png",
+    fap_category="Tools",
+    requires=[
+        "gui",
+        "dolphin",
+    ],
+)

+ 119 - 0
non_catalog_apps/calendar/calendar.c

@@ -0,0 +1,119 @@
+#include "calendar.h"
+#include <furi.h>
+#include <core/check.h>
+#include <core/record.h>
+#include <core/log.h>
+#include <core/log.h>
+#include <furi_hal_rtc.h>
+
+static bool calendar_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    CalendarApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+static bool calendar_app_back_event_callback(void* context) {
+    furi_assert(context);
+    CalendarApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+VariableSharedContext* calendar_app_variable_shared_context_alloc() {
+    FuriHalRtcDateTime datetime;
+    furi_hal_rtc_get_datetime(&datetime);
+    VariableSharedContext* variable_shared_context = malloc(sizeof(VariableSharedContext));
+    variable_shared_context->year_selected = datetime.year;
+    variable_shared_context->month_selected = datetime.month;
+
+    return variable_shared_context;
+}
+
+CalendarApp* calendar_app_alloc() {
+    CalendarApp* app = malloc(sizeof(CalendarApp));
+
+    // Variable shared context
+    app->variable_shared_context = calendar_app_variable_shared_context_alloc();
+
+    // View dispatcher
+    app->view_dispatcher = view_dispatcher_alloc();
+
+    // Scene manager
+    app->scene_manager = scene_manager_alloc(&calendar_scene_handlers, app);
+
+    view_dispatcher_enable_queue(app->view_dispatcher);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, calendar_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, calendar_app_back_event_callback);
+
+    // Open GUI record
+    app->gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    // Views
+    app->calendar_year_picker = calendar_year_picker_alloc(
+        app->variable_shared_context);
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        CalendarAppViewYearPicker,
+        calendar_year_picker_get_view(app->calendar_year_picker));  
+
+    app->calendar_month_picker = calendar_month_picker_alloc(
+        app->variable_shared_context);
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        CalendarAppViewMonthPicker,
+        calendar_month_picker_get_view(app->calendar_month_picker));  
+
+    app->calendar_month_browser = calendar_month_browser_alloc(
+        app->variable_shared_context);
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        CalendarAppViewMonthBrowser,
+        calendar_month_browser_get_view(app->calendar_month_browser));
+
+    scene_manager_next_scene(app->scene_manager, CalendarSceneYearPicker);
+    
+    return app;
+}
+
+void calendar_app_free(CalendarApp* app) {
+    furi_assert(app);
+
+    // Views
+    view_dispatcher_remove_view(
+        app->view_dispatcher, CalendarAppViewYearPicker);
+    calendar_year_picker_free(app->calendar_year_picker);
+
+    view_dispatcher_remove_view(
+        app->view_dispatcher, CalendarAppViewMonthPicker);
+    calendar_month_picker_free(app->calendar_month_picker);
+
+    view_dispatcher_remove_view(
+        app->view_dispatcher, CalendarAppViewMonthBrowser);
+    calendar_month_browser_free(app->calendar_month_browser);
+
+    // View dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+
+    // Scene manager
+    scene_manager_free(app->scene_manager);
+
+    // GUI
+    furi_record_close(RECORD_GUI);
+    app->gui = NULL;
+
+    free(app);
+}
+
+int32_t calendar_app(void* p) {
+    UNUSED(p);
+    CalendarApp* app = calendar_app_alloc();
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    calendar_app_free(app);
+
+    return 0;
+}

+ 27 - 0
non_catalog_apps/calendar/calendar.h

@@ -0,0 +1,27 @@
+#pragma once
+
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include "views/calendar_year_picker.h"
+#include "views/calendar_month_picker.h"
+#include "views/calendar_month_browser.h"
+#include "scenes/calendar_scene.h"
+#include "helpers/variable_shared_context.h"
+
+typedef struct {
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+    YearPicker* calendar_year_picker;
+    MonthPicker* calendar_month_picker;
+    MonthBrowser* calendar_month_browser;
+    VariableSharedContext* variable_shared_context;
+} CalendarApp;
+
+typedef enum {
+    CalendarAppViewYearPicker,
+    CalendarAppViewMonthPicker,
+    CalendarAppViewMonthBrowser,
+} CalendarAppView;

+ 6 - 0
non_catalog_apps/calendar/helpers/calendar_event.h

@@ -0,0 +1,6 @@
+#pragma once
+
+typedef enum {
+    CalendarAppCustomEventYearPicked,
+    CalendarAppCustomEventMontPicked,
+} CalendarAppCustomEvent;

+ 8 - 0
non_catalog_apps/calendar/helpers/variable_shared_context.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include <furi.h>
+
+typedef struct {
+    int16_t year_selected;
+    int8_t month_selected;
+} VariableSharedContext;

BIN
non_catalog_apps/calendar/icon.png


+ 30 - 0
non_catalog_apps/calendar/scenes/calendar_scene.c

@@ -0,0 +1,30 @@
+#include "calendar_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const calendar_scene_on_enter_handlers[])(void*) = {
+#include "calendar_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const calendar_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "calendar_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const calendar_scene_on_exit_handlers[])(void* context) = {
+#include "calendar_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers calendar_scene_handlers = {
+    .on_enter_handlers = calendar_scene_on_enter_handlers,
+    .on_event_handlers = calendar_scene_on_event_handlers,
+    .on_exit_handlers = calendar_scene_on_exit_handlers,
+    .scene_num = CalendarSceneNum,
+};

+ 27 - 0
non_catalog_apps/calendar/scenes/calendar_scene.h

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

+ 3 - 0
non_catalog_apps/calendar/scenes/calendar_scene_config.h

@@ -0,0 +1,3 @@
+ADD_SCENE(calendar, year_picker, YearPicker)
+ADD_SCENE(calendar, month_picker, MonthPicker)
+ADD_SCENE(calendar, month_browser, MonthBrowser)

+ 29 - 0
non_catalog_apps/calendar/scenes/calendar_scene_month_browser.c

@@ -0,0 +1,29 @@
+#include "../calendar.h"
+#include <furi_hal.h>
+#include <gui/scene_manager.h>
+
+void calendar_scene_month_browser_callback(CalendarAppCustomEvent event, void* context) {
+    furi_assert(context);
+    CalendarApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+bool calendar_scene_month_browser_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+
+    return false;
+}
+
+void calendar_scene_month_browser_on_enter(void* context) {
+    CalendarApp* app = context;
+
+    calendar_year_picker_set_callback(
+        app->calendar_year_picker, calendar_scene_month_browser_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, CalendarAppViewMonthBrowser);
+}
+
+void calendar_scene_month_browser_on_exit(void* context) {
+    UNUSED(context);
+}

+ 37 - 0
non_catalog_apps/calendar/scenes/calendar_scene_month_picker.c

@@ -0,0 +1,37 @@
+#include "../calendar.h"
+#include <furi_hal.h>
+#include <gui/scene_manager.h>
+
+void calendar_scene_month_picker_callback(CalendarAppCustomEvent event, void* context) {
+    furi_assert(context);
+    CalendarApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+bool calendar_scene_month_picker_on_event(void* context, SceneManagerEvent event) {
+    CalendarApp* app = context;
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+            case CalendarAppCustomEventMontPicked:
+                scene_manager_next_scene(app->scene_manager, CalendarSceneMonthBrowser);
+                consumed = true;
+                break;
+        }
+    }
+
+    return consumed;
+}
+
+void calendar_scene_month_picker_on_enter(void* context) {
+    CalendarApp* app = context;
+
+    calendar_month_picker_set_callback(
+        app->calendar_month_picker, calendar_scene_month_picker_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, CalendarAppViewMonthPicker);
+}
+
+void calendar_scene_month_picker_on_exit(void* context) {
+    UNUSED(context);
+}

+ 37 - 0
non_catalog_apps/calendar/scenes/calendar_scene_year_picker.c

@@ -0,0 +1,37 @@
+#include "../calendar.h"
+#include <furi_hal.h>
+#include <gui/scene_manager.h>
+
+void calendar_scene_year_picker_callback(CalendarAppCustomEvent event, void* context) {
+    furi_assert(context);
+    CalendarApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+bool calendar_scene_year_picker_on_event(void* context, SceneManagerEvent event) {
+    CalendarApp* app = context;
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+            case CalendarAppCustomEventYearPicked:
+                scene_manager_next_scene(app->scene_manager, CalendarSceneMonthPicker);
+                consumed = true;
+                break;
+        }
+    }
+
+    return consumed;
+}
+
+void calendar_scene_year_picker_on_enter(void* context) {
+    CalendarApp* app = context;
+
+    calendar_year_picker_set_callback(
+        app->calendar_year_picker, calendar_scene_year_picker_callback, app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, CalendarAppViewYearPicker);
+}
+
+void calendar_scene_year_picker_on_exit(void* context) {
+    UNUSED(context);
+}

+ 122 - 0
non_catalog_apps/calendar/views/calendar_month_browser.c

@@ -0,0 +1,122 @@
+#include "calendar_month_browser.h"
+#include <furi.h>
+#include <core/check.h>
+#include <core/record.h>
+#include <core/log.h>
+#include <furi_hal_rtc.h>
+
+#define COLUMN_GAP_PX 17
+#define ROW_GAP_PX 11
+#define GRID_OFFSET_X 3
+#define GRID_OFFSET_Y 8
+#define GRID_TEMPLATE_COLUMNS 7
+#define GRID_TEMPLATE_ROWS 5
+
+struct MonthBrowser {
+    View* view;
+    VariableSharedContext* variable_shared_context;
+};
+
+typedef struct {
+    int16_t year_selected;
+    int8_t month_selected;
+} MonthBrowserViewModel;
+
+static bool is_leap_year(int16_t year) {
+    return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
+}
+
+static int8_t get_days_in_month(int16_t year, int8_t month) {
+    int8_t month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+    if(month - 1 == 1) {
+        bool leap_days = is_leap_year(year);
+        return 28 + leap_days;
+    } else {
+        return month_days[month - 1];
+    }
+}
+
+int8_t get_first_day_of_week(int16_t year, int8_t month) {
+    int16_t a = (14 - month) / 12;
+    int16_t y = year - a;
+    int16_t m = month + 12 * a - 2;
+    return (1 + y + y / 4 - y / 100 + y / 100 + 31 * m / 12 - 1 ) % 7;
+}
+
+static void calendar_month_browser_draw_callback(Canvas* canvas, MonthBrowserViewModel* model) {
+    furi_assert(canvas);
+
+    int8_t days_in_month = get_days_in_month(
+        model->year_selected, model->month_selected);
+    
+    int8_t first_day_of_week = get_first_day_of_week(
+        model->year_selected, model->month_selected);
+
+    canvas_draw_str(canvas, 6, 8, "Su   Mo   Tu   We   Th   Fr   Sa");
+    canvas_set_font(canvas, FontKeyboard);
+
+    for (int8_t week = 1; week <= GRID_TEMPLATE_ROWS; week++) {
+        for (int8_t day_of_week = 1; day_of_week <= GRID_TEMPLATE_COLUMNS; day_of_week++) {
+            
+            int8_t day = (week - 1) * GRID_TEMPLATE_COLUMNS + day_of_week - first_day_of_week;
+
+            if (day > days_in_month)
+                continue;
+
+            if (week == 1 && day_of_week <= first_day_of_week)
+                continue;
+
+            char day_str[5] = {0};
+            snprintf(day_str, sizeof(day_str), "%d", day);
+            canvas_draw_str_aligned(
+                canvas, 
+                GRID_OFFSET_X + day_of_week * COLUMN_GAP_PX, 
+                GRID_OFFSET_Y + week * ROW_GAP_PX, 
+                AlignRight, 
+                AlignBottom, 
+                day_str);
+        }
+    }
+}
+
+void calendar_month_browser_alloc_enter_callback(void* context) {
+    furi_assert(context);
+
+    MonthBrowser* calendar_month_browser = context;
+
+    with_view_model(
+        calendar_month_browser->view,
+        MonthBrowserViewModel * model,
+        {
+            model->year_selected = calendar_month_browser->variable_shared_context->year_selected;  
+            model->month_selected = calendar_month_browser->variable_shared_context->month_selected;
+        },
+        true);
+}
+
+MonthBrowser* calendar_month_browser_alloc(VariableSharedContext* variable_shared_context) {
+    furi_assert(variable_shared_context);
+
+    MonthBrowser* calendar_month_browser = malloc(sizeof(MonthBrowser));
+    calendar_month_browser->variable_shared_context = variable_shared_context;
+    calendar_month_browser->view = view_alloc();
+    
+    view_allocate_model(calendar_month_browser->view, ViewModelTypeLocking, sizeof(MonthBrowserViewModel));
+    view_set_context(calendar_month_browser->view, calendar_month_browser);
+    view_set_draw_callback(calendar_month_browser->view, (ViewDrawCallback)calendar_month_browser_draw_callback);
+
+    view_set_enter_callback(calendar_month_browser->view, calendar_month_browser_alloc_enter_callback);
+
+    return calendar_month_browser;
+}
+
+void calendar_month_browser_free(MonthBrowser* calendar_month_browser) {
+    furi_assert(calendar_month_browser);
+    view_free(calendar_month_browser->view);
+    free(calendar_month_browser);
+}
+
+View* calendar_month_browser_get_view(MonthBrowser* calendar_month_browser) {
+    furi_assert(calendar_month_browser);
+    return calendar_month_browser->view;
+}

+ 13 - 0
non_catalog_apps/calendar/views/calendar_month_browser.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include "../helpers/calendar_event.h"
+#include "../helpers/variable_shared_context.h"
+#include <gui/view.h>
+
+typedef struct MonthBrowser MonthBrowser;
+
+MonthBrowser* calendar_month_browser_alloc(VariableSharedContext* variable_shared_context);
+
+void calendar_month_browser_free(MonthBrowser* calendar_year_picker);
+
+View* calendar_month_browser_get_view(MonthBrowser* calendar_month_browser);

+ 157 - 0
non_catalog_apps/calendar/views/calendar_month_picker.c

@@ -0,0 +1,157 @@
+#include "calendar_month_picker.h"
+#include <furi.h>
+#include <core/check.h>
+#include <core/record.h>
+#include <core/log.h>
+
+#define NUMBER_OF_MONTHS 12
+#define COLUMN_GAP_PX 30
+#define ROW_GAP_PX 17
+#define GRID_TEMPLATE_COLUMNS 4
+#define GRID_TEMPLATE_ROWS 3
+
+struct MonthPicker {
+    View* view;
+    MonthPickerCallback callback;
+    void* context;
+    VariableSharedContext* variable_shared_context;
+};
+
+typedef struct {
+    int16_t grid_cursor;
+} MonthPickerViewModel;
+
+static const char *months[NUMBER_OF_MONTHS] = {
+    "Jan", "Feb", "Mar", "Apr",
+    "May", "Jun", "Jul", "Aug",
+    "Sep", "Oct", "Nov", "Dec"
+};
+
+static void calendar_month_picker_draw_callback(Canvas* canvas, MonthPickerViewModel* model) {
+    furi_assert(canvas);
+
+    canvas_draw_rframe(canvas, 0, 0, 128, 64, 3);
+
+    for(int8_t col = 0; col < GRID_TEMPLATE_COLUMNS; col++) {
+        for(int8_t row = 0; row < GRID_TEMPLATE_ROWS; row++) {
+            int8_t month = col+row*GRID_TEMPLATE_COLUMNS;
+            if(month == model->grid_cursor) {
+                canvas_set_font(canvas, FontPrimary);
+            } else {
+                canvas_set_font(canvas, FontSecondary);
+            }
+            canvas_draw_str_aligned(
+                canvas, 
+                col * COLUMN_GAP_PX + 64 - COLUMN_GAP_PX * (GRID_TEMPLATE_COLUMNS - 1) / 2, 
+                row * ROW_GAP_PX + 32 - ROW_GAP_PX * (GRID_TEMPLATE_ROWS - 1) / 2,
+                AlignCenter,
+                AlignCenter,
+                months[month]);
+        }
+    }
+}
+
+static bool calendar_month_picker_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+
+    MonthPicker* calendar_month_picker = context;
+
+    if(event->key == InputKeyBack || event->type != InputTypeShort) {
+        return false;
+    }
+
+    bool consumed = false;
+
+    with_view_model(
+        calendar_month_picker->view,
+        MonthPickerViewModel * model,
+        {
+            switch(event->key) {
+                case InputKeyOk:
+                    calendar_month_picker->variable_shared_context->month_selected = model->grid_cursor + 1;
+                    calendar_month_picker->callback(CalendarAppCustomEventMontPicked, calendar_month_picker->context);
+                    consumed = true;
+                    break; 
+                    
+                case InputKeyRight:
+                    if (model->grid_cursor + 1 < NUMBER_OF_MONTHS) {
+                        model->grid_cursor++;
+                    }
+                    consumed = true;
+                    break;
+
+                case InputKeyLeft:
+                    if (model->grid_cursor > 0) {
+                        model->grid_cursor--;
+                    }
+                    consumed = true;
+                    break;
+
+                case InputKeyUp:
+                    if (model->grid_cursor > GRID_TEMPLATE_COLUMNS - 1) {
+                        model->grid_cursor -= GRID_TEMPLATE_COLUMNS;
+                    }
+                    consumed = true;
+                    break;
+
+                case InputKeyDown:
+                    if (model->grid_cursor < GRID_TEMPLATE_COLUMNS * (GRID_TEMPLATE_ROWS - 1)) {
+                        model->grid_cursor += GRID_TEMPLATE_COLUMNS;
+                    }
+                    consumed = true;
+                    break;
+
+                default:
+                    break;
+            }
+        },
+        true);
+    
+    return consumed;
+}
+
+void calendar_month_picker_enter_callback(void* context) {
+    MonthPicker* calendar_month_picker = context;
+
+    with_view_model(
+        calendar_month_picker->view,
+        MonthPickerViewModel * model,
+        {
+            model->grid_cursor = calendar_month_picker->variable_shared_context->month_selected - 1;
+        },
+        true);
+}
+
+void calendar_month_picker_set_callback(MonthPicker* calendar_month_picker, MonthPickerCallback callback, void* context) {
+    furi_assert(calendar_month_picker);
+    furi_assert(callback);
+    calendar_month_picker->callback = callback;
+    calendar_month_picker->context = context;
+}
+
+MonthPicker* calendar_month_picker_alloc(VariableSharedContext* variable_shared_context) {
+    furi_assert(variable_shared_context);
+
+    MonthPicker* calendar_month_picker = malloc(sizeof(MonthPicker));
+    calendar_month_picker->variable_shared_context = variable_shared_context;
+    calendar_month_picker->view = view_alloc();
+    
+    view_allocate_model(calendar_month_picker->view, ViewModelTypeLocking, sizeof(MonthPickerViewModel));
+    view_set_context(calendar_month_picker->view, calendar_month_picker);
+    view_set_draw_callback(calendar_month_picker->view, (ViewDrawCallback)calendar_month_picker_draw_callback);
+    view_set_input_callback(calendar_month_picker->view, calendar_month_picker_input_callback);
+    view_set_enter_callback(calendar_month_picker->view, calendar_month_picker_enter_callback);
+
+    return calendar_month_picker;
+}
+
+void calendar_month_picker_free(MonthPicker* calendar_month_picker) {
+    furi_assert(calendar_month_picker);
+    view_free(calendar_month_picker->view);
+    free(calendar_month_picker);
+}
+
+View* calendar_month_picker_get_view(MonthPicker* calendar_month_picker) {
+    furi_assert(calendar_month_picker);
+    return calendar_month_picker->view;
+}

+ 17 - 0
non_catalog_apps/calendar/views/calendar_month_picker.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include "../helpers/calendar_event.h"
+#include "../helpers/variable_shared_context.h"
+#include <gui/view.h>
+
+typedef struct MonthPicker MonthPicker;
+
+typedef void (*MonthPickerCallback)(CalendarAppCustomEvent event, void* context);
+
+void calendar_month_picker_set_callback(MonthPicker* calendar_month_picker, MonthPickerCallback callback, void* context);
+
+MonthPicker* calendar_month_picker_alloc(VariableSharedContext* variable_shared_context);
+
+void calendar_month_picker_free(MonthPicker* calendar_month_picker);
+
+View* calendar_month_picker_get_view(MonthPicker* bt_packet_test);

+ 167 - 0
non_catalog_apps/calendar/views/calendar_year_picker.c

@@ -0,0 +1,167 @@
+#include "calendar_year_picker.h"
+#include <furi.h>
+#include <core/check.h>
+#include <core/record.h>
+#include <core/log.h>
+
+#define COLUMN_GAP_PX 28
+#define ROW_GAP_PX 13
+#define GRID_TEMPLATE_COLUMNS 4
+#define GRID_TEMPLATE_ROWS 4
+
+struct YearPicker {
+    View* view;
+    YearPickerCallback callback;
+    void* context;
+    VariableSharedContext* variable_shared_context;
+};
+
+typedef struct {
+    int16_t grid_cursor;
+    int16_t first_display_year;
+} YearPickerViewModel;
+
+
+static int16_t get_first_display_year(int32_t year) {
+    return year - year % (GRID_TEMPLATE_COLUMNS * GRID_TEMPLATE_ROWS);
+}
+
+static void calendar_year_picker_draw_callback(Canvas* canvas, YearPickerViewModel* model) {
+    furi_assert(canvas);
+
+    canvas_draw_rframe(canvas, 0, 0, 128, 64, 3); 
+
+    char year_str[7] = {0};
+    for(int8_t col = 1; col <= GRID_TEMPLATE_COLUMNS; col++) {
+        for(int8_t row = 1; row <= GRID_TEMPLATE_ROWS; row++) {
+
+            int16_t year = model->first_display_year  + (col - 1) + (GRID_TEMPLATE_COLUMNS * (row - 1));
+
+            if(year == model->grid_cursor) {
+                canvas_set_font(canvas, FontPrimary);
+            } else {
+                canvas_set_font(canvas, FontSecondary);
+            }
+
+            snprintf(year_str, sizeof(year_str), "%d", year);
+            canvas_draw_str_aligned(
+                canvas, 
+                (col - 1) * COLUMN_GAP_PX + 64 - COLUMN_GAP_PX * (GRID_TEMPLATE_COLUMNS - 1) / 2, 
+                (row - 1) * ROW_GAP_PX + 32 - ROW_GAP_PX * (GRID_TEMPLATE_ROWS - 1) / 2,
+                AlignCenter, 
+                AlignCenter, 
+                year_str);
+        }
+    }
+}
+
+static bool calendar_year_picker_input_callback(InputEvent* event, void* context) {
+    furi_assert(context);
+
+    YearPicker* calendar_year_picker = context;
+
+    if(event->key == InputKeyBack || event->type != InputTypeShort) {
+        return false;
+    }
+
+    bool consumed = false;
+
+    with_view_model(
+        calendar_year_picker->view,
+        YearPickerViewModel * model,
+        {
+            switch(event->key) {
+                case InputKeyOk:
+                    calendar_year_picker->variable_shared_context->year_selected = model->grid_cursor;
+                    calendar_year_picker->callback(CalendarAppCustomEventYearPicked, calendar_year_picker->context);
+                    consumed = true;
+                    break; 
+                    
+                case InputKeyRight:
+                    if (model->grid_cursor == model->first_display_year + GRID_TEMPLATE_COLUMNS * GRID_TEMPLATE_ROWS) {
+                        model->first_display_year += GRID_TEMPLATE_COLUMNS * GRID_TEMPLATE_ROWS;
+                    }
+                    model->grid_cursor++;
+                    consumed = true;
+                    break;
+
+                case InputKeyLeft:
+                    if (model->grid_cursor == model->first_display_year) {
+                        model->first_display_year -= GRID_TEMPLATE_COLUMNS * GRID_TEMPLATE_ROWS;
+                    }
+                    model->grid_cursor--;
+                    consumed = true;
+                    break;
+
+                case InputKeyUp:
+                    if (model->grid_cursor - model->first_display_year <= GRID_TEMPLATE_COLUMNS) {
+                        model->first_display_year -= GRID_TEMPLATE_COLUMNS * GRID_TEMPLATE_ROWS;
+                    }
+                    model->grid_cursor-=4;
+                    consumed = true;
+                    break;
+
+                case InputKeyDown:
+                    if (model->grid_cursor - model->first_display_year >= (GRID_TEMPLATE_ROWS - 1) * GRID_TEMPLATE_COLUMNS) {
+                        model->first_display_year += GRID_TEMPLATE_COLUMNS * GRID_TEMPLATE_ROWS;
+                    }
+                    model->grid_cursor+=4;
+                    consumed = true;
+                    break;
+
+                default:
+                    break;
+            }
+        },
+        true);
+    
+    return consumed;
+}
+
+void calendar_year_picker_set_callback(YearPicker* calendar_year_picker, YearPickerCallback callback, void* context) {
+    furi_assert(calendar_year_picker);
+    furi_assert(callback);
+    calendar_year_picker->callback = callback;
+    calendar_year_picker->context = context;
+}
+
+void calendar_year_picker_enter_callback(void* context) {
+    YearPicker* calendar_year_picker = context;
+
+    with_view_model(
+        calendar_year_picker->view,
+        YearPickerViewModel * model,
+        {
+            model->grid_cursor = calendar_year_picker->variable_shared_context->year_selected;
+            model->first_display_year = get_first_display_year(
+                calendar_year_picker->variable_shared_context->year_selected);
+        },
+        true);
+}
+
+YearPicker* calendar_year_picker_alloc(VariableSharedContext* variable_shared_context) {
+    furi_assert(variable_shared_context);
+
+    YearPicker* calendar_year_picker = malloc(sizeof(YearPicker));
+    calendar_year_picker->variable_shared_context = variable_shared_context;
+    calendar_year_picker->view = view_alloc();
+    
+    view_allocate_model(calendar_year_picker->view, ViewModelTypeLocking, sizeof(YearPickerViewModel));
+    view_set_context(calendar_year_picker->view, calendar_year_picker);
+    view_set_draw_callback(calendar_year_picker->view, (ViewDrawCallback)calendar_year_picker_draw_callback);
+    view_set_input_callback(calendar_year_picker->view, calendar_year_picker_input_callback);
+    view_set_enter_callback(calendar_year_picker->view, calendar_year_picker_enter_callback);
+
+    return calendar_year_picker;
+}
+
+void calendar_year_picker_free(YearPicker* calendar_year_picker) {
+    furi_assert(calendar_year_picker);
+    view_free(calendar_year_picker->view);
+    free(calendar_year_picker);
+}
+
+View* calendar_year_picker_get_view(YearPicker* calendar_year_picker) {
+    furi_assert(calendar_year_picker);
+    return calendar_year_picker->view;
+}

+ 17 - 0
non_catalog_apps/calendar/views/calendar_year_picker.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include "../helpers/calendar_event.h"
+#include "../helpers/variable_shared_context.h"
+#include <gui/view.h>
+
+typedef struct YearPicker YearPicker;
+
+typedef void (*YearPickerCallback)(CalendarAppCustomEvent event, void* context);
+
+void calendar_year_picker_set_callback(YearPicker* calendar_year_picker, YearPickerCallback callback, void* context);
+
+YearPicker* calendar_year_picker_alloc(VariableSharedContext* variable_shared_context);
+
+void calendar_year_picker_free(YearPicker* calendar_year_picker);
+
+View* calendar_year_picker_get_view(YearPicker* bt_packet_test);

+ 41 - 0
non_catalog_apps/sudoku/.github/workflows/build.yml

@@ -0,0 +1,41 @@
+name: "FAP: Build for multiple SDK sources"
+# This will build your app for dev and release channels on GitHub. 
+# It will also build your app every day to make sure it's up to date with the latest SDK changes.
+# See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information
+
+on:
+  push:
+    ## put your main branch name under "braches"
+    #branches: 
+    #  - master 
+  pull_request:
+  schedule: 
+    # do a build every day
+    - cron: "1 1 * * *"
+
+jobs:
+  ufbt-build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - name: dev channel
+            sdk-channel: dev
+          - name: release channel
+            sdk-channel: release
+          # You can add unofficial channels here. See ufbt action docs for more info.
+    name: 'ufbt: Build for ${{ matrix.name }}'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Build with ufbt
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1.1
+        id: build-app
+        with:
+          sdk-channel: ${{ matrix.sdk-channel }}
+      - name: Upload app artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          # See ufbt action docs for other output variables
+          name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }}
+          path: ${{ steps.build-app.outputs.fap-artifacts }}

+ 4 - 0
non_catalog_apps/sudoku/.gitignore

@@ -0,0 +1,4 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig

+ 1 - 0
non_catalog_apps/sudoku/README.md

@@ -0,0 +1 @@
+# fz-sudoku

+ 19 - 0
non_catalog_apps/sudoku/application.fam

@@ -0,0 +1,19 @@
+# For details & more options, see documentation/AppManifests.md in firmware repo
+
+App(
+    appid="sudoku",
+    name="Sudoku",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="sudoku_main",
+    requires=[
+        "gui",
+    ],
+    stack_size=1 * 1024,
+    fap_category="Games",
+    fap_version="0.1",
+    fap_icon="sudoku.png",
+    fap_description="Sudoku game",
+    fap_author="@profelis",
+    # fap_weburl="https://github.com/user/template",
+    # fap_icon_assets="images",  # Image assets to compile for this application
+)

+ 0 - 0
non_catalog_apps/sudoku/images/.gitkeep


+ 497 - 0
non_catalog_apps/sudoku/sudoku.c

@@ -0,0 +1,497 @@
+#include <stdio.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <notification/notification_messages.h>
+#include <dolphin/dolphin.h>
+
+#define TAG "sudoku"
+
+#define BOARD_SIZE 9
+#define BOARD_SIZE_3 BOARD_SIZE / 3
+#define FONT_SIZE 6
+
+#define VALUE_MASK 0x0F
+#define FLAGS_MASK ~VALUE_MASK
+#define USER_INPUT_FLAG 0x80
+
+static_assert(USER_INPUT_FLAG > VALUE_MASK);
+
+#define EASY_GAPS 37
+#define NORMAL_GAPS 44
+#define HARD_GAPS 51
+
+typedef enum {
+    GameStateRunning,
+    GameStatePaused,
+    GameStateWin,
+} GameState;
+
+typedef struct {
+    FuriMutex* mutex;
+    uint8_t board[BOARD_SIZE][BOARD_SIZE];
+
+    int8_t cursorX;
+    int8_t cursorY;
+    uint16_t horizontalFlags;
+    uint16_t vertivalFlags;
+    GameState state;
+    int8_t menuCursor;
+} SudokuState;
+
+#define MENU_ITEMS_COUNT 5
+const char* MENU_ITEMS[] = {
+    "Continue",
+    "Easy game",
+    "Nornal game",
+    "Hard game",
+    "Exit",
+};
+
+/*
+Fontname: -Raccoon-Fixed4x6-Medium-R-Normal--6-60-75-75-P-40-ISO10646-1
+Copyright: 
+Glyphs: 95/203
+BBX Build Mode: 0
+*/
+const uint8_t u8g2_font_tom_thumb_4x6_tr[725] =
+    "_\0\2\2\2\3\3\4\4\3\6\0\377\5\377\5\0\0\352\1\330\2\270 \5\340\315\0!\6\265\310"
+    "\254\0\42\6\213\313$\25#\10\227\310\244\241\206\12$\10\227\310\215\70b\2%\10\227\310d\324F\1"
+    "&\10\227\310(\65R\22'\5\251\313\10(\6\266\310\251\62)\10\226\310\304\224\24\0*\6\217\312\244"
+    "\16+\7\217\311\245\225\0,\6\212\310)\0-\5\207\312\14.\5\245\310\4/\7\227\310Ve\4\60"
+    "\7\227\310-k\1\61\6\226\310\255\6\62\10\227\310h\220\312\1\63\11\227\310h\220\62X\0\64\10\227"
+    "\310$\65b\1\65\10\227\310\214\250\301\2\66\10\227\310\315\221F\0\67\10\227\310\314TF\0\70\10\227"
+    "\310\214\64\324\10\71\10\227\310\214\64\342\2:\6\255\311\244\0;\7\222\310e\240\0<\10\227\310\246\32"
+    "d\20=\6\217\311l\60>\11\227\310d\220A*\1\77\10\227\310\314\224a\2@\10\227\310UC\3"
+    "\1A\10\227\310UC\251\0B\10\227\310\250\264\322\2C\7\227\310\315\32\10D\10\227\310\250d-\0"
+    "E\10\227\310\214\70\342\0F\10\227\310\214\70b\4G\10\227\310\315\221\222\0H\10\227\310$\65\224\12"
+    "I\7\227\310\254X\15J\7\227\310\226\252\2K\10\227\310$\265\222\12L\7\227\310\304\346\0M\10\227"
+    "\310\244\61\224\12N\10\227\310\244q\250\0O\7\227\310UV\5P\10\227\310\250\264b\4Q\10\227\310"
+    "Uj$\1R\10\227\310\250\64V\1S\10\227\310m\220\301\2T\7\227\310\254\330\2U\7\227\310$"
+    "W\22V\10\227\310$\253L\0W\10\227\310$\65\206\12X\10\227\310$\325R\1Y\10\227\310$U"
+    "V\0Z\7\227\310\314T\16[\7\227\310\214X\16\134\10\217\311d\220A\0]\7\227\310\314r\4^"
+    "\5\213\313\65_\5\207\310\14`\6\212\313\304\0a\7\223\310\310\65\2b\10\227\310D\225\324\2c\7"
+    "\223\310\315\14\4d\10\227\310\246\245\222\0e\6\223\310\235\2f\10\227\310\246\264b\2g\10\227\307\35"
+    "\61%\0h\10\227\310D\225\254\0i\6\265\310\244\1j\10\233\307f\30U\5k\10\227\310\304\264T"
+    "\1l\7\227\310\310\326\0m\7\223\310<R\0n\7\223\310\250d\5o\7\223\310U\252\2p\10\227"
+    "\307\250\244V\4q\10\227\307-\225d\0r\6\223\310\315\22s\10\223\310\215\70\22\0t\10\227\310\245"
+    "\25\243\0u\7\223\310$+\11v\10\223\310$\65R\2w\7\223\310\244q\4x\7\223\310\244\62\25"
+    "y\11\227\307$\225dJ\0z\7\223\310\254\221\6{\10\227\310\251\32D\1|\6\265\310(\1}\11"
+    "\227\310\310\14RR\0~\6\213\313\215\4\0\0\0\4\377\377\0";
+
+// inspired by game_2048
+static void gray_canvas(Canvas* const canvas) {
+    canvas_set_color(canvas, ColorWhite);
+    for(int x = 0, mx = canvas_width(canvas); x < mx; x += 2) {
+        for(int y = 0, my = canvas_height(canvas); y != my; y++) {
+            canvas_draw_dot(canvas, x + (y % 2 == 1 ? 0 : 1), y);
+        }
+    }
+}
+
+static void draw_callback(Canvas* canvas, void* ctx) {
+    SudokuState* state = ctx;
+    furi_mutex_acquire(state->mutex, FuriWaitForever);
+
+    canvas_clear(canvas);
+    canvas_set_custom_u8g2_font(canvas, u8g2_font_tom_thumb_4x6_tr);
+
+    int gapX = 0;
+    int xOffset = 2;
+    int yOffset = -2;
+    for(int i = 0; i != BOARD_SIZE; ++i) {
+        int gapY = 0;
+        bool vflag = state->vertivalFlags & (1 << i);
+        if((i % 3) == 0) gapX += 2;
+        if(vflag) {
+            // draw vertical hint line
+            canvas_set_color(canvas, ColorBlack);
+            canvas_draw_line(
+                canvas,
+                i * FONT_SIZE + gapX + xOffset - 1,
+                0,
+                i * FONT_SIZE + gapX + xOffset + FONT_SIZE - 3,
+                0);
+        }
+        for(int j = 0; j != BOARD_SIZE; ++j) {
+            if((j % 3) == 0) gapY += 4;
+            canvas_set_color(canvas, ColorBlack);
+            if(i == 0) {
+                bool hflag = state->horizontalFlags & (1 << j);
+                if(hflag) {
+                    // draw horizontal hint line
+                    canvas_draw_line(
+                        canvas,
+                        0,
+                        j * FONT_SIZE + gapY + yOffset + 1,
+                        0,
+                        j * FONT_SIZE + gapY + yOffset + FONT_SIZE - 1);
+                }
+            }
+            bool userInput = state->board[i][j] & USER_INPUT_FLAG;
+            bool cursor = i == state->cursorX && j == state->cursorY;
+            if(!userInput) {
+                int xBoxOffset = cursor ? -1 : 0;
+                // draw black box around the locked number
+                canvas_draw_box(
+                    canvas,
+                    i * FONT_SIZE + gapX - 1 + xBoxOffset + xOffset,
+                    j * FONT_SIZE + gapY + yOffset,
+                    FONT_SIZE - 1 - xBoxOffset * 2,
+                    FONT_SIZE + 1);
+                // text will be white
+                canvas_set_color(canvas, ColorXOR);
+            } else if(cursor) {
+                // draw frame around the cursor
+                canvas_draw_frame(
+                    canvas,
+                    i * FONT_SIZE + gapX - 2 + xOffset,
+                    j * FONT_SIZE + gapY + yOffset,
+                    FONT_SIZE + 1,
+                    FONT_SIZE + 1);
+            }
+            int value = state->board[i][j] & VALUE_MASK;
+            if(value != 0) {
+                canvas_draw_glyph(
+                    canvas,
+                    i * FONT_SIZE + gapX + xOffset,
+                    (j + 1) * FONT_SIZE + gapY + yOffset,
+                    '0' + value);
+            }
+        }
+    }
+    canvas_set_color(canvas, ColorBlack);
+    gapX = 0;
+    int gapY = 0;
+    yOffset = 2;
+    for(int i = 1; i != BOARD_SIZE / 3; ++i) {
+        gapX += i;
+        gapY += i * 2;
+        // vertical lines
+        canvas_draw_line(
+            canvas,
+            i * FONT_SIZE * 3 + xOffset + gapX,
+            yOffset,
+            i * FONT_SIZE * 3 + xOffset + gapX,
+            FONT_SIZE * BOARD_SIZE + 8 + yOffset);
+        // horizontal lines
+        canvas_draw_line(
+            canvas,
+            xOffset,
+            i * FONT_SIZE * 3 + gapY + yOffset,
+            FONT_SIZE * BOARD_SIZE + xOffset + 3,
+            i * FONT_SIZE * 3 + gapY + yOffset);
+    }
+
+    if(state->state == GameStateWin || state->state == GameStatePaused) {
+        gray_canvas(canvas);
+        canvas_set_color(canvas, ColorWhite);
+        int w = canvas_width(canvas);
+        int h = canvas_height(canvas);
+        int winW = 58;
+        int winH = 48;
+        int winX = (w - winW) / 2;
+        int winY = (h - winH) / 2;
+        canvas_draw_rbox(canvas, winX, winY, winW, winH, 4);
+        canvas_set_color(canvas, ColorBlack);
+        canvas_draw_rframe(canvas, winX, winY, winW, winH, 4);
+
+        int offX = 6;
+        int offY = 3;
+        int itemH = FONT_SIZE + 2;
+        for(int i = 0; i < MENU_ITEMS_COUNT; i++) {
+            if(i == state->menuCursor) {
+                canvas_set_color(canvas, ColorBlack);
+                canvas_draw_box(
+                    canvas, winX + offX, winY + offY + itemH * i, winW - offX * 2, itemH);
+            }
+
+            canvas_set_color(canvas, i == state->menuCursor ? ColorWhite : ColorBlack);
+            canvas_draw_str_aligned(
+                canvas,
+                w / 2,
+                winY + offY + itemH * i + itemH / 2,
+                AlignCenter,
+                AlignCenter,
+                i == 0 && state->state == GameStateWin ? "VICTORY!" : MENU_ITEMS[i]);
+        }
+    }
+    furi_mutex_release(state->mutex);
+}
+
+static void input_callback(InputEvent* input_event, void* ctx) {
+    FuriMessageQueue* event_queue = ctx;
+
+    furi_message_queue_put(event_queue, input_event, FuriWaitForever);
+}
+
+static void init_board(SudokuState* state) {
+    for(int i = 0; i != BOARD_SIZE; ++i) {
+        for(int j = 0; j != BOARD_SIZE; ++j) {
+            state->board[i][j] = 1 + (i * BOARD_SIZE_3 + i % BOARD_SIZE_3 + j) % 9;
+        }
+    }
+}
+
+static void shuffle_board(SudokuState* state, int times) {
+    uint8_t tmp[BOARD_SIZE];
+    for(int t = 0; t < times; ++t) {
+        // swap numbers
+        int swapX, swapY;
+        do {
+            swapX = 1 + furi_hal_random_get() % BOARD_SIZE;
+            swapY = 1 + furi_hal_random_get() % BOARD_SIZE;
+        } while(swapX == swapY);
+        for(int i = 0; i != BOARD_SIZE; ++i) {
+            for(int j = 0; j != BOARD_SIZE; ++j) {
+                if(state->board[i][j] == swapX) {
+                    state->board[i][j] = swapY;
+                } else if(state->board[i][j] == swapY) {
+                    state->board[i][j] = swapX;
+                }
+            }
+        }
+        // swap columns
+        for(int i = 0; i != BOARD_SIZE_3; ++i) {
+            int swapX, swapY;
+            int offset = i * BOARD_SIZE_3;
+            do {
+                swapX = offset + furi_hal_random_get() % BOARD_SIZE_3;
+                swapY = offset + furi_hal_random_get() % BOARD_SIZE_3;
+            } while(swapX == swapY);
+            memcpy(tmp, state->board[swapX], BOARD_SIZE);
+            memcpy(state->board[swapX], state->board[swapY], BOARD_SIZE);
+            memcpy(state->board[swapY], tmp, BOARD_SIZE);
+        }
+        // swap rows
+        for(int i = 0; i != BOARD_SIZE_3; ++i) {
+            int swapX, swapY;
+            int offset = i * BOARD_SIZE_3;
+            do {
+                swapX = offset + furi_hal_random_get() % BOARD_SIZE_3;
+                swapY = offset + furi_hal_random_get() % BOARD_SIZE_3;
+            } while(swapX == swapY);
+            for(int k = 0; k != BOARD_SIZE; ++k) {
+                FURI_SWAP(state->board[k][swapX], state->board[k][swapY]);
+            }
+        }
+    }
+}
+
+static void add_gaps(SudokuState* state, int inputCells) {
+    for(int i = 0; i < inputCells; ++i) {
+        int x, y;
+        do {
+            x = furi_hal_random_get() % BOARD_SIZE;
+            y = furi_hal_random_get() % BOARD_SIZE;
+        } while(state->board[x][y] & USER_INPUT_FLAG);
+        state->board[x][y] = USER_INPUT_FLAG;
+    }
+}
+
+static bool validate_board(SudokuState* state) {
+    bool res = true;
+    // check vertical lines for duplicates
+    state->vertivalFlags = 0;
+    for(int i = 0; i != BOARD_SIZE; ++i) {
+        uint flags = 0;
+        bool ok = true;
+        for(int j = 0; j != BOARD_SIZE; ++j) {
+            int value = state->board[i][j] & VALUE_MASK;
+            if(value == 0) {
+                ok = false;
+                res = false;
+            }
+            if(flags & (1 << value)) {
+                ok = false;
+                res = false;
+            }
+            flags |= 1 << value;
+        }
+        if(ok) {
+            state->vertivalFlags |= 1 << i;
+        }
+    }
+    // check horizontal lines for duplicates
+    state->horizontalFlags = 0;
+    for(int i = 0; i != BOARD_SIZE; ++i) {
+        bool ok = true;
+        uint flags = 0;
+        for(int j = 0; j != BOARD_SIZE; ++j) {
+            int value = state->board[j][i] & VALUE_MASK;
+            if(value == 0) {
+                ok = false;
+                res = false;
+            }
+            if(flags & (1 << value)) {
+                ok = false;
+                res = false;
+            }
+            flags |= 1 << value;
+        }
+        if(ok) {
+            state->horizontalFlags |= 1 << i;
+        }
+    }
+    if(!res) {
+        return res;
+    }
+    // check 3x3 squares for duplicates
+    for(int i = 0; i != BOARD_SIZE_3; ++i) {
+        for(int j = 0; j != BOARD_SIZE_3; ++j) {
+            uint flags = 0;
+            for(int k = 0; k != BOARD_SIZE_3; ++k) {
+                for(int l = 0; l != BOARD_SIZE_3; ++l) {
+                    int value = state->board[i * BOARD_SIZE_3 + k][j * BOARD_SIZE_3 + l] &
+                                VALUE_MASK;
+                    if(flags & (1 << value)) {
+                        return false;
+                    }
+                    flags |= 1 << value;
+                }
+            }
+        }
+    }
+    return true;
+}
+
+static bool start_game(SudokuState* state, int inputCells) {
+    state->cursorX = 0;
+    state->cursorY = 0;
+    init_board(state);
+    shuffle_board(state, 10);
+    add_gaps(state, inputCells);
+    return validate_board(state);
+}
+
+int32_t sudoku_main(void* p) {
+    UNUSED(p);
+
+    InputEvent event;
+    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+    SudokuState* state = malloc(sizeof(SudokuState));
+    state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    furi_check(state->mutex, "mutex alloc failed");
+    state->state = GameStateRunning;
+    state->menuCursor = 0;
+    start_game(state, NORMAL_GAPS);
+    ViewPort* view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, draw_callback, state);
+    view_port_input_callback_set(view_port, input_callback, event_queue);
+    view_port_set_orientation(view_port, ViewPortOrientationVertical);
+
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    dolphin_deed(DolphinDeedPluginGameStart);
+
+    while(true) {
+        furi_check(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk);
+
+        furi_mutex_acquire(state->mutex, FuriWaitForever);
+
+        if(state->state == GameStatePaused || state->state == GameStateWin) {
+            bool exit = false;
+            if(event.type == InputTypePress || event.type == InputTypeLong ||
+               event.type == InputTypeRepeat) {
+                switch(event.key) {
+                case InputKeyLeft:
+                case InputKeyUp:
+                    state->menuCursor =
+                        (state->menuCursor + MENU_ITEMS_COUNT - 1) % MENU_ITEMS_COUNT;
+                    break;
+                case InputKeyRight:
+                case InputKeyDown:
+                    state->menuCursor = (state->menuCursor + 1) % MENU_ITEMS_COUNT;
+                    break;
+                case InputKeyOk:
+                    if(state->state == GameStatePaused && state->menuCursor == 0) {
+                        state->state = GameStateRunning;
+                    } else if(state->menuCursor >= 1 && state->menuCursor <= 3) {
+                        state->state = GameStateRunning;
+                        int gaps = state->menuCursor == 1 ? EASY_GAPS :
+                                   state->menuCursor == 2 ? NORMAL_GAPS :
+                                                            HARD_GAPS;
+                        start_game(state, gaps);
+                        state->menuCursor = 0;
+                    } else if(state->menuCursor == 4) {
+                        exit = true;
+                        break;
+                    }
+                    break;
+                default:
+                    break;
+                }
+            }
+            if(exit) {
+                furi_mutex_release(state->mutex);
+                break;
+            }
+        } else if(state->state == GameStateRunning) {
+            bool invalidField = false;
+            bool userInput = state->board[state->cursorX][state->cursorY] & USER_INPUT_FLAG;
+            if(event.key == InputKeyBack) {
+                if(event.type == InputTypeLong) {
+                    state->state = GameStatePaused;
+                } else if(userInput && event.type == InputTypeShort) {
+                    invalidField = state->board[state->cursorX][state->cursorY] & VALUE_MASK;
+                    state->board[state->cursorX][state->cursorY] &= FLAGS_MASK;
+                }
+            }
+
+            if(event.type == InputTypePress || event.type == InputTypeLong ||
+               event.type == InputTypeRepeat) {
+                switch(event.key) {
+                case InputKeyLeft:
+                    state->cursorX = (state->cursorX + BOARD_SIZE - 1) % BOARD_SIZE;
+                    break;
+                case InputKeyRight:
+                    state->cursorX = (state->cursorX + 1) % BOARD_SIZE;
+                    break;
+                case InputKeyUp:
+                    state->cursorY = (state->cursorY + BOARD_SIZE - 1) % BOARD_SIZE;
+                    break;
+                case InputKeyDown:
+                    state->cursorY = (state->cursorY + 1) % BOARD_SIZE;
+                    break;
+                case InputKeyOk:
+                    if(userInput) {
+                        int flags = state->board[state->cursorX][state->cursorY] & FLAGS_MASK;
+                        int value = state->board[state->cursorX][state->cursorY] & VALUE_MASK;
+                        state->board[state->cursorX][state->cursorY] = flags | ((value + 1) % 10);
+                        invalidField = true;
+                    }
+                    break;
+                default:
+                    break;
+                }
+            }
+            if(invalidField && validate_board(state)) {
+                dolphin_deed(DolphinDeedPluginGameWin);
+                state->state = GameStateWin;
+                state->menuCursor = 0;
+                for(int i = 0; i != BOARD_SIZE; ++i) {
+                    for(int j = 0; j != BOARD_SIZE; ++j) {
+                        state->board[i][j] &= ~USER_INPUT_FLAG;
+                    }
+                }
+            }
+        }
+        furi_mutex_release(state->mutex);
+        view_port_update(view_port);
+    }
+
+    furi_message_queue_free(event_queue);
+
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    view_port_free(view_port);
+    furi_record_close(RECORD_GUI);
+
+    furi_mutex_free(state->mutex);
+    free(state);
+
+    return 0;
+}

BIN
non_catalog_apps/sudoku/sudoku.png