Przeglądaj źródła

Merge flip_store from https://github.com/jblanked/FlipStore

# Conflicts:
#	flip_store/flip_store.c
#	flip_store/flip_store.h
Willy-JL 1 rok temu
rodzic
commit
eaa0c03081

+ 0 - 2
flip_store/.gitignore

@@ -1,2 +0,0 @@
-
-.DS_Store

+ 6 - 16
flip_store/README.md

@@ -1,12 +1,13 @@
 # FlipStore
-Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no longer need another device to install apps. FlipStore uses the FlipperHTTP flash for the WiFi Devboard, first introduced in the WebCrawler app: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP
+Download Flipper Zero apps directly to your Flipper Zero using WiFi. 
 
 ## Features
 - App Catalog
 - Install Apps
 - Delete Apps 
-- Install Developer Board Flashes
-- Install Custom Apps (coming soon)
+- Install WiFi Developer Board Firmware
+- Install Video Game Module Firmware
+- Install GitHub Repositories (Beta)
 - Install Official Firmware (coming soon)
 
 ## Installation
@@ -38,18 +39,7 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long
 - UX Improvements
 
 **v0.8**
-- Download custom apps/assets from a GitHub URL
+- Download GitHub repositories
 
 **1.0**
-- Download Official Firmware/Firmware Updates
-
-## Contribution
-This is a big task, and I welcome all contributors, especially developers interested in animations and graphics. Fork the repository, create a pull request, and I will review your edits.
-
-## Known Bugs
-1. Clicking a category in the app catalog results in an "Out of Memory" error.
-   - This issue has been addressed, but it may still occur. If it does, restart the app.
-2. The app file is corrupted.
-   - This is likely due to an error parsing the data. Restart the app and wait until the green LED light turns off after downloading the app before exiting the view. If this happens more than three times, the current version of FlipStore may not be able to download that app successfully.
-3. The app is frozen on the "Installing", "Loading", or "Receiving data" screen. 
-   - If it there LED is not on and it's been more than 5 seconds, restart your Flipper Zero with the devboard plugged in.
+- Download Official Flipper Zero Firmware

+ 11 - 134
flip_store/alloc/flip_store_alloc.c

@@ -6,133 +6,27 @@ FlipStoreApp *flip_store_app_alloc()
 
     Gui *gui = furi_record_open(RECORD_GUI);
 
-    if (!flipper_http_init(flipper_http_rx_callback, app))
-    {
-        FURI_LOG_E(TAG, "Failed to initialize flipper http");
-        return NULL;
-    }
-
-    // Allocate the text input buffer
-    app->uart_text_input_buffer_size_ssid = 64;
-    app->uart_text_input_buffer_size_pass = 64;
-    if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_pass, app->uart_text_input_buffer_size_pass))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_size_pass))
-    {
-        return NULL;
-    }
-
     // Allocate ViewDispatcher
     if (!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app))
     {
         return NULL;
     }
     view_dispatcher_set_custom_event_callback(app->view_dispatcher, flip_store_custom_event_callback);
+
     // Main view
     if (!easy_flipper_set_view(&app->view_loader, FlipStoreViewLoader, flip_store_loader_draw_callback, NULL, callback_to_submenu_options, &app->view_dispatcher, app))
     {
         return NULL;
     }
     flip_store_loader_init(app->view_loader);
-    if (!easy_flipper_set_widget(&app->widget_result, FlipStoreViewWidgetResult, "Error, try again.", callback_to_submenu_options, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-
-    // Main view
-    if (!easy_flipper_set_view(&app->view_app_info, FlipStoreViewAppInfo, flip_store_view_draw_callback_app_list, flip_store_input_callback, callback_to_app_list, &app->view_dispatcher, app))
-    {
-        return NULL;
-    }
-
-    // Widget
-    if (!easy_flipper_set_widget(
-            &app->widget,
-            FlipStoreViewAbout,
-            "Welcome to the FlipStore!\n------\nDownload apps via WiFi and\nrun them on your Flipper!\n------\nwww.github.com/jblanked",
-            callback_to_submenu,
-            &app->view_dispatcher))
-    {
-        return NULL;
-    }
-
-    // Popup
-    if (!easy_flipper_set_popup(&app->popup, FlipStoreViewPopup, "Failed", 0, 0, "You are not connected to Wifi.\n\nIf you have the FlipperHTTP\nflash installed, then update\nyour WiFi credentials.", 0, 10, popup_callback, callback_to_submenu, &app->view_dispatcher, app))
-    {
-        FURI_LOG_E(TAG, "Failed to create popup");
-    }
 
-    // Dialog
-    if (!easy_flipper_set_dialog_ex(
-            &app->dialog_delete,
-            FlipStoreViewAppDelete,
-            "Delete App",
-            0,
-            0,
-            "Are you sure you want to delete this app?",
-            0,
-            10,
-            "No",
-            "Yes",
-            NULL,
-            dialog_delete_callback,
-            callback_to_app_list,
-            &app->view_dispatcher,
-            app))
-    {
-        return NULL;
-    }
-
-    if (!easy_flipper_set_dialog_ex(
-            &app->dialog_firmware,
-            FlipStoreViewFirmwareDialog,
-            "Download Firmware",
-            0,
-            0,
-            "Are you sure you want to\ndownload this firmware?",
-            0,
-            10,
-            "No",
-            "Yes",
-            NULL,
-            dialog_firmware_callback,
-            callback_to_firmware_list,
-            &app->view_dispatcher,
-            app))
-    {
-        return NULL;
-    }
-
-    // Text Input
-    if (!easy_flipper_set_uart_text_input(&app->uart_text_input_ssid, FlipStoreViewTextInputSSID, "Enter SSID", app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid, flip_store_text_updated_ssid, callback_to_submenu, &app->view_dispatcher, app))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_uart_text_input(&app->uart_text_input_pass, FlipStoreViewTextInputPass, "Enter Password", app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_size_pass, flip_store_text_updated_pass, callback_to_submenu, &app->view_dispatcher, app))
-    {
-        return NULL;
-    }
-
-    // Variable Item List
-    if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipStoreViewSettings, settings_item_selected, callback_to_submenu, &app->view_dispatcher, app))
+    if (!easy_flipper_set_widget(&app->widget_result, FlipStoreViewWidgetResult, "Error, try again.", callback_to_submenu_options, &app->view_dispatcher))
     {
         return NULL;
     }
-    app->variable_item_ssid = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL);
-    app->variable_item_pass = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL);
 
     // Submenu
-    if (!easy_flipper_set_submenu(&app->submenu_main, FlipStoreViewSubmenu, "FlipStore v0.7", callback_exit_app, &app->view_dispatcher))
+    if (!easy_flipper_set_submenu(&app->submenu_main, FlipStoreViewSubmenu, VERSION_TAG, callback_exit_app, &app->view_dispatcher))
     {
         return NULL;
     }
@@ -144,7 +38,11 @@ FlipStoreApp *flip_store_app_alloc()
     {
         return NULL;
     }
-    if (!easy_flipper_set_submenu(&app->submenu_firmwares, FlipStoreViewFirmwares, "ESP32 Firmwares", callback_to_submenu_options, &app->view_dispatcher))
+    if (!easy_flipper_set_submenu(&app->submenu_firmwares, FlipStoreViewFirmwares, "ESP32 Firmware", callback_to_submenu_options, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_vgm_firmwares, FlipStoreViewVGMFirmwares, "VGM Firmware", callback_to_submenu_options, &app->view_dispatcher))
     {
         return NULL;
     }
@@ -155,8 +53,9 @@ FlipStoreApp *flip_store_app_alloc()
     submenu_add_item(app->submenu_main, "Settings", FlipStoreSubmenuIndexSettings, callback_submenu_choices, app);
     //
     submenu_add_item(app->submenu_options, "App Catalog", FlipStoreSubmenuIndexAppList, callback_submenu_choices, app);
-    submenu_add_item(app->submenu_options, "ESP32 Firmwares", FlipStoreSubmenuIndexFirmwares, callback_submenu_choices, app);
-
+    submenu_add_item(app->submenu_options, "ESP32 Firmware", FlipStoreSubmenuIndexFirmwares, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_options, "VGM Firmware", FlipStoreSubmenuIndexVGMFirmwares, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_options, "GitHub Repository", FlipStoreSubmenuIndexGitHub, callback_submenu_choices, app);
     //
     submenu_add_item(app->submenu_app_list, "Bluetooth", FlipStoreSubmenuIndexAppListBluetooth, callback_submenu_choices, app);
     submenu_add_item(app->submenu_app_list, "Games", FlipStoreSubmenuIndexAppListGames, callback_submenu_choices, app);
@@ -170,28 +69,6 @@ FlipStoreApp *flip_store_app_alloc()
     submenu_add_item(app->submenu_app_list, "Tools", FlipStoreSubmenuIndexAppListTools, callback_submenu_choices, app);
     submenu_add_item(app->submenu_app_list, "USB", FlipStoreSubmenuIndexAppListUSB, callback_submenu_choices, app);
     //
-    // dont add any items to the app list submenu of each category yet
-
-    // load settings
-    if (load_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid, app->uart_text_input_buffer_pass, app->uart_text_input_buffer_size_pass))
-    {
-        // Update variable items
-        if (app->variable_item_ssid)
-            variable_item_set_current_value_text(app->variable_item_ssid, app->uart_text_input_buffer_ssid);
-        // do not display the password
-
-        // Copy items into their temp buffers with safety checks
-        if (app->uart_text_input_buffer_ssid && app->uart_text_input_temp_buffer_ssid)
-        {
-            strncpy(app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid - 1);
-            app->uart_text_input_temp_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0';
-        }
-        if (app->uart_text_input_buffer_pass && app->uart_text_input_temp_buffer_pass)
-        {
-            strncpy(app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_pass, app->uart_text_input_buffer_size_pass - 1);
-            app->uart_text_input_temp_buffer_pass[app->uart_text_input_buffer_size_pass - 1] = '\0';
-        }
-    }
 
     // Switch to the main view
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenu);

+ 28 - 7
flip_store/app.c

@@ -1,6 +1,5 @@
 #include <flip_store.h>
 #include <alloc/flip_store_alloc.h>
-#include <apps/flip_store_apps.h>
 
 // Entry point for the Hello World application
 int32_t main_flip_store(void *p)
@@ -9,25 +8,47 @@ int32_t main_flip_store(void *p)
     UNUSED(p);
 
     // Initialize the Hello World application
-    app_instance = flip_store_app_alloc();
-    if (!app_instance)
+    FlipStoreApp *app = flip_store_app_alloc();
+    if (!app)
     {
         FURI_LOG_E(TAG, "Failed to allocate FlipStoreApp");
         return -1;
     }
 
-    if (!flipper_http_ping())
+    // check if board is connected (Derek Jamison)
+    FlipperHTTP *fhttp = flipper_http_alloc();
+    if (!fhttp)
+    {
+        easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero.");
+        return -1;
+    }
+
+    if (!flipper_http_ping(fhttp))
     {
         FURI_LOG_E(TAG, "Failed to ping the device");
+        flipper_http_free(fhttp);
         return -1;
     }
 
+    // Try to wait for pong response.
+    uint32_t counter = 10;
+    while (fhttp->state == INACTIVE && --counter > 0)
+    {
+        FURI_LOG_D(TAG, "Waiting for PONG");
+        furi_delay_ms(100);
+    }
+
+    flipper_http_free(fhttp);
+    if (counter == 0)
+    {
+        easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.");
+    }
+
     // Run the view dispatcher
-    view_dispatcher_run(app_instance->view_dispatcher);
+    view_dispatcher_run(app->view_dispatcher);
 
     // Free the resources used by the Hello World application
-    flip_store_app_free(app_instance);
-    flip_catalog_free();
+    flip_store_app_free(app);
 
     // Return 0 to indicate success
     return 0;

+ 1 - 1
flip_store/application.fam

@@ -10,5 +10,5 @@ App(
     fap_description="Download apps via WiFi directly to your Flipper Zero",
     fap_author="JBlanked",
     fap_weburl="https://github.com/jblanked/FlipStore",
-    fap_version="0.7",
+    fap_version="0.8",
 )

+ 132 - 247
flip_store/apps/flip_store_apps.c

@@ -3,80 +3,72 @@
 FlipStoreAppInfo *flip_catalog = NULL;
 
 uint32_t app_selected_index = 0;
-bool flip_store_sent_request = false;
-bool flip_store_success = false;
-bool flip_store_saved_data = false;
-bool flip_store_saved_success = false;
 uint32_t flip_store_category_index = 0;
+int catalog_iteration = 0;
 
 // define the list of categories
+char *category_ids[] = {
+    "64a69817effe1f448a4053b4", // "Bluetooth",
+    "64971d11be1a76c06747de2f", // "Games",
+    "64971d106617ba37a4bc79b9", // "GPIO",
+    "64971d106617ba37a4bc79b6", // "Infrared",
+    "64971d11be1a76c06747de29", // "iButton",
+    "64971d116617ba37a4bc79bc", // "Media",
+    "64971d10be1a76c06747de26", // "NFC",
+    "64971d10577d519190ede5c2", // "RFID",
+    "64971d0f6617ba37a4bc79b3", // "Sub-GHz",
+    "64971d11577d519190ede5c5", // "Tools",
+    "64971d11be1a76c06747de2c", // "USB",
+};
+
 char *categories[] = {
-    "Bluetooth",
-    "Games",
-    "GPIO",
-    "Infrared",
-    "iButton",
-    "Media",
-    "NFC",
-    "RFID",
-    "Sub-GHz",
-    "Tools",
-    "USB",
+    "Bluetooth", // "64a69817effe1f448a4053b4"
+    "Games",     // "64971d11be1a76c06747de2f"
+    "GPIO",      // "64971d106617ba37a4bc79b9"
+    "Infrared",  // "64971d106617ba37a4bc79b6"
+    "iButton",   // "64971d11be1a76c06747de29"
+    "Media",     // "64971d116617ba37a4bc79bc"
+    "NFC",       // "64971d10be1a76c06747de26"
+    "RFID",      // "64971d10577d519190ede5c2"
+    "Sub-GHz",   // "64971d0f6617ba37a4bc79b3"
+    "Tools",     // "64971d11577d519190ede5c5"
+    "USB",       // "64971d11be1a76c06747de2c"
 };
 
 FlipStoreAppInfo *flip_catalog_alloc()
 {
-    FlipStoreAppInfo *app_catalog = (FlipStoreAppInfo *)malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo));
-    if (!app_catalog)
+    if (memmgr_get_free_heap() < MAX_APP_COUNT * sizeof(FlipStoreAppInfo))
     {
-        FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog.");
+        FURI_LOG_E(TAG, "Not enough memory to allocate flip_catalog.");
         return NULL;
     }
-    for (int i = 0; i < MAX_APP_COUNT; i++)
+    FlipStoreAppInfo *app_catalog = malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo));
+    if (!app_catalog)
     {
-        app_catalog[i].app_name = (char *)malloc(MAX_APP_NAME_LENGTH * sizeof(char));
-        if (!app_catalog[i].app_name)
-        {
-            FURI_LOG_E(TAG, "Failed to allocate memory for app_name.");
-            return NULL;
-        }
-        app_catalog[i].app_id = (char *)malloc(MAX_APP_NAME_LENGTH * sizeof(char));
-        if (!app_catalog[i].app_id)
-        {
-            FURI_LOG_E(TAG, "Failed to allocate memory for app_id.");
-            return NULL;
-        }
-        app_catalog[i].app_build_id = (char *)malloc(MAX_ID_LENGTH * sizeof(char));
-        if (!app_catalog[i].app_build_id)
-        {
-            FURI_LOG_E(TAG, "Failed to allocate memory for app_build_id.");
-            return NULL;
-        }
-        app_catalog[i].app_version = (char *)malloc(MAX_APP_VERSION_LENGTH * sizeof(char));
-        if (!app_catalog[i].app_version)
-        {
-            FURI_LOG_E(TAG, "Failed to allocate memory for app_version.");
-            return NULL;
-        }
-        app_catalog[i].app_description = (char *)malloc(MAX_APP_DESCRIPTION_LENGTH * sizeof(char));
-        if (!app_catalog[i].app_description)
-        {
-            FURI_LOG_E(TAG, "Failed to allocate memory for app_description.");
-            return NULL;
-        }
+        FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog.");
+        return NULL;
     }
+    app_catalog->count = 0;
+    app_catalog->iteration = catalog_iteration;
     return app_catalog;
 }
+
 void flip_catalog_free()
 {
     if (flip_catalog)
     {
         free(flip_catalog);
+        flip_catalog = NULL;
     }
 }
 
-bool flip_store_process_app_list()
+bool flip_store_process_app_list(FlipperHTTP *fhttp)
 {
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL.");
+        return false;
+    }
     // Initialize the flip_catalog
     flip_catalog = flip_catalog_alloc();
     if (!flip_catalog)
@@ -85,244 +77,137 @@ bool flip_store_process_app_list()
         return false;
     }
 
-    FuriString *feed_data = flipper_http_load_from_file(fhttp.file_path);
+    FuriString *feed_data = flipper_http_load_from_file(fhttp->file_path);
     if (feed_data == NULL)
     {
         FURI_LOG_E(TAG, "Failed to load received data from file.");
         return false;
     }
 
-    char *data_cstr = (char *)furi_string_get_cstr(feed_data);
-    if (data_cstr == NULL)
+    FuriString *json_data_str = furi_string_alloc();
+    if (!json_data_str)
+    {
+        FURI_LOG_E("Game", "Failed to allocate json_data string");
+        return NULL;
+    }
+    furi_string_cat_str(json_data_str, "{\"json_data\":");
+    if (memmgr_get_free_heap() < furi_string_size(feed_data) + furi_string_size(json_data_str) + 2)
     {
-        FURI_LOG_E(TAG, "Failed to get C-string from FuriString.");
+        FURI_LOG_E(TAG, "Not enough memory to allocate json_data_str.");
         furi_string_free(feed_data);
+        furi_string_free(json_data_str);
         return false;
     }
+    furi_string_cat(json_data_str, feed_data);
+    furi_string_free(feed_data);
+    furi_string_cat_str(json_data_str, "}");
 
-    // Parser state variables
-    bool in_string = false;
-    bool is_escaped = false;
-    bool reading_key = false;
-    bool reading_value = false;
-    bool inside_app_object = false;
-    bool found_name = false, found_id = false, found_build_id = false, found_version = false, found_description = false;
-    char current_key[MAX_KEY_LENGTH] = {0};
-    size_t key_index = 0;
-    char current_value[MAX_VALUE_LENGTH] = {0};
-    size_t value_index = 0;
-    int app_count = 0;
-    enum ObjectState object_state = OBJECT_EXPECT_KEY;
-    enum
-    {
-        STATE_SEARCH_APPS_KEY,
-        STATE_SEARCH_ARRAY_START,
-        STATE_READ_ARRAY_ELEMENTS,
-        STATE_DONE
-    } state = STATE_SEARCH_APPS_KEY;
+    flip_catalog->count = 0;
 
-    // Iterate through the data
-    for (size_t i = 0; data_cstr[i] != '\0' && state != STATE_DONE; ++i)
+    // parse the JSON data
+    for (int i = 0; i < MAX_APP_COUNT; i++)
     {
-        char c = data_cstr[i];
-
-        if (is_escaped)
+        FuriString *json_data_array = get_json_array_value_furi("json_data", i, json_data_str);
+        if (!json_data_array)
         {
-            is_escaped = false;
-            if (reading_key && key_index < MAX_KEY_LENGTH - 1)
-            {
-                current_key[key_index++] = c;
-            }
-            else if (reading_value && value_index < MAX_VALUE_LENGTH - 1)
-            {
-                current_value[value_index++] = c;
-            }
-            continue;
+            break;
         }
 
-        if (c == '\\')
+        FuriString *app_id = get_json_value_furi("alias", json_data_array);
+        if (!app_id)
         {
-            is_escaped = true;
-            continue;
+            FURI_LOG_E(TAG, "Failed to get app_id.");
+            furi_string_free(json_data_array);
+            break;
         }
+        snprintf(flip_catalog[i].app_id, MAX_ID_LENGTH, "%s", furi_string_get_cstr(app_id));
+        furi_string_free(app_id);
 
-        if (c == '\"')
+        FuriString *current_version = get_json_value_furi("current_version", json_data_array);
+        if (!current_version)
         {
-            in_string = !in_string;
-            if (in_string)
-            {
-                if (!reading_key && !reading_value)
-                {
-                    if (state == STATE_SEARCH_APPS_KEY || object_state == OBJECT_EXPECT_KEY)
-                    {
-                        reading_key = true;
-                        key_index = 0;
-                        current_key[0] = '\0';
-                    }
-                    else if (object_state == OBJECT_EXPECT_VALUE)
-                    {
-                        reading_value = true;
-                        value_index = 0;
-                        current_value[0] = '\0';
-                    }
-                }
-            }
-            else
-            {
-                if (reading_key)
-                {
-                    reading_key = false;
-                    current_key[key_index] = '\0';
-                    if (state == STATE_SEARCH_APPS_KEY && strcmp(current_key, "apps") == 0)
-                    {
-                        state = STATE_SEARCH_ARRAY_START;
-                    }
-                    else if (inside_app_object)
-                    {
-                        object_state = OBJECT_EXPECT_COLON;
-                    }
-                }
-                else if (reading_value)
-                {
-                    reading_value = false;
-                    current_value[value_index] = '\0';
-
-                    if (inside_app_object)
-                    {
-                        if (strcmp(current_key, "name") == 0)
-                        {
-                            snprintf(flip_catalog[app_count].app_name, MAX_APP_NAME_LENGTH, "%.31s", current_value);
-                            found_name = true;
-                        }
-                        else if (strcmp(current_key, "id") == 0)
-                        {
-                            snprintf(flip_catalog[app_count].app_id, MAX_ID_LENGTH, "%.31s", current_value);
-                            found_id = true;
-                        }
-                        else if (strcmp(current_key, "build_id") == 0)
-                        {
-                            snprintf(flip_catalog[app_count].app_build_id, MAX_ID_LENGTH, "%.31s", current_value);
-                            found_build_id = true;
-                        }
-                        else if (strcmp(current_key, "version") == 0)
-                        {
-                            snprintf(flip_catalog[app_count].app_version, MAX_APP_VERSION_LENGTH, "%.3s", current_value);
-                            found_version = true;
-                        }
-                        else if (strcmp(current_key, "description") == 0)
-                        {
-                            snprintf(flip_catalog[app_count].app_description, MAX_APP_DESCRIPTION_LENGTH, "%.99s", current_value);
-                            found_description = true;
-                        }
-
-                        if (found_name && found_id && found_build_id && found_version && found_description)
-                        {
-                            app_count++;
-                            if (app_count >= MAX_APP_COUNT)
-                            {
-                                FURI_LOG_I(TAG, "Reached maximum app count.");
-                                state = STATE_DONE;
-                                break;
-                            }
-
-                            found_name = found_id = found_build_id = found_version = found_description = false;
-                        }
+            FURI_LOG_E(TAG, "Failed to get current_version.");
+            furi_string_free(json_data_array);
+            break;
+        }
 
-                        object_state = OBJECT_EXPECT_COMMA_OR_END;
-                    }
-                }
-            }
-            continue;
+        FuriString *app_name = get_json_value_furi("name", current_version);
+        if (!app_name)
+        {
+            FURI_LOG_E(TAG, "Failed to get app_name.");
+            furi_string_free(json_data_array);
+            furi_string_free(current_version);
+            break;
         }
+        snprintf(flip_catalog[i].app_name, MAX_APP_NAME_LENGTH, "%s", furi_string_get_cstr(app_name));
+        furi_string_free(app_name);
 
-        if (in_string)
+        FuriString *app_description = get_json_value_furi("short_description", current_version);
+        if (!app_description)
         {
-            if (reading_key && key_index < MAX_KEY_LENGTH - 1)
-            {
-                current_key[key_index++] = c;
-            }
-            else if (reading_value && value_index < MAX_VALUE_LENGTH - 1)
-            {
-                current_value[value_index++] = c;
-            }
-            continue;
+            FURI_LOG_E(TAG, "Failed to get app_description.");
+            furi_string_free(json_data_array);
+            furi_string_free(current_version);
+            break;
         }
+        snprintf(flip_catalog[i].app_description, MAX_APP_DESCRIPTION_LENGTH, "%s", furi_string_get_cstr(app_description));
+        furi_string_free(app_description);
 
-        if (state == STATE_SEARCH_ARRAY_START && c == '[')
+        FuriString *app_version = get_json_value_furi("version", current_version);
+        if (!app_version)
         {
-            state = STATE_READ_ARRAY_ELEMENTS;
-            continue;
+            FURI_LOG_E(TAG, "Failed to get app_version.");
+            furi_string_free(json_data_array);
+            furi_string_free(current_version);
+            break;
         }
+        snprintf(flip_catalog[i].app_version, MAX_APP_VERSION_LENGTH, "%s", furi_string_get_cstr(app_version));
+        furi_string_free(app_version);
 
-        if (state == STATE_READ_ARRAY_ELEMENTS)
+        FuriString *_id = get_json_value_furi("_id", current_version);
+        if (!_id)
         {
-            if (c == '{')
-            {
-                inside_app_object = true;
-                object_state = OBJECT_EXPECT_KEY;
-            }
-            else if (c == '}')
-            {
-                inside_app_object = false;
-            }
-            else if (c == ':')
-            {
-                object_state = OBJECT_EXPECT_VALUE;
-            }
-            else if (c == ',')
-            {
-                object_state = OBJECT_EXPECT_KEY;
-            }
-            else if (c == ']')
-            {
-                state = STATE_DONE;
-                break;
-            }
+            FURI_LOG_E(TAG, "Failed to get _id.");
+            furi_string_free(json_data_array);
+            furi_string_free(current_version);
+            break;
         }
+        snprintf(flip_catalog[i].app_build_id, MAX_ID_LENGTH, "%s", furi_string_get_cstr(_id));
+        furi_string_free(_id);
+
+        flip_catalog->count++;
+        furi_string_free(json_data_array);
+        furi_string_free(current_version);
     }
 
-    // Clean up
-    furi_string_free(feed_data);
-    free(data_cstr);
-    return app_count > 0;
+    furi_string_free(json_data_str);
+    return flip_catalog->count > 0;
 }
 
-bool flip_store_get_fap_file(char *build_id, uint8_t target, uint16_t api_major, uint16_t api_minor)
+static bool flip_store_get_fap_file(FlipperHTTP *fhttp, char *build_id, uint8_t target, uint16_t api_major, uint16_t api_minor)
 {
-    char url[128];
-    fhttp.save_received_data = false;
-    fhttp.is_bytes_request = true;
+    if (!fhttp || !build_id)
+    {
+        FURI_LOG_E(TAG, "FlipperHTTP or build_id is NULL.");
+        return false;
+    }
+    char url[256];
+    fhttp->save_received_data = false;
+    fhttp->is_bytes_request = true;
     snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d", build_id, target, api_major, api_minor);
-    char *headers = jsmn("Content-Type", "application/octet-stream");
-    bool sent_request = flipper_http_get_request_bytes(url, headers);
-    free(headers);
-    return sent_request;
+    return flipper_http_get_request_bytes(fhttp, url, "{\"Content-Type\": \"application/octet-stream\"}");
 }
 
-bool flip_store_install_app(char *category)
+bool flip_store_install_app(FlipperHTTP *fhttp, char *category)
 {
-    // create /apps/FlipStore directory if it doesn't exist
-    char directory_path[128];
-    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps/%s", category);
-
-    // Create the directory
-    Storage *storage = furi_record_open(RECORD_STORAGE);
-    storage_common_mkdir(storage, directory_path);
-
-    snprintf(fhttp.file_path, sizeof(fhttp.file_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", category, flip_catalog[app_selected_index].app_id);
-
-    uint8_t target = furi_hal_version_get_hw_target();
-    uint16_t api_major, api_minor;
-    furi_hal_info_get_api_version(&api_major, &api_minor);
-    if (fhttp.state != INACTIVE && flip_store_get_fap_file(flip_catalog[app_selected_index].app_build_id, target, api_major, api_minor))
-    {
-        fhttp.state = RECEIVING;
-        return true;
-    }
-    else
+    if (!fhttp || !category)
     {
-        FURI_LOG_E(TAG, "Failed to send the request");
-        flip_store_success = false;
+        FURI_LOG_E(TAG, "FlipperHTTP or category is NULL.");
         return false;
     }
+    snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", category, flip_catalog[app_selected_index].app_id);
+    uint8_t target = furi_hal_version_get_hw_target();
+    uint16_t api_major, api_minor;
+    furi_hal_info_get_api_version(&api_major, &api_minor);
+    return flip_store_get_fap_file(fhttp, flip_catalog[app_selected_index].app_build_id, target, api_major, api_minor);
 }

+ 12 - 15
flip_store/apps/flip_store_apps.h

@@ -8,29 +8,28 @@
 // Define maximum limits
 #define MAX_APP_NAME_LENGTH 32
 #define MAX_ID_LENGTH 32
-#define MAX_APP_COUNT 100
+#define MAX_APP_COUNT 50
 #define MAX_APP_DESCRIPTION_LENGTH 100
 #define MAX_APP_VERSION_LENGTH 5
 
 // define the list of categories
+extern char *category_ids[];
 extern char *categories[];
+extern int catalog_iteration;
 
 typedef struct
 {
-    char *app_name;
-    char *app_id;
-    char *app_build_id;
-    char *app_version;
-    char *app_description;
+    char app_name[MAX_APP_NAME_LENGTH];
+    char app_id[MAX_APP_NAME_LENGTH];
+    char app_build_id[MAX_ID_LENGTH];
+    char app_version[MAX_APP_VERSION_LENGTH];
+    char app_description[MAX_APP_DESCRIPTION_LENGTH];
+    size_t count;
+    int iteration;
 } FlipStoreAppInfo;
 
 extern FlipStoreAppInfo *flip_catalog;
-
 extern uint32_t app_selected_index;
-extern bool flip_store_sent_request;
-extern bool flip_store_success;
-extern bool flip_store_saved_data;
-extern bool flip_store_saved_success;
 extern uint32_t flip_store_category_index;
 
 enum ObjectState
@@ -46,10 +45,8 @@ FlipStoreAppInfo *flip_catalog_alloc();
 void flip_catalog_free();
 
 // Utility function to parse JSON incrementally from a file
-bool flip_store_process_app_list();
-
-bool flip_store_get_fap_file(char *build_id, uint8_t target, uint16_t api_major, uint16_t api_minor);
+bool flip_store_process_app_list(FlipperHTTP *fhttp);
 
 // function to handle the entire installation process "asynchronously"
-bool flip_store_install_app(char *category);
+bool flip_store_install_app(FlipperHTTP *fhttp, char *category);
 #endif // FLIP_STORE_APPS_H

BIN
flip_store/assets/01-main-menu.png


+ 14 - 0
flip_store/assets/CHANGELOG.md

@@ -1,3 +1,17 @@
+## v0.8
+- Updated FlipperHTTP to the latest library.
+- Switched to use Flipper catalog API.
+- Added an option to download Video Game Module firmware (FlipperHTTP)
+- Added an option to download Github repositories.
+- Updated Marauder to the version 1.2
+
+## v0.7.2
+- Final memory allocation improvements
+
+## v0.7.1
+- Improved memory allocation
+- Fixed a crash when re-entering the same app list  
+
 ## v0.7
 - Improved memory allocation
 - Added updates from Derek Jamison

+ 7 - 18
flip_store/assets/README.md

@@ -1,12 +1,12 @@
-# FlipStore
-Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no longer need another device to install apps. FlipStore uses the FlipperHTTP flash for the WiFi Devboard, first introduced in the WebCrawler app: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP
+Download Flipper Zero apps directly to your Flipper Zero using WiFi. 
 
-## Features 
+## Features
 - App Catalog
 - Install Apps
 - Delete Apps 
-- Install Developer Board Flashes
-- Install Custom Apps (coming soon)
+- Install WiFi Developer Board Firmware
+- Install Video Game Module Firmware
+- Install GitHub Repositories (Beta)
 - Install Official Firmware (coming soon)
 
 ## Installation
@@ -38,18 +38,7 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long
 - UX Improvements
 
 **v0.8**
-- Download custom apps from a GitHub URL
+- Download GitHub repositories
 
 **1.0**
-- Download Official Firmware/Firmware Updates
-
-## Contribution
-This is a big task, and I welcome all contributors, especially developers interested in animations and graphics. Fork the repository, create a pull request, and I will review your edits.
-
-## Known Bugs
-1. Clicking a category in the app catalog results in an "Out of Memory" error.
-   - This issue has been addressed, but it may still occur. If it does, restart the app.
-2. The app file is corrupted.
-   - This is likely due to an error parsing the data. Restart the app and wait until the green LED light turns off after downloading the app before exiting the view. If this happens more than three times, the current version of FlipStore may not be able to download that app successfully.
-3. The app is frozen on the "Installing", "Loading", or "Receiving data" screen. 
-   - If it there LED is not on and it's been more than 5 seconds, restart your Flipper Zero with the devboard plugged in.
+- Download Official Flipper Zero Firmware

Plik diff jest za duży
+ 745 - 310
flip_store/callback/flip_store_callback.c


+ 4 - 18
flip_store/callback/flip_store_callback.h

@@ -15,35 +15,20 @@
 extern bool flip_store_app_does_exist;
 extern uint32_t selected_firmware_index;
 
-// Function to draw the description on the canvas with word wrapping
-void draw_description(Canvas *canvas, const char *user_message, int x, int y);
-
-void flip_store_view_draw_callback_app_list(Canvas *canvas, void *model);
-
-bool flip_store_input_callback(InputEvent *event, void *context);
-
-void flip_store_text_updated_ssid(void *context);
-
-void flip_store_text_updated_pass(void *context);
-
 uint32_t callback_to_submenu(void *context);
 
 uint32_t callback_to_submenu_options(void *context);
 
 uint32_t callback_to_firmware_list(void *context);
+uint32_t callback_to_vgm_firmware_list(void *context);
 
 uint32_t callback_to_app_list(void *context);
 
-void settings_item_selected(void *context, uint32_t index);
-
-void dialog_delete_callback(DialogExResult result, void *context);
-void dialog_firmware_callback(DialogExResult result, void *context);
-
-void popup_callback(void *context);
-
 uint32_t callback_exit_app(void *context);
 void callback_submenu_choices(void *context, uint32_t index);
 
+void free_all_views(FlipStoreApp *app, bool should_free_variable_item_list);
+
 // Add edits by Derek Jamison
 typedef enum DataState DataState;
 enum DataState
@@ -77,6 +62,7 @@ struct DataLoaderModel
     size_t request_count;
     ViewNavigationCallback back_callback;
     FuriTimer *timer;
+    FlipperHTTP *fhttp;
 };
 void flip_store_generic_switch_to_view(FlipStoreApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id);
 

+ 20 - 0
flip_store/easy_flipper/easy_flipper.c

@@ -1,5 +1,25 @@
 #include <easy_flipper/easy_flipper.h>
 
+void easy_flipper_dialog(
+    char *header,
+    char *text)
+{
+    DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS);
+    DialogMessage *message = dialog_message_alloc();
+    dialog_message_set_header(
+        message, header, 64, 0, AlignCenter, AlignTop);
+    dialog_message_set_text(
+        message,
+        text,
+        0,
+        63,
+        AlignLeft,
+        AlignBottom);
+    dialog_message_show(dialogs, message);
+    dialog_message_free(message);
+    furi_record_close(RECORD_DIALOGS);
+}
+
 /**
  * @brief Navigation callback for exiting the application
  * @param context The context - unused

+ 7 - 0
flip_store/easy_flipper/easy_flipper.h

@@ -8,6 +8,7 @@
 #include <gui/view.h>
 #include <gui/modules/submenu.h>
 #include <gui/view_dispatcher.h>
+#include <gui/elements.h>
 #include <gui/modules/menu.h>
 #include <gui/modules/submenu.h>
 #include <gui/modules/widget.h>
@@ -15,6 +16,8 @@
 #include <gui/modules/text_box.h>
 #include <gui/modules/variable_item_list.h>
 #include <gui/modules/dialog_ex.h>
+#include <notification/notification.h>
+#include <dialogs/dialogs.h>
 #include <gui/modules/popup.h>
 #include <gui/modules/loading.h>
 #include <stdio.h>
@@ -23,6 +26,10 @@
 
 #define EASY_TAG "EasyFlipper"
 
+void easy_flipper_dialog(
+    char *header,
+    char *text);
+
 /**
  * @brief Navigation callback for exiting the application
  * @param context The context - unused

+ 66 - 73
flip_store/firmwares/flip_store_firmwares.c

@@ -1,17 +1,8 @@
 #include <firmwares/flip_store_firmwares.h>
 
 Firmware *firmwares = NULL;
-bool sent_firmware_request = false;
-bool sent_firmware_request_2 = false;
-bool sent_firmware_request_3 = false;
-//
-bool firmware_request_success = false;
-bool firmware_request_success_2 = false;
-bool firmware_request_success_3 = false;
-//
-bool firmware_download_success = false;
-bool firmware_download_success_2 = false;
-bool firmware_download_success_3 = false;
+VGMFirmware *vgm_firmwares = NULL;
+bool is_esp32_firmware = true;
 
 Firmware *firmware_alloc()
 {
@@ -21,93 +12,95 @@ Firmware *firmware_alloc()
         FURI_LOG_E(TAG, "Failed to allocate memory for Firmware");
         return NULL;
     }
-    for (int i = 0; i < FIRMWARE_COUNT; i++)
-    {
-        if (fw[i].name == NULL)
-        {
-            fw[i].name = (char *)malloc(16);
-            if (!fw[i].name)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate memory for Firmware name");
-                return NULL;
-            }
-        }
-        for (int j = 0; j < FIRMWARE_LINKS; j++)
-        {
-            if (fw[i].links[j] == NULL)
-            {
-                fw[i].links[j] = (char *)malloc(256);
-                if (!fw[i].links[j])
-                {
-                    FURI_LOG_E(TAG, "Failed to allocate memory for Firmware links");
-                    return NULL;
-                }
-            }
-        }
-    }
 
     // Black Magic
-    fw[0].name = "Black Magic";
-    fw[0].links[0] = "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/bootloader.bin";
-    fw[0].links[1] = "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/partition-table.bin";
-    fw[0].links[2] = "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/blackmagic.bin";
+    snprintf(fw[0].name, sizeof(fw[0].name), "%s", "Black Magic");
+    snprintf(fw[0].links[0], sizeof(fw[0].links[0]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/bootloader.bin");
+    snprintf(fw[0].links[1], sizeof(fw[0].links[1]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/partition-table.bin");
+    snprintf(fw[0].links[2], sizeof(fw[0].links[2]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/BM/blackmagic.bin");
 
     // FlipperHTTP
-    fw[1].name = "FlipperHTTP";
-    fw[1].links[0] = "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_bootloader.bin";
-    fw[1].links[1] = "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_firmware_a.bin";
-    fw[1].links[2] = "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_partitions.bin";
+    snprintf(fw[1].name, sizeof(fw[1].name), "%s", "FlipperHTTP");
+    snprintf(fw[1].links[0], sizeof(fw[1].links[0]), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_bootloader.bin");
+    snprintf(fw[1].links[1], sizeof(fw[1].links[1]), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_firmware_a.bin");
+    snprintf(fw[1].links[2], sizeof(fw[1].links[2]), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_partitions.bin");
 
     // Marauder
-    fw[2].name = "Marauder";
-    fw[2].links[0] = "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.bootloader.bin";
-    fw[2].links[1] = "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.partitions.bin";
-    fw[2].links[2] = "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/CURRENT/esp32_marauder_v1_1_0_20241128_flipper.bin";
+    snprintf(fw[2].name, sizeof(fw[2].name), "%s", "Marauder");
+    snprintf(fw[2].links[0], sizeof(fw[2].links[0]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.bootloader.bin");
+    snprintf(fw[2].links[1], sizeof(fw[2].links[1]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.partitions.bin");
+    snprintf(fw[2].links[2], sizeof(fw[2].links[2]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/CURRENT/esp32_marauder_v1_2_0_12192024_flipper.bin");
+
+    return fw;
+}
 
-    // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/BM/bootloader.bin
-    // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/BM/partition-table.bin
-    // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/BM/blackmagic.bin
+VGMFirmware *vgm_firmware_alloc()
+{
+    VGMFirmware *fw = (VGMFirmware *)malloc(VGM_FIRMWARE_COUNT * sizeof(VGMFirmware));
+    if (!fw)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate memory for VGM Firmware");
+        return NULL;
+    }
 
-    // https://api.github.com/repos/jblanked/FlipperHTTP/contents/flipper_http_bootloader.bin
-    // https://api.github.com/repos/jblanked/FlipperHTTP/contents/flipper_http_firmware_a.bin
-    // https://api.github.com/repos/jblanked/FlipperHTTP/contents/flipper_http_partitions.bin
+    // FlipperHTTP
+    snprintf(fw[0].name, sizeof(fw[0].name), "%s", "FlipperHTTP");
+    snprintf(fw[0].link, sizeof(fw[0].link), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/Video%20Game%20Module/MicroPython/flipper_http_vgm_micro_python.uf2");
 
-    // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.bootloader.bin
-    // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.partitions.bin
-    // https://api.github.com/repos/FZEEFlasher/fzeeflasher.github.io/contents/resources/CURRENT/esp32_marauder_v1_0_0_20240626_flipper.bin
     return fw;
 }
+
 void firmware_free()
 {
     if (firmwares)
     {
         free(firmwares);
+        firmwares = NULL;
+    }
+}
+void vgm_firmware_free()
+{
+    if (vgm_firmwares)
+    {
+        free(vgm_firmwares);
+        vgm_firmwares = NULL;
     }
 }
 
-bool flip_store_get_firmware_file(char *link, char *name, char *filename)
+bool flip_store_get_firmware_file(FlipperHTTP *fhttp, char *link, char *name, char *filename)
 {
-    if (fhttp.state == INACTIVE)
+    if (!fhttp)
     {
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL");
         return false;
     }
+    if (fhttp->state == INACTIVE)
+    {
+        return false;
+    }
+
     Storage *storage = furi_record_open(RECORD_STORAGE);
+
     char directory_path[64];
-    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/esp_flasher");
-    storage_common_mkdir(storage, directory_path);
-    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/esp_flasher/%s", firmwares[selected_firmware_index].name);
-    storage_common_mkdir(storage, directory_path);
-    snprintf(fhttp.file_path, sizeof(fhttp.file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/esp_flasher/%s/%s", name, filename);
-    fhttp.save_received_data = false;
-    fhttp.is_bytes_request = true;
-    char *headers = jsmn("Content-Type", "application/octet-stream");
-    bool sent_request = flipper_http_get_request_bytes(link, headers);
-    free(headers);
-    if (sent_request)
+    // save in ESP32 flasher directory
+    if (is_esp32_firmware)
+    {
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/esp_flasher");
+        storage_common_mkdir(storage, directory_path);
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/esp_flasher/%s", firmwares[selected_firmware_index].name);
+        storage_common_mkdir(storage, directory_path);
+        snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/esp_flasher/%s/%s", name, filename);
+    }
+    else // install in app_data directory
     {
-        fhttp.state = RECEIVING;
-        return true;
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm");
+        storage_common_mkdir(storage, directory_path);
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm/%s", name);
+        storage_common_mkdir(storage, directory_path);
+        snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm/%s/%s", name, filename);
     }
-    fhttp.state = ISSUE;
-    return false;
+    furi_record_close(RECORD_STORAGE);
+    fhttp->save_received_data = false;
+    fhttp->is_bytes_request = true;
+    return flipper_http_get_request_bytes(fhttp, link, "{\"Content-Type\":\"application/octet-stream\"}");
 }

+ 13 - 13
flip_store/firmwares/flip_store_firmwares.h

@@ -7,25 +7,25 @@
 
 typedef struct
 {
-    char *name;
-    char *links[FIRMWARE_LINKS];
+    char name[16];
+    char links[FIRMWARE_LINKS][256];
 } Firmware;
 
+typedef struct
+{
+    char name[16];
+    char link[256];
+} VGMFirmware;
+
 extern Firmware *firmwares;
+extern VGMFirmware *vgm_firmwares;
 Firmware *firmware_alloc();
+VGMFirmware *vgm_firmware_alloc();
 void firmware_free();
+void vgm_firmware_free();
+extern bool is_esp32_firmware;
 
 // download and waiting process
-bool flip_store_get_firmware_file(char *link, char *name, char *filename);
-
-extern bool sent_firmware_request;
-extern bool sent_firmware_request_2;
-extern bool sent_firmware_request_3;
-extern bool firmware_request_success;
-extern bool firmware_request_success_2;
-extern bool firmware_request_success_3;
-extern bool firmware_download_success;
-extern bool firmware_download_success_2;
-extern bool firmware_download_success_3;
+bool flip_store_get_firmware_file(FlipperHTTP *fhttp, char *link, char *name, char *filename);
 
 #endif // FLIP_STORE_FIRMWARES_H

+ 128 - 0
flip_store/flip_storage/flip_store_storage.c

@@ -315,4 +315,132 @@ cleanup:
         furi_record_close(RECORD_STORAGE);
     }
     return false;
+}
+
+bool save_char(
+    const char *path_name, const char *value)
+{
+    if (!value)
+    {
+        return false;
+    }
+    // Create the directory for saving settings
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data");
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+
+    // Open the settings file
+    File *file = storage_file_alloc(storage);
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s.txt", path_name);
+
+    // Open the file in write mode
+    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write the data to the file
+    size_t data_size = strlen(value) + 1; // Include null terminator
+    if (storage_file_write(file, value, data_size) != data_size)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+
+bool load_char(
+    const char *path_name,
+    char *value,
+    size_t value_size)
+{
+    if (!value)
+    {
+        return false;
+    }
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s.txt", path_name);
+
+    // Open the file for reading
+    if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL; // Return false if the file does not exist
+    }
+
+    // Read data into the buffer
+    size_t read_count = storage_file_read(file, value, value_size);
+    if (storage_file_get_error(file) != FSE_OK)
+    {
+        FURI_LOG_E(HTTP_TAG, "Error reading from file.");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Ensure null-termination
+    value[read_count - 1] = '\0';
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+
+bool save_char_with_path(const char *full_path, const char *value)
+{
+    if (!value)
+    {
+        return false;
+    }
+
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    // Open the file in write mode
+    if (!storage_file_open(file, full_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", full_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write the data to the file
+    size_t data_size = strlen(value) + 1; // Include null terminator
+    if (storage_file_write(file, value, data_size) != data_size)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
 }

+ 10 - 0
flip_store/flip_storage/flip_store_storage.h

@@ -27,4 +27,14 @@ bool app_exists(const char *app_id, const char *app_category);
 // Function to parse JSON incrementally from a file
 bool parse_json_incrementally(const char *file_path, const char *target_key, char *value_buffer, size_t value_buffer_size);
 
+bool save_char(
+    const char *path_name, const char *value);
+
+bool load_char(
+    const char *path_name,
+    char *value,
+    size_t value_size);
+
+bool save_char_with_path(const char *full_path, const char *value);
+
 #endif

+ 15 - 163
flip_store/flip_store.c

@@ -1,204 +1,56 @@
 #include <flip_store.h>
-
-void flip_store_loader_free_model(View *view);
-FlipStoreApp *app_instance = NULL;
+#include <apps/flip_store_apps.h>
 
 // Function to free the resources used by FlipStoreApp
-void flip_store_app_free(FlipStoreApp *app)
-{
-    if (!app)
-    {
+void flip_store_app_free(FlipStoreApp* app) {
+    if(!app) {
         FURI_LOG_E(TAG, "FlipStoreApp is NULL");
         return;
     }
 
     // Free Widget(s)
-    if (app->widget_result)
-    {
+    if(app->widget_result) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewWidgetResult);
         widget_free(app->widget_result);
     }
 
     // Free View(s)
-    if (app->view_loader)
-    {
+    if(app->view_loader) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewLoader);
         flip_store_loader_free_model(app->view_loader);
         view_free(app->view_loader);
     }
 
-    if (app->view_app_info)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppInfo);
-        view_free(app->view_app_info);
-    }
-
     // Free Submenu(s)
-    if (app->submenu_main)
-    {
+    if(app->submenu_main) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSubmenu);
         submenu_free(app->submenu_main);
     }
-    if (app->submenu_options)
-    {
+    if(app->submenu_options) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSubmenuOptions);
         submenu_free(app->submenu_options);
     }
-    if (app->submenu_app_list)
-    {
+    if(app->submenu_app_list) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppList);
         submenu_free(app->submenu_app_list);
     }
-    if (app->submenu_firmwares)
-    {
+    if(app->submenu_firmwares) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewFirmwares);
         submenu_free(app->submenu_firmwares);
     }
-    if (app->submenu_app_list_bluetooth)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListBluetooth);
-        submenu_free(app->submenu_app_list_bluetooth);
-    }
-    if (app->submenu_app_list_games)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListGames);
-        submenu_free(app->submenu_app_list_games);
-    }
-    if (app->submenu_app_list_gpio)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListGPIO);
-        submenu_free(app->submenu_app_list_gpio);
-    }
-    if (app->submenu_app_list_infrared)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListInfrared);
-        submenu_free(app->submenu_app_list_infrared);
-    }
-    if (app->submenu_app_list_ibutton)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListiButton);
-        submenu_free(app->submenu_app_list_ibutton);
-    }
-    if (app->submenu_app_list_media)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListMedia);
-        submenu_free(app->submenu_app_list_media);
-    }
-    if (app->submenu_app_list_nfc)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListNFC);
-        submenu_free(app->submenu_app_list_nfc);
-    }
-    if (app->submenu_app_list_rfid)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListRFID);
-        submenu_free(app->submenu_app_list_rfid);
-    }
-    if (app->submenu_app_list_subghz)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListSubGHz);
-        submenu_free(app->submenu_app_list_subghz);
-    }
-    if (app->submenu_app_list_tools)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListTools);
-        submenu_free(app->submenu_app_list_tools);
-    }
-    if (app->submenu_app_list_usb)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListUSB);
-        submenu_free(app->submenu_app_list_usb);
+    if(app->submenu_vgm_firmwares) {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewVGMFirmwares);
+        submenu_free(app->submenu_vgm_firmwares);
     }
 
-    // Free Widget(s)
-    if (app->widget)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAbout);
-        widget_free(app->widget);
-    }
-
-    // Free Variable Item List(s)
-    if (app->variable_item_list)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSettings);
-        variable_item_list_free(app->variable_item_list);
-    }
-
-    // Free Text Input(s)
-    if (app->uart_text_input_ssid)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewTextInputSSID);
-        text_input_free(app->uart_text_input_ssid);
-    }
-    if (app->uart_text_input_pass)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewTextInputPass);
-        text_input_free(app->uart_text_input_pass);
-    }
-
-    // Free popup
-    if (app->popup)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewPopup);
-        popup_free(app->popup);
-    }
-
-    // Free dialog
-    if (app->dialog_delete)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppDelete);
-        dialog_ex_free(app->dialog_delete);
-    }
-    if (app->dialog_firmware)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewFirmwareDialog);
-        dialog_ex_free(app->dialog_firmware);
-    }
-
-    // deinitalize flipper http
-    flipper_http_deinit();
+    free_all_views(app, true);
 
     // free the view dispatcher
-    view_dispatcher_free(app->view_dispatcher);
+    if(app->view_dispatcher) view_dispatcher_free(app->view_dispatcher);
 
     // close the gui
     furi_record_close(RECORD_GUI);
 
     // free the app
-    free(app);
+    if(app) free(app);
 }
-
-void flip_store_request_error(Canvas *canvas)
-{
-    if (fhttp.last_response != NULL)
-    {
-        if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
-        {
-            canvas_clear(canvas);
-            canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
-            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
-            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
-        }
-        else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL)
-        {
-            canvas_clear(canvas);
-            canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
-            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
-            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
-        }
-        else
-        {
-            FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response);
-            canvas_draw_str(canvas, 0, 42, "Unusual error...");
-            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
-            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
-        }
-    }
-    else
-    {
-        canvas_clear(canvas);
-        canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error.");
-        canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
-        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
-    }
-}

+ 63 - 86
flip_store/flip_store.h

@@ -11,22 +11,32 @@
 #include <notification/notification.h>
 #include <dialogs/dialogs.h>
 #include <jsmn/jsmn.h>
+#include <jsmn/jsmn_furi.h>
 #include <flip_store_icons.h>
-#define TAG "FlipStore"
+
+#define TAG         "FlipStore"
+#define VERSION_TAG "FlipStore v0.8"
+
 #define FIRMWARE_COUNT 3
 #define FIRMWARE_LINKS 3
 
+#define VGM_FIRMWARE_COUNT 1
+#define VGM_FIRMWARE_LINKS 1
+
+#define MAX_GITHUB_FILES 30
+
 // Define the submenu items for our FlipStore application
-typedef enum
-{
+typedef enum {
     FlipStoreSubmenuIndexMain, // Click to start downloading the selected app
     FlipStoreSubmenuIndexAbout,
     FlipStoreSubmenuIndexSettings,
     //
     FlipStoreSubmenuIndexOptions, // Click to view the options
     //
-    FlipStoreSubmenuIndexAppList,
-    FlipStoreSubmenuIndexFirmwares,
+    FlipStoreSubmenuIndexAppList, // Click to view the app list
+    FlipStoreSubmenuIndexFirmwares, // Click to view the ESP32 firmwares
+    FlipStoreSubmenuIndexVGMFirmwares, // Click to view the VGM firmwares
+    FlipStoreSubmenuIndexGitHub, // Click to view the GitHub repository view
     //
     FlipStoreSubmenuIndexAppListBluetooth,
     FlipStoreSubmenuIndexAppListGames,
@@ -40,101 +50,68 @@ typedef enum
     FlipStoreSubmenuIndexAppListTools,
     FlipStoreSubmenuIndexAppListUSB,
     //
-    FlipStoreSubmenuIndexStartFirmwares,
+    FlipStoreSubmenuIndexStartFirmwares = 50,
+    FlipStoreSubmenuIndexStartVGMFirmwares = 60,
     //
     FlipStoreSubmenuIndexStartAppList = 100,
 } FlipStoreSubmenuIndex;
 
 // Define a single view for our FlipStore application
-typedef enum
-{
+typedef enum {
     //
-    FlipStoreViewSubmenu,          // The submenu
-    FlipStoreViewSubmenuOptions,   // The submenu options
-                                   //
-    FlipStoreViewAbout,            // The about screen
-    FlipStoreViewSettings,         // The settings screen
-    FlipStoreViewTextInputSSID,    // The text input screen for SSID
-    FlipStoreViewTextInputPass,    // The text input screen for password
-                                   //
-    FlipStoreViewPopup,            // The popup screen
-                                   //
-    FlipStoreViewAppList,          // The app list screen
-    FlipStoreViewFirmwares,        // The firmwares screen (submenu)
-    FlipStoreViewFirmwareDialog,   // The firmware view (DialogEx) of the selected firmware
-                                   //
-    FlipStoreViewAppInfo,          // The app info screen (widget) of the selected app
-    FlipStoreViewAppDownload,      // The app download screen (widget) of the selected app
-    FlipStoreViewAppDelete,        // The app delete screen (DialogEx) of the selected app
-                                   //
-    FlipStoreViewAppListBluetooth, // the app list screen for Bluetooth
-    FlipStoreViewAppListGames,     // the app list screen for Games
-    FlipStoreViewAppListGPIO,      // the app list screen for GPIO
-    FlipStoreViewAppListInfrared,  // the app list screen for Infrared
-    FlipStoreViewAppListiButton,   // the app list screen for iButton
-    FlipStoreViewAppListMedia,     // the app list screen for Media
-    FlipStoreViewAppListNFC,       // the app list screen for NFC
-    FlipStoreViewAppListRFID,      // the app list screen for RFID
-    FlipStoreViewAppListSubGHz,    // the app list screen for Sub-GHz
-    FlipStoreViewAppListTools,     // the app list screen for Tools
-    FlipStoreViewAppListUSB,       // the app list screen for USB
-                                   //
-                                   //
-    FlipStoreViewWidgetResult,     // The text box that displays the random fact
-    FlipStoreViewLoader,           // The loader screen retrieves data from the internet
+    FlipStoreViewSubmenu, // The submenu
+    FlipStoreViewSubmenuOptions, // The submenu options
+        //
+    FlipStoreViewAbout, // The about screen
+    FlipStoreViewSettings, // The settings screen
+    FlipStoreViewTextInput, // The text input screen
+        //
+    FlipStoreViewAppList, // The app list screen
+    FlipStoreViewFirmwares, // The firmwares screen (submenu)
+    FlipStoreViewVGMFirmwares, // The VGM firmwares screen (submenu)
+    FlipStoreViewAGithub, // The GitHub repository screen
+        //
+    FlipStoreViewFirmwareDialog, // The firmware view (DialogEx) of the selected firmware
+        //
+    FlipStoreViewAppInfo, // The app info screen (widget) of the selected app
+    FlipStoreViewAppDownload, // The app download screen (widget) of the selected app
+    FlipStoreViewAppDelete, // The app delete screen (DialogEx) of the selected app
+        //
+    FlipStoreViewAppListCategory, // the app list screen for each category
+        //
+        //
+    FlipStoreViewWidgetResult, // The text box that displays the random fact
+    FlipStoreViewLoader, // The loader screen retrieves data from the internet
 } FlipStoreView;
 
 // Each screen will have its own view
-typedef struct
-{
-    View *view_loader;
-    Widget *widget_result;
+typedef struct {
+    View* view_loader;
+    Widget* widget_result;
     //
-    ViewDispatcher *view_dispatcher; // Switches between our views
-    View *view_app_info;             // The app info screen (view) of the selected app
+    ViewDispatcher* view_dispatcher; // Switches between our views
+    View* view_app_info; // The app info screen (view) of the selected app
     //
-    DialogEx *dialog_firmware;    // The dialog for installing a firmware
-    View *view_firmware_download; // The firmware download screen (view) of the selected firmware
+    DialogEx* dialog_firmware; // The dialog for installing a firmware
     //
-    Submenu *submenu_main; // The submenu (main)
+    Submenu* submenu_main; // The submenu (main)
     //
-    Submenu *submenu_options;   // The submenu (options)
-    Submenu *submenu_app_list;  // The submenu (app list) for the selected category
-    Submenu *submenu_firmwares; // The submenu (firmwares)
+    Submenu* submenu_options; // The submenu (options)
+    Submenu* submenu_app_list; // The submenu (app list) for the selected category
+    Submenu* submenu_firmwares; // The submenu (firmwares)
+    Submenu* submenu_vgm_firmwares; // The submenu (VGM firmwares)
+    Submenu* submenu_app_list_category; // The submenu (app list) for each category
+    Widget* widget_about; // The widget
+    VariableItemList* variable_item_list; // The variable item list (settngs)
+    VariableItem* variable_item_ssid; // The variable item
+    VariableItem* variable_item_pass; // The variable item
     //
-    Submenu *submenu_app_list_bluetooth; // The submenu (app list) for Bluetooth
-    Submenu *submenu_app_list_games;     // The submenu (app list) for Games
-    Submenu *submenu_app_list_gpio;      // The submenu (app list) for GPIO
-    Submenu *submenu_app_list_infrared;  // The submenu (app list) for Infrared
-    Submenu *submenu_app_list_ibutton;   // The submenu (app list) for iButton
-    Submenu *submenu_app_list_media;     // The submenu (app list) for Media
-    Submenu *submenu_app_list_nfc;       // The submenu (app list) for NFC
-    Submenu *submenu_app_list_rfid;      // The submenu (app list) for RFID
-    Submenu *submenu_app_list_subghz;    // The submenu (app list) for Sub-GHz
-    Submenu *submenu_app_list_tools;     // The submenu (app list) for Tools
-    Submenu *submenu_app_list_usb;       // The submenu (app list) for USB
-    //
-    Widget *widget;                       // The widget
-    Popup *popup;                         // The popup
-    DialogEx *dialog_delete;              // The dialog for deleting an app
-    VariableItemList *variable_item_list; // The variable item list (settngs)
-    VariableItem *variable_item_ssid;     // The variable item
-    VariableItem *variable_item_pass;     // The variable item
-    TextInput *uart_text_input_ssid; // The text input
-    TextInput *uart_text_input_pass; // The text input
-
-    char *uart_text_input_buffer_ssid;         // Buffer for the text input
-    char *uart_text_input_temp_buffer_ssid;    // Temporary buffer for the text input
-    uint32_t uart_text_input_buffer_size_ssid; // Size of the text input buffer
-
-    char *uart_text_input_buffer_pass;         // Buffer for the text input
-    char *uart_text_input_temp_buffer_pass;    // Temporary buffer for the text input
-    uint32_t uart_text_input_buffer_size_pass; // Size of the text input buffer
+    TextInput* uart_text_input; // The text input
+    char* uart_text_input_buffer; // Buffer for the text input
+    char* uart_text_input_temp_buffer; // Temporary buffer for the text input
+    uint32_t uart_text_input_buffer_size; // Size of the text input buffer
 } FlipStoreApp;
 
-void flip_store_app_free(FlipStoreApp *app);
-
-void flip_store_request_error(Canvas *canvas);
-extern FlipStoreApp *app_instance;
+void flip_store_app_free(FlipStoreApp* app);
 
-#endif // FLIP_STORE_E_H
+#endif // FLIP_STORE_E_H

Plik diff jest za duży
+ 401 - 148
flip_store/flipper_http/flipper_http.c


+ 63 - 41
flip_store/flipper_http/flipper_http.h

@@ -1,6 +1,8 @@
-// flipper_http.h
-#ifndef FLIPPER_HTTP_H
-#define FLIPPER_HTTP_H
+// Description: Flipper HTTP API (For use with Flipper Zero and the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP)
+// License: MIT
+// Author: JBlanked
+// File: flipper_http.h
+#pragma once
 
 #include <gui/gui.h>
 #include <gui/view.h>
@@ -20,9 +22,9 @@
 #define UART_CH (momentum_settings.uart_esp_channel)    // UART channel
 #define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds
 #define BAUDRATE (115200)                 // UART baudrate
-#define RX_BUF_SIZE 1024                  // UART RX buffer size
-#define RX_LINE_BUFFER_SIZE 8192          // UART RX line buffer size (increase for large responses)
-#define MAX_FILE_SHOW 8192                // Maximum data from file to show
+#define RX_BUF_SIZE 2048                  // UART RX buffer size
+#define RX_LINE_BUFFER_SIZE 2048          // UART RX line buffer size (increase for large responses)
+#define MAX_FILE_SHOW 12000               // Maximum data from file to show
 #define FILE_BUFFER_SIZE 512              // File buffer size
 
 // Forward declaration for callback
@@ -83,13 +85,11 @@ typedef struct
     bool save_received_data;   // Flag to save the received data to a file
 
     bool just_started_bytes; // Indicates if bytes data reception has just started
-} FlipperHTTP;
 
-extern FlipperHTTP fhttp;
-// Global static array for the line buffer
-extern char rx_line_buffer[RX_LINE_BUFFER_SIZE];
-extern uint8_t file_buffer[FILE_BUFFER_SIZE];
-extern size_t file_buffer_len;
+    char rx_line_buffer[RX_LINE_BUFFER_SIZE];
+    uint8_t file_buffer[FILE_BUFFER_SIZE];
+    size_t file_buffer_len;
+} FlipperHTTP;
 
 // fhttp.last_response holds the last received data from the UART
 
@@ -102,6 +102,7 @@ bool flipper_http_append_to_file(
     char *file_path);
 
 FuriString *flipper_http_load_from_file(char *file_path);
+FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit);
 
 // UART worker thread
 /**
@@ -139,172 +140,189 @@ void _flipper_http_rx_callback(
 // UART initialization function
 /**
  * @brief      Initialize UART.
- * @return     true if the UART was initialized successfully, false otherwise.
- * @param      callback  The callback function to handle received data (ex. flipper_http_rx_callback).
- * @param      context   The context to pass to the callback.
+ * @return     FlipperHTTP context if the UART was initialized successfully, NULL otherwise.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_init(FlipperHTTP_Callback callback, void *context);
+FlipperHTTP *flipper_http_alloc();
 
 // Deinitialize UART
 /**
  * @brief      Deinitialize UART.
  * @return     void
+ * @param fhttp The FlipperHTTP context
  * @note       This function will stop the asynchronous RX, release the serial handle, and free the resources.
  */
-void flipper_http_deinit();
+void flipper_http_free(FlipperHTTP *fhttp);
 
 // Function to send data over UART with newline termination
 /**
  * @brief      Send data over UART with newline termination.
  * @return     true if the data was sent successfully, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      data  The data to send over UART.
  * @note       The data will be sent over UART with a newline character appended.
  */
-bool flipper_http_send_data(const char *data);
+bool flipper_http_send_data(FlipperHTTP *fhttp, const char *data);
 
 // Function to send a PING request
 /**
  * @brief      Send a PING request to check if the Wifi Dev Board is connected.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  * @note       This is best used to check if the Wifi Dev Board is connected.
  * @note       The state will remain INACTIVE until a PONG is received.
  */
-bool flipper_http_ping();
+bool flipper_http_ping(FlipperHTTP *fhttp);
 
 // Function to list available commands
 /**
  * @brief      Send a command to list available commands.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_list_commands();
+bool flipper_http_list_commands(FlipperHTTP *fhttp);
 
 // Function to turn on the LED
 /**
  * @brief      Allow the LED to display while processing.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_led_on();
+bool flipper_http_led_on(FlipperHTTP *fhttp);
 
 // Function to turn off the LED
 /**
  * @brief      Disable the LED from displaying while processing.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_led_off();
+bool flipper_http_led_off(FlipperHTTP *fhttp);
 
 // Function to parse JSON data
 /**
  * @brief      Parse JSON data.
  * @return     true if the JSON data was parsed successfully, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      key       The key to parse from the JSON data.
  * @param      json_data The JSON data to parse.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_parse_json(const char *key, const char *json_data);
+bool flipper_http_parse_json(FlipperHTTP *fhttp, const char *key, const char *json_data);
 
 // Function to parse JSON array data
 /**
  * @brief      Parse JSON array data.
  * @return     true if the JSON array data was parsed successfully, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      key       The key to parse from the JSON array data.
  * @param      index     The index to parse from the JSON array data.
  * @param      json_data The JSON array data to parse.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_parse_json_array(const char *key, int index, const char *json_data);
+bool flipper_http_parse_json_array(FlipperHTTP *fhttp, const char *key, int index, const char *json_data);
 
 // Function to scan for WiFi networks
 /**
  * @brief      Send a command to scan for WiFi networks.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_scan_wifi();
+bool flipper_http_scan_wifi(FlipperHTTP *fhttp);
 
 // Function to save WiFi settings (returns true if successful)
 /**
  * @brief      Send a command to save WiFi settings.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_save_wifi(const char *ssid, const char *password);
+bool flipper_http_save_wifi(FlipperHTTP *fhttp, const char *ssid, const char *password);
 
 // Function to get IP address of WiFi Devboard
 /**
  * @brief      Send a command to get the IP address of the WiFi Devboard
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_ip_address();
+bool flipper_http_ip_address(FlipperHTTP *fhttp);
 
 // Function to get IP address of the connected WiFi network
 /**
  * @brief      Send a command to get the IP address of the connected WiFi network.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_ip_wifi();
+bool flipper_http_ip_wifi(FlipperHTTP *fhttp);
 
 // Function to disconnect from WiFi (returns true if successful)
 /**
  * @brief      Send a command to disconnect from WiFi.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_disconnect_wifi();
+bool flipper_http_disconnect_wifi(FlipperHTTP *fhttp);
 
 // Function to connect to WiFi (returns true if successful)
 /**
  * @brief      Send a command to connect to WiFi.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_connect_wifi();
+bool flipper_http_connect_wifi(FlipperHTTP *fhttp);
 
 // Function to send a GET request
 /**
  * @brief      Send a GET request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the GET request to.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_get_request(const char *url);
+bool flipper_http_get_request(FlipperHTTP *fhttp, const char *url);
 
 // Function to send a GET request with headers
 /**
  * @brief      Send a GET request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the GET request to.
  * @param      headers  The headers to send with the GET request.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_get_request_with_headers(const char *url, const char *headers);
+bool flipper_http_get_request_with_headers(FlipperHTTP *fhttp, const char *url, const char *headers);
 
 // Function to send a GET request with headers and return bytes
 /**
  * @brief      Send a GET request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the GET request to.
  * @param      headers  The headers to send with the GET request.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_get_request_bytes(const char *url, const char *headers);
+bool flipper_http_get_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers);
 
 // Function to send a POST request with headers
 /**
  * @brief      Send a POST request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the POST request to.
  * @param      headers  The headers to send with the POST request.
  * @param      data  The data to send with the POST request.
  * @note       The received data will be handled asynchronously via the callback.
  */
 bool flipper_http_post_request_with_headers(
+    FlipperHTTP *fhttp,
     const char *url,
     const char *headers,
     const char *payload);
@@ -313,23 +331,26 @@ bool flipper_http_post_request_with_headers(
 /**
  * @brief      Send a POST request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the POST request to.
  * @param      headers  The headers to send with the POST request.
  * @param      payload  The data to send with the POST request.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_post_request_bytes(const char *url, const char *headers, const char *payload);
+bool flipper_http_post_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload);
 
 // Function to send a PUT request with headers
 /**
  * @brief      Send a PUT request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the PUT request to.
  * @param      headers  The headers to send with the PUT request.
  * @param      data  The data to send with the PUT request.
  * @note       The received data will be handled asynchronously via the callback.
  */
 bool flipper_http_put_request_with_headers(
+    FlipperHTTP *fhttp,
     const char *url,
     const char *headers,
     const char *payload);
@@ -338,12 +359,14 @@ bool flipper_http_put_request_with_headers(
 /**
  * @brief      Send a DELETE request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the DELETE request to.
  * @param      headers  The headers to send with the DELETE request.
  * @param      data  The data to send with the DELETE request.
  * @note       The received data will be handled asynchronously via the callback.
  */
 bool flipper_http_delete_request_with_headers(
+    FlipperHTTP *fhttp,
     const char *url,
     const char *headers,
     const char *payload);
@@ -353,23 +376,23 @@ bool flipper_http_delete_request_with_headers(
  * @brief      Callback function to handle received data asynchronously.
  * @return     void
  * @param      line     The received line.
- * @param      context  The context passed to the callback.
+ * @param      context  The FlipperHTTP context.
  * @note       The received data will be handled asynchronously via the callback and handles the state of the UART.
  */
 void flipper_http_rx_callback(const char *line, void *context);
 
-// Function to trim leading and trailing spaces and newlines from a constant string
-char *trim(const char *str);
 /**
  * @brief Process requests and parse JSON data asynchronously
+ * @param fhttp The FlipperHTTP context
  * @param http_request The function to send the request
  * @param parse_json The function to parse the JSON
  * @return true if successful, false otherwise
  */
-bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void));
+bool flipper_http_process_response_async(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void));
 
 /**
  * @brief Perform a task while displaying a loading screen
+ * @param fhttp The FlipperHTTP context
  * @param http_request The function to send the request
  * @param parse_response The function to parse the response
  * @param success_view_id The view ID to switch to on success
@@ -377,10 +400,9 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars
  * @param view_dispatcher The view dispatcher to use
  * @return
  */
-void flipper_http_loading_task(bool (*http_request)(void),
+void flipper_http_loading_task(FlipperHTTP *fhttp,
+                               bool (*http_request)(void),
                                bool (*parse_response)(void),
                                uint32_t success_view_id,
                                uint32_t failure_view_id,
                                ViewDispatcher **view_dispatcher);
-
-#endif // FLIPPER_HTTP_H

+ 378 - 0
flip_store/github/flip_store_github.c

@@ -0,0 +1,378 @@
+#include <github/flip_store_github.h>
+#include <flip_storage/flip_store_storage.h>
+
+// Helper to download a file from Github and save it to the storage
+bool flip_store_download_github_file(
+    FlipperHTTP *fhttp,
+    const char *filename,
+    const char *author,
+    const char *repo,
+    const char *link)
+{
+    if (!fhttp || !filename || !author || !repo || !link)
+    {
+        FURI_LOG_E(TAG, "Invalid arguments.");
+        return false;
+    }
+
+    snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s.txt", author, repo, filename);
+    fhttp->state = IDLE;
+    fhttp->save_received_data = false;
+    fhttp->is_bytes_request = true;
+
+    return flipper_http_get_request_bytes(fhttp, link, "{\"Content-Type\":\"application/octet-stream\"}");
+}
+
+bool flip_store_get_github_contents(FlipperHTTP *fhttp, const char *author, const char *repo)
+{
+    // Create Initial directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+
+    char dir[256];
+
+    // create a data directory: /ext/apps_data/flip_store/data
+    snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data");
+    storage_common_mkdir(storage, dir);
+
+    // create a data directory for the author: /ext/apps_data/flip_store/data/author
+    snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s", author);
+    storage_common_mkdir(storage, dir);
+
+    // example path: /ext/apps_data/flip_store/data/author/info.json
+    snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/info.json", author);
+
+    // create a data directory for the repo: /ext/apps_data/flip_store/data/author/repo
+    snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s", author, repo);
+    storage_common_mkdir(storage, dir);
+
+    // example path: /ext/apps_data/flip_store/author
+    snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s", author);
+    storage_common_mkdir(storage, dir);
+
+    // example path: /ext/apps_data/flip_store/author/repo
+    snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s", author, repo);
+    storage_common_mkdir(storage, dir);
+
+    furi_record_close(RECORD_STORAGE);
+
+    // get the contents of the repo
+    char link[256];
+    snprintf(link, sizeof(link), "https://api.github.com/repos/%s/%s/contents", author, repo);
+    fhttp->save_received_data = true;
+    return flipper_http_get_request_with_headers(fhttp, link, "{\"Content-Type\":\"application/json\"}");
+}
+
+#include <stdio.h>
+#include <string.h>
+#include <stdbool.h>
+
+// Assuming necessary headers and definitions for FuriString, FURI_LOG, etc., are included.
+
+bool flip_store_parse_github_contents(char *file_path, const char *author, const char *repo)
+{
+    FURI_LOG_I(TAG, "Parsing Github contents from %s - %s.", author, repo);
+    if (!file_path || !author || !repo)
+    {
+        FURI_LOG_E(TAG, "Invalid arguments.");
+        return false;
+    }
+
+    // Load JSON data from file
+    FuriString *git_data = flipper_http_load_from_file(file_path);
+    if (git_data == NULL)
+    {
+        FURI_LOG_E(TAG, "Failed to load received data from file.");
+        return false;
+    }
+
+    // Allocate a new FuriString to hold the entire JSON structure
+    FuriString *git_data_str = furi_string_alloc();
+    if (!git_data_str)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate git_data_str.");
+        furi_string_free(git_data);
+        return false;
+    }
+
+    // Construct the full JSON string
+    furi_string_cat_str(git_data_str, "{\"json_data\":");
+    furi_string_cat(git_data_str, git_data);
+    furi_string_cat_str(git_data_str, "}");
+    furi_string_free(git_data); // Free the original git_data as it's now part of git_data_str
+
+    // Check available memory
+    const size_t additional_bytes = strlen("{\"json_data\":") + 1; // +1 for the closing "}"
+    if (memmgr_get_free_heap() < furi_string_size(git_data_str) + additional_bytes)
+    {
+        FURI_LOG_E(TAG, "Not enough memory to allocate git_data_str.");
+        furi_string_free(git_data_str);
+        return false;
+    }
+
+    int file_count = 0;
+    char dir[512]; // Increased size to accommodate longer paths if necessary
+    FURI_LOG_I(TAG, "Looping through Github files.");
+    FURI_LOG_I(TAG, "Available memory: %d bytes", memmgr_get_free_heap());
+
+    // Get the C-string and its length for processing
+    char *data = (char *)furi_string_get_cstr(git_data_str);
+    size_t data_len = furi_string_size(git_data_str);
+
+    size_t pos = 0; // Current position in the data string
+    // Locate the start of the JSON array
+    char *array_start = strchr(data, '[');
+    if (!array_start)
+    {
+        FURI_LOG_E(TAG, "Invalid JSON format: '[' not found.");
+        furi_string_free(git_data_str);
+        return false;
+    }
+    pos = array_start - data; // Update position to the start of the array
+    size_t brace_count = 0;
+    size_t obj_start = 0;
+    bool in_string = false; // To handle braces inside strings
+
+    while (pos < data_len && file_count < MAX_GITHUB_FILES)
+    {
+        char current = data[pos];
+
+        // Toggle in_string flag if a quote is found (handling escaped quotes)
+        if (current == '"' && (pos == 0 || data[pos - 1] != '\\'))
+        {
+            in_string = !in_string;
+        }
+
+        if (!in_string)
+        {
+            if (current == '{')
+            {
+                if (brace_count == 0)
+                {
+                    obj_start = pos; // Potential start of a JSON object
+                }
+                brace_count++;
+            }
+            else if (current == '}')
+            {
+                brace_count--;
+                if (brace_count == 0)
+                {
+                    size_t obj_end = pos;
+                    size_t obj_length = obj_end - obj_start + 1;
+                    // Extract the JSON object substring
+                    char *obj_str = malloc(obj_length + 1);
+                    if (!obj_str)
+                    {
+                        FURI_LOG_E(TAG, "Memory allocation failed for obj_str.");
+                        break;
+                    }
+                    strncpy(obj_str, data + obj_start, obj_length);
+                    obj_str[obj_length] = '\0'; // Null-terminate
+
+                    FuriString *json_data_array = furi_string_alloc();
+                    furi_string_set(json_data_array, obj_str); // Set the string to the allocated memory
+                    free(obj_str);                             // Free the temporary C-string
+
+                    if (!json_data_array)
+                    {
+                        FURI_LOG_E(TAG, "Failed to initialize json_data_array.");
+                        break;
+                    }
+
+                    FURI_LOG_I(TAG, "Loaded json data array value %d. Available memory: %d bytes", file_count, memmgr_get_free_heap());
+
+                    // Extract "type" field
+                    FuriString *type = get_json_value_furi("type", json_data_array);
+                    if (!type)
+                    {
+                        FURI_LOG_E(TAG, "Failed to get type.");
+                        furi_string_free(json_data_array);
+                        break;
+                    }
+
+                    // Skip non-file types (e.g., directories)
+                    if (strcmp(furi_string_get_cstr(type), "file") != 0)
+                    {
+                        furi_string_free(type);
+                        furi_string_free(json_data_array);
+                        pos = obj_end + 1; // Move past this object
+                        continue;
+                    }
+                    furi_string_free(type);
+
+                    // Extract "download_url" and "name"
+                    FuriString *download_url = get_json_value_furi("download_url", json_data_array);
+                    if (!download_url)
+                    {
+                        FURI_LOG_E(TAG, "Failed to get download_url.");
+                        furi_string_free(json_data_array);
+                        break;
+                    }
+
+                    FuriString *name = get_json_value_furi("name", json_data_array);
+                    if (!name)
+                    {
+                        FURI_LOG_E(TAG, "Failed to get name.");
+                        furi_string_free(json_data_array);
+                        furi_string_free(download_url);
+                        break;
+                    }
+
+                    furi_string_free(json_data_array);
+                    FURI_LOG_I(TAG, "Received name and download_url. Available memory: %d bytes", memmgr_get_free_heap());
+
+                    // Create JSON to save
+                    FuriString *json = furi_string_alloc();
+                    if (!json)
+                    {
+                        FURI_LOG_E(TAG, "Failed to allocate json.");
+                        furi_string_free(download_url);
+                        furi_string_free(name);
+                        break;
+                    }
+
+                    furi_string_cat_str(json, "{\"name\":\"");
+                    furi_string_cat(json, name);
+                    furi_string_cat_str(json, "\",\"link\":\"");
+                    furi_string_cat(json, download_url);
+                    furi_string_cat_str(json, "\"}");
+
+                    FURI_LOG_I(TAG, "Created json. Available memory: %d bytes", memmgr_get_free_heap());
+
+                    // Save the JSON to the data folder: /ext/apps_data/flip_store/data/author/repo/fileX.json
+                    snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/file%d.json", author, repo, file_count);
+                    if (!save_char_with_path(dir, furi_string_get_cstr(json)))
+                    {
+                        FURI_LOG_E(TAG, "Failed to save json.");
+                    }
+
+                    FURI_LOG_I(TAG, "Saved file %s.", furi_string_get_cstr(name));
+
+                    // Free allocated resources
+                    furi_string_free(name);
+                    furi_string_free(download_url);
+                    furi_string_free(json);
+
+                    file_count++;
+
+                    // This can be expensive for large strings; consider memory constraints
+                    size_t remaining_length = data_len - (obj_end + 1);
+                    memmove(data + obj_start, data + obj_end + 1, remaining_length + 1); // +1 to include null terminator
+                    data_len -= (obj_end + 1 - obj_start);
+                    pos = obj_start; // Reset position to the start of the modified string
+                    continue;
+                }
+            }
+        }
+
+        pos++;
+    }
+
+    furi_string_free(git_data_str);
+
+    // Save file count
+    char file_count_str[16];
+    snprintf(file_count_str, sizeof(file_count_str), "%d", file_count);
+    char file_count_dir[512]; // Increased size for longer paths
+    snprintf(file_count_dir, sizeof(file_count_dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/file_count.txt", author);
+
+    FURI_LOG_I(TAG, "Successfully parsed %d files.", file_count);
+    return save_char_with_path(file_count_dir, file_count_str);
+}
+
+bool flip_store_install_all_github_files(FlipperHTTP *fhttp, const char *author, const char *repo)
+{
+    FURI_LOG_I(TAG, "Installing all Github files.");
+    if (!fhttp || !author || !repo)
+    {
+        FURI_LOG_E(TAG, "Invalid arguments.");
+        return false;
+    }
+    fhttp->state = RECEIVING;
+    // get the file count
+    char file_count_dir[256]; // /ext/apps_data/flip_store/data/author/file_count.txt
+    snprintf(file_count_dir, sizeof(file_count_dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/file_count.txt", author);
+    FuriString *file_count = flipper_http_load_from_file(file_count_dir);
+    if (file_count == NULL)
+    {
+        FURI_LOG_E(TAG, "Failed to load file count.");
+        return false;
+    }
+    int count = atoi(furi_string_get_cstr(file_count));
+    furi_string_free(file_count);
+
+    // install all files
+    char file_dir[256]; // /ext/apps_data/flip_store/data/author/repo/file.json
+    FURI_LOG_I(TAG, "Installing %d files.", count);
+    for (int i = 0; i < count; i++)
+    {
+        snprintf(file_dir, sizeof(file_dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/file%d.json", author, repo, i);
+        FURI_LOG_I(TAG, "Loading file %s. Available memory: %d bytes", file_dir, memmgr_get_free_heap());
+        FuriString *file = flipper_http_load_from_file_with_limit(file_dir, 512);
+        if (!file)
+        {
+            FURI_LOG_E(TAG, "Failed to load file.");
+            return false;
+        }
+        FURI_LOG_I(TAG, "Loaded file %s.", file_dir);
+        FuriString *name = get_json_value_furi("name", file);
+        if (!name)
+        {
+            FURI_LOG_E(TAG, "Failed to get name.");
+            furi_string_free(file);
+            return false;
+        }
+        FuriString *link = get_json_value_furi("link", file);
+        if (!link)
+        {
+            FURI_LOG_E(TAG, "Failed to get link.");
+            furi_string_free(file);
+            furi_string_free(name);
+            return false;
+        }
+        furi_string_free(file);
+
+        bool fetch_file()
+        {
+            return flip_store_download_github_file(fhttp, furi_string_get_cstr(name), author, repo, furi_string_get_cstr(link));
+        }
+
+        bool parse()
+        {
+            // remove .txt from the filename
+            char current_file_path[512];
+            char new_file_path[512];
+            snprintf(current_file_path, sizeof(current_file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s.txt", author, repo, furi_string_get_cstr(name));
+            snprintf(new_file_path, sizeof(new_file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s", author, repo, furi_string_get_cstr(name));
+            Storage *storage = furi_record_open(RECORD_STORAGE);
+            if (!storage_file_exists(storage, current_file_path))
+            {
+                FURI_LOG_E(TAG, "Failed to download file.");
+                furi_record_close(RECORD_STORAGE);
+                return false;
+            }
+            if (storage_common_rename(storage, current_file_path, new_file_path) != FSE_OK)
+            {
+                FURI_LOG_E(TAG, "Failed to rename file.");
+                furi_record_close(RECORD_STORAGE);
+                return false;
+            }
+            furi_record_close(RECORD_STORAGE);
+            return true;
+        }
+        // download the file and wait until it is downloaded
+        FURI_LOG_I(TAG, "Downloading file %s", furi_string_get_cstr(name));
+        if (!flipper_http_process_response_async(fhttp, fetch_file, parse))
+        {
+            FURI_LOG_E(TAG, "Failed to download file.");
+            furi_string_free(name);
+            furi_string_free(link);
+            return false;
+        }
+        FURI_LOG_I(TAG, "Downloaded file %s", furi_string_get_cstr(name));
+        furi_string_free(name);
+        furi_string_free(link);
+    }
+    fhttp->state = IDLE;
+    return true;
+}

+ 26 - 0
flip_store/github/flip_store_github.h

@@ -0,0 +1,26 @@
+#pragma once
+#include <flip_store.h>
+
+/*
+I did try downloading a zip file from Github but a few issues
+- unsure how to unzip the file
+- either Github redirected us to a loading page, or their was no response on if using an IoT device
+- any contributions to this would be appreciated
+*/
+
+// Helper to download a file from Github and save it to the storage
+bool flip_store_download_github_file(
+    FlipperHTTP *fhttp,
+    const char *filename,
+    const char *author,
+    const char *repo,
+    const char *link);
+
+// Helper to get the contents of a Github repo (from https://api.github.com/repos/author/repo/contents)
+bool flip_store_get_github_contents(FlipperHTTP *fhttp, const char *author, const char *repo);
+
+// Helper to parse the contents of a Github repo (takes parts of the data and saves it for easy access later)
+bool flip_store_parse_github_contents(char *file_path, const char *author, const char *repo);
+
+// Helper to install all files from the parsed Github repo contents in flip_store_parse_github_contents
+bool flip_store_install_all_github_files(FlipperHTTP *fhttp, const char *author, const char *repo);

+ 103 - 47
flip_store/jsmn/jsmn.c

@@ -7,8 +7,6 @@
  */
 
 #include <jsmn/jsmn.h>
-#include <stdlib.h>
-#include <string.h>
 
 /**
  * Allocates a fresh unused token from the token pool.
@@ -424,7 +422,7 @@ int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
 }
 
 // Helper function to create a JSON object
-char *jsmn(const char *key, const char *value)
+char *get_json(const char *key, const char *value)
 {
     int length = strlen(key) + strlen(value) + 8;         // Calculate required length
     char *result = (char *)malloc(length * sizeof(char)); // Allocate memory
@@ -448,14 +446,19 @@ int jsoneq(const char *json, jsmntok_t *tok, const char *s)
 }
 
 // Return the value of the key in the JSON data
-char *get_json_value(char *key, char *json_data, uint32_t max_tokens)
+char *get_json_value(char *key, const char *json_data)
 {
     // Parse the JSON feed
     if (json_data != NULL)
     {
         jsmn_parser parser;
         jsmn_init(&parser);
-
+        uint32_t max_tokens = json_token_count(json_data);
+        if (!jsmn_memory_check(max_tokens))
+        {
+            FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+            return NULL;
+        }
         // Allocate tokens array on the heap
         jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens);
         if (tokens == NULL)
@@ -510,26 +513,79 @@ char *get_json_value(char *key, char *json_data, uint32_t max_tokens)
     {
         FURI_LOG_E("JSMM.H", "JSON data is NULL");
     }
-    FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON.");
+    char warning[128];
+    snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key);
+    FURI_LOG_E("JSMM.H", warning);
     return NULL; // Return NULL if something goes wrong
 }
 
-// Revised get_json_array_value function
-char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t max_tokens)
+// Helper function to skip a token and all its descendants.
+// Returns the index of the next token after skipping this one.
+// On error or out of bounds, returns -1.
+static int skip_token(const jsmntok_t *tokens, int start, int total)
 {
-    // Retrieve the array string for the given key
-    char *array_str = get_json_value(key, json_data, max_tokens);
+    if (start < 0 || start >= total)
+        return -1;
+
+    int i = start;
+    if (tokens[i].type == JSMN_OBJECT)
+    {
+        // For an object: size is number of key-value pairs
+        int pairs = tokens[i].size;
+        i++; // move to first key-value pair
+        for (int p = 0; p < pairs; p++)
+        {
+            // skip key (primitive/string)
+            i++;
+            if (i >= total)
+                return -1;
+            // skip value (which could be object/array and must be skipped recursively)
+            i = skip_token(tokens, i, total);
+            if (i == -1)
+                return -1;
+        }
+        return i; // i is now just past the object
+    }
+    else if (tokens[i].type == JSMN_ARRAY)
+    {
+        // For an array: size is number of elements
+        int elems = tokens[i].size;
+        i++; // move to first element
+        for (int e = 0; e < elems; e++)
+        {
+            i = skip_token(tokens, i, total);
+            if (i == -1)
+                return -1;
+        }
+        return i; // i is now just past the array
+    }
+    else
+    {
+        // Primitive or string token, just skip it
+        return i + 1;
+    }
+}
+
+// Revised get_json_array_value
+char *get_json_array_value(char *key, uint32_t index, const char *json_data)
+{
+    // Always extract the full array each time from the original json_data
+    char *array_str = get_json_value(key, json_data);
     if (array_str == NULL)
     {
         FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key);
         return NULL;
     }
+    uint32_t max_tokens = json_token_count(array_str);
+    if (!jsmn_memory_check(max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        free(array_str);
+        return NULL;
+    }
 
-    // Initialize the JSON parser
     jsmn_parser parser;
     jsmn_init(&parser);
-
-    // Allocate memory for JSON tokens
     jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens);
     if (tokens == NULL)
     {
@@ -538,7 +594,6 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
         return NULL;
     }
 
-    // Parse the JSON array
     int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens);
     if (ret < 0)
     {
@@ -548,7 +603,6 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
         return NULL;
     }
 
-    // Ensure the root element is an array
     if (ret < 1 || tokens[0].type != JSMN_ARRAY)
     {
         FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key);
@@ -557,50 +611,33 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
         return NULL;
     }
 
-    // Check if the index is within bounds
     if (index >= (uint32_t)tokens[0].size)
     {
-        FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %d.", (unsigned long)index, tokens[0].size);
+        // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size);
         free(tokens);
         free(array_str);
         return NULL;
     }
 
-    // Locate the token corresponding to the desired array element
-    int current_token = 1; // Start after the array token
+    // Find the index-th element: start from token[1], which is the first element
+    int elem_token = 1;
     for (uint32_t i = 0; i < index; i++)
     {
-        if (tokens[current_token].type == JSMN_OBJECT)
-        {
-            // For objects, skip all key-value pairs
-            current_token += 1 + 2 * tokens[current_token].size;
-        }
-        else if (tokens[current_token].type == JSMN_ARRAY)
-        {
-            // For nested arrays, skip all elements
-            current_token += 1 + tokens[current_token].size;
-        }
-        else
+        elem_token = skip_token(tokens, elem_token, ret);
+        if (elem_token == -1 || elem_token >= ret)
         {
-            // For primitive types, simply move to the next token
-            current_token += 1;
-        }
-
-        // Safety check to prevent out-of-bounds
-        if (current_token >= ret)
-        {
-            FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array.");
+            FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i);
             free(tokens);
             free(array_str);
             return NULL;
         }
     }
 
-    // Extract the array element
-    jsmntok_t element = tokens[current_token];
+    // Now elem_token should point to the token of the requested element
+    jsmntok_t element = tokens[elem_token];
     int length = element.end - element.start;
     char *value = malloc(length + 1);
-    if (value == NULL)
+    if (!value)
     {
         FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element.");
         free(tokens);
@@ -608,11 +645,9 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
         return NULL;
     }
 
-    // Copy the element value to a new string
     strncpy(value, array_str + element.start, length);
-    value[length] = '\0'; // Null-terminate the string
+    value[length] = '\0';
 
-    // Clean up
     free(tokens);
     free(array_str);
 
@@ -620,16 +655,22 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
 }
 
 // Revised get_json_array_values function with correct token skipping
-char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, int *num_values)
+char **get_json_array_values(char *key, char *json_data, int *num_values)
 {
     // Retrieve the array string for the given key
-    char *array_str = get_json_value(key, json_data, max_tokens);
+    char *array_str = get_json_value(key, json_data);
     if (array_str == NULL)
     {
         FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key);
         return NULL;
     }
-
+    uint32_t max_tokens = json_token_count(array_str);
+    if (!jsmn_memory_check(max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        free(array_str);
+        return NULL;
+    }
     // Initialize the JSON parser
     jsmn_parser parser;
     jsmn_init(&parser);
@@ -745,3 +786,18 @@ char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, in
     free(array_str);
     return values;
 }
+
+int json_token_count(const char *json)
+{
+    if (json == NULL)
+    {
+        return JSMN_ERROR_INVAL;
+    }
+
+    jsmn_parser parser;
+    jsmn_init(&parser);
+
+    // Pass NULL for tokens and 0 for num_tokens to get the token count only
+    int ret = jsmn_parse(&parser, json, strlen(json), NULL, 0);
+    return ret; // If ret >= 0, it represents the number of tokens needed.
+}

+ 7 - 65
flip_store/jsmn/jsmn.h

@@ -17,6 +17,7 @@
 #define JSMN_H
 
 #include <stddef.h>
+#include <jsmn/jsmn_h.h>
 
 #ifdef __cplusplus
 extern "C"
@@ -28,61 +29,6 @@ extern "C"
 #else
 #define JSMN_API extern
 #endif
-
-    /**
-     * JSON type identifier. Basic types are:
-     * 	o Object
-     * 	o Array
-     * 	o String
-     * 	o Other primitive: number, boolean (true/false) or null
-     */
-    typedef enum
-    {
-        JSMN_UNDEFINED = 0,
-        JSMN_OBJECT = 1 << 0,
-        JSMN_ARRAY = 1 << 1,
-        JSMN_STRING = 1 << 2,
-        JSMN_PRIMITIVE = 1 << 3
-    } jsmntype_t;
-
-    enum jsmnerr
-    {
-        /* Not enough tokens were provided */
-        JSMN_ERROR_NOMEM = -1,
-        /* Invalid character inside JSON string */
-        JSMN_ERROR_INVAL = -2,
-        /* The string is not a full JSON packet, more bytes expected */
-        JSMN_ERROR_PART = -3
-    };
-
-    /**
-     * JSON token description.
-     * type		type (object, array, string etc.)
-     * start	start position in JSON data string
-     * end		end position in JSON data string
-     */
-    typedef struct
-    {
-        jsmntype_t type;
-        int start;
-        int end;
-        int size;
-#ifdef JSMN_PARENT_LINKS
-        int parent;
-#endif
-    } jsmntok_t;
-
-    /**
-     * JSON parser. Contains an array of token blocks available. Also stores
-     * the string being parsed now and current position in that string.
-     */
-    typedef struct
-    {
-        unsigned int pos;     /* offset in the JSON string */
-        unsigned int toknext; /* next token to allocate */
-        int toksuper;         /* superior token node, e.g. parent object or array */
-    } jsmn_parser;
-
     /**
      * Create JSON parser over an array of tokens
      */
@@ -110,23 +56,19 @@ extern "C"
 #define JB_JSMN_EDIT
 /* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/
 
-#include <string.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <stdio.h>
-#include <furi.h>
-
 // Helper function to create a JSON object
-char *jsmn(const char *key, const char *value);
+char *get_json(const char *key, const char *value);
 // Helper function to compare JSON keys
 int jsoneq(const char *json, jsmntok_t *tok, const char *s);
 
 // Return the value of the key in the JSON data
-char *get_json_value(char *key, char *json_data, uint32_t max_tokens);
+char *get_json_value(char *key, const char *json_data);
 
 // Revised get_json_array_value function
-char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t max_tokens);
+char *get_json_array_value(char *key, uint32_t index, const char *json_data);
 
 // Revised get_json_array_values function with correct token skipping
-char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, int *num_values);
+char **get_json_array_values(char *key, char *json_data, int *num_values);
+
+int json_token_count(const char *json);
 #endif /* JB_JSMN_EDIT */

+ 736 - 0
flip_store/jsmn/jsmn_furi.c

@@ -0,0 +1,736 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010 Serge Zaitsev
+ *
+ * [License text continues...]
+ */
+
+#include <jsmn/jsmn_furi.h>
+
+// Forward declarations of helper functions
+static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s);
+static int skip_token(const jsmntok_t *tokens, int start, int total);
+
+/**
+ * Allocates a fresh unused token from the token pool.
+ */
+static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens,
+                                   const size_t num_tokens)
+{
+    if (parser->toknext >= num_tokens)
+    {
+        return NULL;
+    }
+    jsmntok_t *tok = &tokens[parser->toknext++];
+    tok->start = tok->end = -1;
+    tok->size = 0;
+#ifdef JSMN_PARENT_LINKS
+    tok->parent = -1;
+#endif
+    return tok;
+}
+
+/**
+ * Fills token type and boundaries.
+ */
+static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type,
+                            const int start, const int end)
+{
+    token->type = type;
+    token->start = start;
+    token->end = end;
+    token->size = 0;
+}
+
+/**
+ * Fills next available token with JSON primitive.
+ * Now uses FuriString to access characters.
+ */
+static int jsmn_parse_primitive(jsmn_parser *parser, const FuriString *js,
+                                jsmntok_t *tokens, const size_t num_tokens)
+{
+    size_t len = furi_string_size(js);
+    int start = parser->pos;
+
+    for (; parser->pos < len; parser->pos++)
+    {
+        char c = furi_string_get_char(js, parser->pos);
+        switch (c)
+        {
+#ifndef JSMN_STRICT
+        case ':':
+#endif
+        case '\t':
+        case '\r':
+        case '\n':
+        case ' ':
+        case ',':
+        case ']':
+        case '}':
+            goto found;
+        default:
+            break;
+        }
+        if (c < 32 || c >= 127)
+        {
+            parser->pos = start;
+            return JSMN_ERROR_INVAL;
+        }
+    }
+
+#ifdef JSMN_STRICT
+    // In strict mode primitive must be followed by a comma/object/array
+    parser->pos = start;
+    return JSMN_ERROR_PART;
+#endif
+
+found:
+    if (tokens == NULL)
+    {
+        parser->pos--;
+        return 0;
+    }
+    jsmntok_t *token = jsmn_alloc_token(parser, tokens, num_tokens);
+    if (token == NULL)
+    {
+        parser->pos = start;
+        return JSMN_ERROR_NOMEM;
+    }
+    jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+    token->parent = parser->toksuper;
+#endif
+    parser->pos--;
+    return 0;
+}
+
+/**
+ * Fills next token with JSON string.
+ * Now uses FuriString to access characters.
+ */
+static int jsmn_parse_string(jsmn_parser *parser, const FuriString *js,
+                             jsmntok_t *tokens, const size_t num_tokens)
+{
+    size_t len = furi_string_size(js);
+    int start = parser->pos;
+    parser->pos++;
+
+    for (; parser->pos < len; parser->pos++)
+    {
+        char c = furi_string_get_char(js, parser->pos);
+        if (c == '\"')
+        {
+            if (tokens == NULL)
+            {
+                return 0;
+            }
+            jsmntok_t *token = jsmn_alloc_token(parser, tokens, num_tokens);
+            if (token == NULL)
+            {
+                parser->pos = start;
+                return JSMN_ERROR_NOMEM;
+            }
+            jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+            token->parent = parser->toksuper;
+#endif
+            return 0;
+        }
+
+        if (c == '\\' && (parser->pos + 1) < len)
+        {
+            parser->pos++;
+            char esc = furi_string_get_char(js, parser->pos);
+            switch (esc)
+            {
+            case '\"':
+            case '/':
+            case '\\':
+            case 'b':
+            case 'f':
+            case 'r':
+            case 'n':
+            case 't':
+                break;
+            case 'u':
+            {
+                parser->pos++;
+                for (int i = 0; i < 4 && parser->pos < len; i++)
+                {
+                    char hex = furi_string_get_char(js, parser->pos);
+                    if (!((hex >= '0' && hex <= '9') ||
+                          (hex >= 'A' && hex <= 'F') ||
+                          (hex >= 'a' && hex <= 'f')))
+                    {
+                        parser->pos = start;
+                        return JSMN_ERROR_INVAL;
+                    }
+                    parser->pos++;
+                }
+                parser->pos--;
+                break;
+            }
+            default:
+                parser->pos = start;
+                return JSMN_ERROR_INVAL;
+            }
+        }
+    }
+    parser->pos = start;
+    return JSMN_ERROR_PART;
+}
+
+/**
+ * Create JSON parser
+ */
+void jsmn_init_furi(jsmn_parser *parser)
+{
+    parser->pos = 0;
+    parser->toknext = 0;
+    parser->toksuper = -1;
+}
+
+/**
+ * Parse JSON string and fill tokens.
+ * Now uses FuriString for the input JSON.
+ */
+int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js,
+                    jsmntok_t *tokens, const unsigned int num_tokens)
+{
+    size_t len = furi_string_size(js);
+    int r;
+    int i;
+    int count = parser->toknext;
+
+    for (; parser->pos < len; parser->pos++)
+    {
+        char c = furi_string_get_char(js, parser->pos);
+        jsmntype_t type;
+
+        switch (c)
+        {
+        case '{':
+        case '[':
+        {
+            count++;
+            if (tokens == NULL)
+            {
+                break;
+            }
+            jsmntok_t *token = jsmn_alloc_token(parser, tokens, num_tokens);
+            if (token == NULL)
+                return JSMN_ERROR_NOMEM;
+            if (parser->toksuper != -1)
+            {
+                jsmntok_t *t = &tokens[parser->toksuper];
+#ifdef JSMN_STRICT
+                if (t->type == JSMN_OBJECT)
+                    return JSMN_ERROR_INVAL;
+#endif
+                t->size++;
+#ifdef JSMN_PARENT_LINKS
+                token->parent = parser->toksuper;
+#endif
+            }
+            token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
+            token->start = parser->pos;
+            parser->toksuper = parser->toknext - 1;
+            break;
+        }
+        case '}':
+        case ']':
+            if (tokens == NULL)
+            {
+                break;
+            }
+            type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
+#ifdef JSMN_PARENT_LINKS
+            if (parser->toknext < 1)
+            {
+                return JSMN_ERROR_INVAL;
+            }
+            {
+                jsmntok_t *token = &tokens[parser->toknext - 1];
+                for (;;)
+                {
+                    if (token->start != -1 && token->end == -1)
+                    {
+                        if (token->type != type)
+                            return JSMN_ERROR_INVAL;
+                        token->end = parser->pos + 1;
+                        parser->toksuper = token->parent;
+                        break;
+                    }
+                    if (token->parent == -1)
+                    {
+                        if (token->type != type || parser->toksuper == -1)
+                        {
+                            return JSMN_ERROR_INVAL;
+                        }
+                        break;
+                    }
+                    token = &tokens[token->parent];
+                }
+            }
+#else
+            {
+                jsmntok_t *token;
+                for (i = parser->toknext - 1; i >= 0; i--)
+                {
+                    token = &tokens[i];
+                    if (token->start != -1 && token->end == -1)
+                    {
+                        if (token->type != type)
+                            return JSMN_ERROR_INVAL;
+                        parser->toksuper = -1;
+                        token->end = parser->pos + 1;
+                        break;
+                    }
+                }
+                if (i == -1)
+                    return JSMN_ERROR_INVAL;
+                for (; i >= 0; i--)
+                {
+                    token = &tokens[i];
+                    if (token->start != -1 && token->end == -1)
+                    {
+                        parser->toksuper = i;
+                        break;
+                    }
+                }
+            }
+#endif
+            break;
+        case '\"':
+            r = jsmn_parse_string(parser, js, tokens, num_tokens);
+            if (r < 0)
+                return r;
+            count++;
+            if (parser->toksuper != -1 && tokens != NULL)
+            {
+                tokens[parser->toksuper].size++;
+            }
+            break;
+        case '\t':
+        case '\r':
+        case '\n':
+        case ' ':
+            // Whitespace - ignore
+            break;
+        case ':':
+            parser->toksuper = parser->toknext - 1;
+            break;
+        case ',':
+            if (tokens != NULL && parser->toksuper != -1 &&
+                tokens[parser->toksuper].type != JSMN_ARRAY &&
+                tokens[parser->toksuper].type != JSMN_OBJECT)
+            {
+#ifdef JSMN_PARENT_LINKS
+                parser->toksuper = tokens[parser->toksuper].parent;
+#else
+                for (i = parser->toknext - 1; i >= 0; i--)
+                {
+                    if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT)
+                    {
+                        if (tokens[i].start != -1 && tokens[i].end == -1)
+                        {
+                            parser->toksuper = i;
+                            break;
+                        }
+                    }
+                }
+#endif
+            }
+            break;
+#ifdef JSMN_STRICT
+        case '-':
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+        case 't':
+        case 'f':
+        case 'n':
+            if (tokens != NULL && parser->toksuper != -1)
+            {
+                const jsmntok_t *t = &tokens[parser->toksuper];
+                if (t->type == JSMN_OBJECT ||
+                    (t->type == JSMN_STRING && t->size != 0))
+                {
+                    return JSMN_ERROR_INVAL;
+                }
+            }
+#else
+        default:
+#endif
+            r = jsmn_parse_primitive(parser, js, tokens, num_tokens);
+            if (r < 0)
+                return r;
+            count++;
+            if (parser->toksuper != -1 && tokens != NULL)
+            {
+                tokens[parser->toksuper].size++;
+            }
+            break;
+#ifdef JSMN_STRICT
+        default:
+            return JSMN_ERROR_INVAL;
+#endif
+        }
+    }
+
+    if (tokens != NULL)
+    {
+        for (i = parser->toknext - 1; i >= 0; i--)
+        {
+            if (tokens[i].start != -1 && tokens[i].end == -1)
+            {
+                return JSMN_ERROR_PART;
+            }
+        }
+    }
+
+    return count;
+}
+
+// Helper function to create a JSON object: {"key":"value"}
+FuriString *get_json_furi(const FuriString *key, const FuriString *value)
+{
+    FuriString *result = furi_string_alloc();
+    furi_string_printf(result, "{\"%s\":\"%s\"}",
+                       furi_string_get_cstr(key),
+                       furi_string_get_cstr(value));
+    return result; // Caller responsible for furi_string_free
+}
+
+// Helper function to compare JSON keys
+static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s)
+{
+    size_t s_len = furi_string_size(s);
+    size_t tok_len = tok->end - tok->start;
+
+    if (tok->type != JSMN_STRING)
+        return -1;
+    if (s_len != tok_len)
+        return -1;
+
+    FuriString *sub = furi_string_alloc_set(json);
+    furi_string_mid(sub, tok->start, tok_len);
+
+    int res = furi_string_cmp(sub, s);
+    furi_string_free(sub);
+
+    return (res == 0) ? 0 : -1;
+}
+
+// Skip a token and its descendants
+static int skip_token(const jsmntok_t *tokens, int start, int total)
+{
+    if (start < 0 || start >= total)
+        return -1;
+
+    int i = start;
+    if (tokens[i].type == JSMN_OBJECT)
+    {
+        int pairs = tokens[i].size;
+        i++;
+        for (int p = 0; p < pairs; p++)
+        {
+            i++; // skip key
+            if (i >= total)
+                return -1;
+            i = skip_token(tokens, i, total); // skip value
+            if (i == -1)
+                return -1;
+        }
+        return i;
+    }
+    else if (tokens[i].type == JSMN_ARRAY)
+    {
+        int elems = tokens[i].size;
+        i++;
+        for (int e = 0; e < elems; e++)
+        {
+            i = skip_token(tokens, i, total);
+            if (i == -1)
+                return -1;
+        }
+        return i;
+    }
+    else
+    {
+        return i + 1;
+    }
+}
+
+/**
+ * Parse JSON and return the value associated with a given char* key.
+ */
+FuriString *get_json_value_furi(const char *key, const FuriString *json_data)
+{
+    if (json_data == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "JSON data is NULL");
+        return NULL;
+    }
+    uint32_t max_tokens = json_token_count_furi(json_data);
+    if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        return NULL;
+    }
+    // Create a temporary FuriString from key
+    FuriString *key_str = furi_string_alloc();
+    furi_string_cat_str(key_str, key);
+
+    jsmn_parser parser;
+    jsmn_init_furi(&parser);
+
+    jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens);
+    if (tokens == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens.");
+        furi_string_free(key_str);
+        return NULL;
+    }
+
+    int ret = jsmn_parse_furi(&parser, json_data, tokens, max_tokens);
+    if (ret < 0)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret);
+        free(tokens);
+        furi_string_free(key_str);
+        return NULL;
+    }
+
+    if (ret < 1 || tokens[0].type != JSMN_OBJECT)
+    {
+        FURI_LOG_E("JSMM.H", "Root element is not an object.");
+        free(tokens);
+        furi_string_free(key_str);
+        return NULL;
+    }
+
+    for (int i = 1; i < ret; i++)
+    {
+        if (jsoneq_furi(json_data, &tokens[i], key_str) == 0)
+        {
+            int length = tokens[i + 1].end - tokens[i + 1].start;
+            FuriString *value = furi_string_alloc_set(json_data);
+            furi_string_mid(value, tokens[i + 1].start, length);
+            free(tokens);
+            furi_string_free(key_str);
+            return value;
+        }
+    }
+
+    free(tokens);
+    furi_string_free(key_str);
+    char warning[128];
+    snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key);
+    FURI_LOG_E("JSMM.H", warning);
+    return NULL;
+}
+
+/**
+ * Return the value at a given index in a JSON array for a given char* key.
+ */
+FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data)
+{
+    FuriString *array_str = get_json_value_furi(key, json_data);
+    if (array_str == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to get array for key");
+        return NULL;
+    }
+    uint32_t max_tokens = json_token_count_furi(array_str);
+    if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        furi_string_free(array_str);
+        return NULL;
+    }
+    jsmn_parser parser;
+    jsmn_init_furi(&parser);
+
+    jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens);
+    if (tokens == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens.");
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens);
+    if (ret < 0)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret);
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    if (ret < 1 || tokens[0].type != JSMN_ARRAY)
+    {
+        FURI_LOG_E("JSMM.H", "Value for key is not an array.");
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    if (index >= (uint32_t)tokens[0].size)
+    {
+        // FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size);
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int elem_token = 1;
+    for (uint32_t i = 0; i < index; i++)
+    {
+        elem_token = skip_token(tokens, elem_token, ret);
+        if (elem_token == -1 || elem_token >= ret)
+        {
+            FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i);
+            free(tokens);
+            furi_string_free(array_str);
+            return NULL;
+        }
+    }
+
+    jsmntok_t element = tokens[elem_token];
+    int length = element.end - element.start;
+
+    FuriString *value = furi_string_alloc_set(array_str);
+    furi_string_mid(value, element.start, length);
+
+    free(tokens);
+    furi_string_free(array_str);
+
+    return value;
+}
+
+/**
+ * Extract all object values from a JSON array associated with a given char* key.
+ */
+FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values)
+{
+    *num_values = 0;
+    // Convert key to FuriString and call get_json_value_furi
+    FuriString *array_str = get_json_value_furi(key, json_data);
+    if (array_str == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to get array for key");
+        return NULL;
+    }
+
+    uint32_t max_tokens = json_token_count_furi(array_str);
+    if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        furi_string_free(array_str);
+        return NULL;
+    }
+    jsmn_parser parser;
+    jsmn_init_furi(&parser);
+
+    jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens);
+    if (tokens == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens.");
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens);
+    if (ret < 0)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret);
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    if (tokens[0].type != JSMN_ARRAY)
+    {
+        FURI_LOG_E("JSMM.H", "Value for key is not an array.");
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int array_size = tokens[0].size;
+    FuriString **values = (FuriString **)malloc(array_size * sizeof(FuriString *));
+    if (values == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values.");
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int actual_num_values = 0;
+    int current_token = 1;
+    for (int i = 0; i < array_size; i++)
+    {
+        if (current_token >= ret)
+        {
+            FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array.");
+            break;
+        }
+
+        jsmntok_t element = tokens[current_token];
+
+        int length = element.end - element.start;
+        FuriString *value = furi_string_alloc_set(array_str);
+        furi_string_mid(value, element.start, length);
+
+        values[actual_num_values] = value;
+        actual_num_values++;
+
+        // Skip this element and its descendants
+        current_token = skip_token(tokens, current_token, ret);
+        if (current_token == -1)
+        {
+            FURI_LOG_E("JSMM.H", "Error skipping tokens after element %d.", i);
+            break;
+        }
+    }
+
+    *num_values = actual_num_values;
+    if (actual_num_values < array_size)
+    {
+        FuriString **reduced_values = (FuriString **)realloc(values, actual_num_values * sizeof(FuriString *));
+        if (reduced_values != NULL)
+        {
+            values = reduced_values;
+        }
+    }
+
+    free(tokens);
+    furi_string_free(array_str);
+    return values;
+}
+
+uint32_t json_token_count_furi(const FuriString *json)
+{
+    if (json == NULL)
+    {
+        return JSMN_ERROR_INVAL;
+    }
+
+    jsmn_parser parser;
+    jsmn_init_furi(&parser);
+
+    // Pass NULL for tokens and 0 for num_tokens to get the token count only
+    int ret = jsmn_parse_furi(&parser, json, NULL, 0);
+    return ret; // If ret >= 0, it represents the number of tokens needed.
+}

+ 74 - 0
flip_store/jsmn/jsmn_furi.h

@@ -0,0 +1,74 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010 Serge Zaitsev
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * [License text continues...]
+ */
+
+#ifndef JSMN_FURI_H
+#define JSMN_FURI_H
+
+#include <jsmn/jsmn_h.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#ifdef JSMN_STATIC
+#define JSMN_API static
+#else
+#define JSMN_API extern
+#endif
+
+    JSMN_API void jsmn_init_furi(jsmn_parser *parser);
+    JSMN_API int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js,
+                                 jsmntok_t *tokens, const unsigned int num_tokens);
+
+#ifndef JSMN_HEADER
+/* Implementation in jsmn_furi.c */
+#endif /* JSMN_HEADER */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* JSMN_FURI_H */
+
+#ifndef JB_JSMN_FURI_EDIT
+#define JB_JSMN_FURI_EDIT
+
+// Helper function to create a JSON object
+FuriString *get_json_furi(const FuriString *key, const FuriString *value);
+
+// Updated signatures to accept const char* key
+FuriString *get_json_value_furi(const char *key, const FuriString *json_data);
+FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data);
+FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values);
+
+uint32_t json_token_count_furi(const FuriString *json);
+/* Example usage:
+char *json = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
+FuriString *json_data = char_to_furi_string(json);
+if (!json_data)
+{
+    FURI_LOG_E(TAG, "Failed to allocate FuriString");
+    return -1;
+}
+FuriString *value = get_json_value_furi("key1", json_data, json_token_count_furi(json_data));
+if (value)
+{
+    FURI_LOG_I(TAG, "Value: %s", furi_string_get_cstr(value));
+    furi_string_free(value);
+}
+furi_string_free(json_data);
+*/
+#endif /* JB_JSMN_EDIT */

+ 15 - 0
flip_store/jsmn/jsmn_h.c

@@ -0,0 +1,15 @@
+#include <jsmn/jsmn_h.h>
+FuriString *char_to_furi_string(const char *str)
+{
+    FuriString *furi_str = furi_string_alloc();
+    if (!furi_str)
+    {
+        return NULL;
+    }
+    for (size_t i = 0; i < strlen(str); i++)
+    {
+        furi_string_push_back(furi_str, str[i]);
+    }
+    return furi_str;
+}
+bool jsmn_memory_check(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); }

+ 56 - 0
flip_store/jsmn/jsmn_h.h

@@ -0,0 +1,56 @@
+#pragma once
+#include <furi.h>
+#include <stddef.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+typedef enum
+{
+    JSMN_UNDEFINED = 0,
+    JSMN_OBJECT = 1 << 0,
+    JSMN_ARRAY = 1 << 1,
+    JSMN_STRING = 1 << 2,
+    JSMN_PRIMITIVE = 1 << 3
+} jsmntype_t;
+
+enum jsmnerr
+{
+    JSMN_ERROR_NOMEM = -1,
+    JSMN_ERROR_INVAL = -2,
+    JSMN_ERROR_PART = -3
+};
+
+typedef struct
+{
+    jsmntype_t type;
+    int start;
+    int end;
+    int size;
+#ifdef JSMN_PARENT_LINKS
+    int parent;
+#endif
+} jsmntok_t;
+
+typedef struct
+{
+    unsigned int pos;     /* offset in the JSON string */
+    unsigned int toknext; /* next token to allocate */
+    int toksuper;         /* superior token node, e.g. parent object or array */
+} jsmn_parser;
+
+typedef struct
+{
+    char *key;
+    char *value;
+} JSON;
+
+typedef struct
+{
+    FuriString *key;
+    FuriString *value;
+} FuriJSON;
+
+FuriString *char_to_furi_string(const char *str);
+
+// check memory
+bool jsmn_memory_check(size_t heap_size);

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików