瀏覽代碼

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

Willy-JL 1 年之前
父節點
當前提交
f8db0adf0d

+ 6 - 4
flip_store/README.md

@@ -4,13 +4,13 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long
 ## Features
 - App Catalog
 - Install Apps
-- Delete Apps (coming soon)
+- Delete Apps 
+- Install Developer Board Flashes
 - Install Custom Apps (coming soon)
-- Install Devboard Flashes (coming soon)
 - Install Official Firmware (coming soon)
 
 ## Installation
-1. Flash your WiFi Devboard: https://github.com/jblanked/FlipperHTTP
+1. Flash your WiFi Dveloper Board or Raspberry Pi Pico W: https://github.com/jblanked/FlipperHTTP
 2. Install the app.
 3. Enjoy :D
 
@@ -47,7 +47,9 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long
 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 the catalog results in an "Out of Memory" error.
+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.

+ 71 - 7
flip_store/alloc/flip_store_alloc.c

@@ -56,6 +56,16 @@ FlipStoreApp* flip_store_app_alloc() {
            app)) {
         return NULL;
     }
+    if(!easy_flipper_set_view(
+           &app->view_firmware_download,
+           FlipStoreViewFirmwareDownload,
+           flip_store_view_draw_callback_firmware,
+           NULL,
+           callback_to_firmware_list,
+           &app->view_dispatcher,
+           app)) {
+        return NULL;
+    }
 
     // Widget
     if(!easy_flipper_set_widget(
@@ -97,13 +107,32 @@ FlipStoreApp* flip_store_app_alloc() {
            "No",
            "Yes",
            NULL,
-           dialog_callback,
+           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,
@@ -147,18 +176,34 @@ FlipStoreApp* flip_store_app_alloc() {
 
     // Submenu
     if(!easy_flipper_set_submenu(
-           &app->submenu,
+           &app->submenu_main,
            FlipStoreViewSubmenu,
-           "FlipStore v0.3",
+           "FlipStore v0.6",
            callback_exit_app,
            &app->view_dispatcher)) {
         return NULL;
     }
+    if(!easy_flipper_set_submenu(
+           &app->submenu_options,
+           FlipStoreViewSubmenuOptions,
+           "Browse",
+           callback_to_submenu,
+           &app->view_dispatcher)) {
+        return NULL;
+    }
     if(!easy_flipper_set_submenu(
            &app->submenu_app_list,
            FlipStoreViewAppList,
            "App Catalog",
-           callback_to_submenu,
+           callback_to_submenu_options,
+           &app->view_dispatcher)) {
+        return NULL;
+    }
+    if(!easy_flipper_set_submenu(
+           &app->submenu_firmwares,
+           FlipStoreViewFirmwares,
+           "ESP32 Firmwares",
+           callback_to_submenu_options,
            &app->view_dispatcher)) {
         return NULL;
     }
@@ -250,12 +295,31 @@ FlipStoreApp* flip_store_app_alloc() {
            &app->view_dispatcher)) {
         return NULL;
     }
+    //
     submenu_add_item(
-        app->submenu, "Catalog", FlipStoreSubmenuIndexAppList, callback_submenu_choices, app);
+        app->submenu_main, "Browse", FlipStoreSubmenuIndexOptions, callback_submenu_choices, app);
     submenu_add_item(
-        app->submenu, "About", FlipStoreSubmenuIndexAbout, callback_submenu_choices, app);
+        app->submenu_main, "About", FlipStoreSubmenuIndexAbout, callback_submenu_choices, app);
     submenu_add_item(
-        app->submenu, "Settings", FlipStoreSubmenuIndexSettings, callback_submenu_choices, app);
+        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_app_list,

+ 2 - 0
flip_store/app.c

@@ -1,5 +1,6 @@
 #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) {
@@ -23,6 +24,7 @@ int32_t main_flip_store(void* p) {
 
     // Free the resources used by the Hello World application
     flip_store_app_free(app);
+    flip_catalog_free();
 
     // 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.3",
+    fap_version="0.6",
 )

+ 180 - 217
flip_store/apps/flip_store_apps.c

@@ -9,6 +9,21 @@ bool flip_store_saved_data = false;
 bool flip_store_saved_success = false;
 uint32_t flip_store_category_index = 0;
 
+// define the list of categories
+char* categories[] = {
+    "Bluetooth",
+    "Games",
+    "GPIO",
+    "Infrared",
+    "iButton",
+    "Media",
+    "NFC",
+    "RFID",
+    "Sub-GHz",
+    "Tools",
+    "USB",
+};
+
 FlipStoreAppInfo* flip_catalog_alloc() {
     FlipStoreAppInfo* app_catalog =
         (FlipStoreAppInfo*)malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo));
@@ -32,6 +47,16 @@ FlipStoreAppInfo* flip_catalog_alloc() {
             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;
+        }
     }
     return app_catalog;
 }
@@ -49,38 +74,50 @@ void flip_catalog_free() {
         if(flip_catalog[i].app_build_id) {
             free(flip_catalog[i].app_build_id);
         }
+        if(flip_catalog[i].app_version) {
+            free(flip_catalog[i].app_version);
+        }
+        if(flip_catalog[i].app_description) {
+            free(flip_catalog[i].app_description);
+        }
     }
 }
 
-// Utility function to parse JSON incrementally from a file
 bool flip_store_process_app_list() {
-    // initialize the flip_catalog
+    // Initialize the flip_catalog
     flip_catalog = flip_catalog_alloc();
     if(!flip_catalog) {
         FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog.");
         return false;
     }
 
-    Storage* _storage = NULL;
-    File* _file = NULL;
-    char buffer[BUFFER_SIZE];
-    size_t bytes_read;
+    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) {
+        FURI_LOG_E(TAG, "Failed to get C-string from FuriString.");
+        furi_string_free(feed_data);
+        return false;
+    }
+
+    // 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;
-    bool found_id = false;
-    bool found_build_id = 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; // Initialize object_state
-
-    // Initialize parser state
+    enum ObjectState object_state = OBJECT_EXPECT_KEY;
     enum {
         STATE_SEARCH_APPS_KEY,
         STATE_SEARCH_ARRAY_START,
@@ -88,191 +125,144 @@ bool flip_store_process_app_list() {
         STATE_DONE
     } state = STATE_SEARCH_APPS_KEY;
 
-    // Open storage and file
-    _storage = furi_record_open(RECORD_STORAGE);
-    if(!_storage) {
-        FURI_LOG_E(TAG, "Failed to open storage.");
-        return false;
-    }
-
-    _file = storage_file_alloc(_storage);
-    if(!_file) {
-        FURI_LOG_E(TAG, "Failed to allocate file.");
-        furi_record_close(RECORD_STORAGE);
-        return false;
-    }
+    // Iterate through the data
+    for(size_t i = 0; data_cstr[i] != '\0' && state != STATE_DONE; ++i) {
+        char c = data_cstr[i];
 
-    if(!storage_file_open(_file, fhttp.file_path, FSAM_READ, FSOM_OPEN_EXISTING)) {
-        FURI_LOG_E(TAG, "Failed to open JSON file for reading.");
-        storage_file_free(_file);
-        furi_record_close(RECORD_STORAGE);
-        return false;
-    }
+        if(is_escaped) {
+            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;
+        }
 
-    while((bytes_read = storage_file_read(_file, buffer, BUFFER_SIZE)) > 0 &&
-          state != STATE_DONE) {
-        for(size_t i = 0; i < bytes_read; ++i) {
-            char c = buffer[i];
+        if(c == '\\') {
+            is_escaped = true;
+            continue;
+        }
 
-            if(is_escaped) {
-                is_escaped = false;
-                if(reading_key) {
-                    if(key_index < MAX_KEY_LENGTH - 1) {
-                        current_key[key_index++] = c;
-                    }
-                } else if(reading_value) {
-                    if(value_index < MAX_VALUE_LENGTH - 1) {
-                        current_value[value_index++] = c;
+        if(c == '\"') {
+            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';
                     }
                 }
-                continue;
-            }
-
-            if(c == '\\') {
-                is_escaped = true;
-                continue;
-            }
-
-            if(c == '\"') {
-                in_string = !in_string;
-
-                if(in_string) {
-                    // Start of a string
-                    if(!reading_key && !reading_value) {
-                        if(state == STATE_SEARCH_APPS_KEY) {
-                            reading_key = true;
-                            key_index = 0;
-                            current_key[0] = '\0';
-                        } else if(inside_app_object) {
-                            if(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 {
-                    // End of a string
-                    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;
                         }
-                    } 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;
+                        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;
                             }
 
-                            // After processing value, expect comma or end
-                            object_state = OBJECT_EXPECT_COMMA_OR_END;
-
-                            // Check if both name and id are found
-                            if(found_name && found_id && found_build_id) {
-                                app_count++;
-                                if(app_count >= MAX_APP_COUNT) {
-                                    FURI_LOG_I(TAG, "Reached maximum app count.");
-                                    state = STATE_DONE;
-                                    break;
-                                }
-
-                                // Reset for next app
-                                found_name = false;
-                                found_id = false;
-                                found_build_id = false;
-                            }
+                            found_name = found_id = found_build_id = found_version =
+                                found_description = false;
                         }
-                    }
-                }
-                continue;
-            }
 
-            if(in_string) {
-                if(reading_key) {
-                    if(key_index < MAX_KEY_LENGTH - 1) {
-                        current_key[key_index++] = c;
-                    }
-                } else if(reading_value) {
-                    if(value_index < MAX_VALUE_LENGTH - 1) {
-                        current_value[value_index++] = c;
+                        object_state = OBJECT_EXPECT_COMMA_OR_END;
                     }
                 }
-                continue;
             }
+            continue;
+        }
 
-            // Not inside a string
-
-            if(state == STATE_SEARCH_ARRAY_START) {
-                if(c == '[') {
-                    state = STATE_READ_ARRAY_ELEMENTS;
-                }
-                continue;
+        if(in_string) {
+            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;
+        }
 
-            if(state == STATE_READ_ARRAY_ELEMENTS) {
-                if(c == '{') {
-                    inside_app_object = true;
-                    found_name = false;
-                    found_id = false;
-                    found_build_id = false;
-                    object_state = OBJECT_EXPECT_KEY;
-                } else if(c == '}') {
-                    inside_app_object = false;
-                    object_state = OBJECT_EXPECT_KEY;
-                } else if(c == ']') {
-                    state = STATE_DONE;
-                    break;
-                } else if(c == ':') {
-                    if(inside_app_object && object_state == OBJECT_EXPECT_COLON) {
-                        object_state = OBJECT_EXPECT_VALUE;
-                    }
-                } else if(c == ',') {
-                    if(inside_app_object && object_state == OBJECT_EXPECT_COMMA_OR_END) {
-                        object_state = OBJECT_EXPECT_KEY;
-                    }
-                    // Else, separator between objects or values
-                }
-                // Ignore other characters like whitespace, etc.
-                continue;
+        if(state == STATE_SEARCH_ARRAY_START && c == '[') {
+            state = STATE_READ_ARRAY_ELEMENTS;
+            continue;
+        }
+
+        if(state == STATE_READ_ARRAY_ELEMENTS) {
+            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;
             }
         }
     }
 
     // Clean up
-    storage_file_close(_file);
-    storage_file_free(_file);
-    furi_record_close(RECORD_STORAGE);
-
+    furi_string_free(feed_data);
+    free(data_cstr);
     return app_count > 0;
 }
 
@@ -298,32 +288,6 @@ bool flip_store_get_fap_file(
     return sent_request;
 }
 
-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.");
-    }
-}
 // function to handle the entire installation process "asynchronously"
 bool flip_store_install_app(Canvas* canvas, char* category) {
     // create /apps/FlipStore directory if it doesn't exist
@@ -335,10 +299,10 @@ bool flip_store_install_app(Canvas* canvas, char* category) {
     storage_common_mkdir(storage, directory_path);
 
     // Adjusted to access flip_catalog as an array of structures
-    char installing_text[64];
+    char installation_text[64];
     snprintf(
-        installing_text,
-        sizeof(installing_text),
+        installation_text,
+        sizeof(installation_text),
         "Installing %s",
         flip_catalog[app_selected_index].app_name);
     snprintf(
@@ -347,7 +311,7 @@ bool flip_store_install_app(Canvas* canvas, char* category) {
         STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap",
         category,
         flip_catalog[app_selected_index].app_id);
-    canvas_draw_str(canvas, 0, 10, installing_text);
+    canvas_draw_str(canvas, 0, 10, installation_text);
     canvas_draw_str(canvas, 0, 20, "Sending request..");
     uint8_t target = furi_hal_version_get_hw_target();
     uint16_t api_major, api_minor;
@@ -357,7 +321,6 @@ bool flip_store_install_app(Canvas* canvas, char* category) {
            flip_catalog[app_selected_index].app_build_id, target, api_major, api_minor)) {
         canvas_draw_str(canvas, 0, 30, "Request sent.");
         fhttp.state = RECEIVING;
-        // furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
         canvas_draw_str(canvas, 0, 40, "Receiving...");
     } else {
         FURI_LOG_E(TAG, "Failed to send the request");
@@ -391,31 +354,29 @@ int32_t flip_store_handle_app_list(
         FURI_LOG_E(TAG, "FlipStoreApp is NULL");
         return FlipStoreViewPopup;
     }
-    char url[128];
     snprintf(
         fhttp.file_path,
         sizeof(fhttp.file_path),
-        STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/apps.txt");
+        STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s.json",
+        category);
 
     fhttp.save_received_data = true;
     fhttp.is_bytes_request = false;
-    snprintf(
-        url, sizeof(url), "https://www.flipsocial.net/api/flipper/apps/%s/extended/", category);
-    char* headers = jsmn("Content-Type", "application/json");
+    char url[128];
+    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/flipper/apps/%s/max/", category);
     // async call to the app list with timer
-    if(fhttp.state != INACTIVE && flipper_http_get_request_with_headers(url, headers)) {
+    if(fhttp.state != INACTIVE &&
+       flipper_http_get_request_with_headers(url, "{\"Content-Type\":\"application/json\"}")) {
         furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
-        free(headers);
         fhttp.state = RECEIVING;
     } else {
         FURI_LOG_E(TAG, "Failed to send the request");
-        free(headers);
         fhttp.state = ISSUE;
         return FlipStoreViewPopup;
     }
     while(fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0) {
         // Wait for the feed to be received
-        furi_delay_ms(100);
+        furi_delay_ms(10);
     }
     furi_timer_stop(fhttp.get_timeout_timer);
     if(fhttp.state == ISSUE) {
@@ -441,7 +402,7 @@ int32_t flip_store_handle_app_list(
                         AlignLeft,
                         AlignTop);
                 } else {
-                    popup_set_text(app->popup, fhttp.last_response, 0, 10, AlignLeft, AlignTop);
+                    popup_set_text(app->popup, fhttp.last_response, 0, 50, AlignLeft, AlignTop);
                 }
             } else {
                 popup_set_text(
@@ -454,12 +415,12 @@ int32_t flip_store_handle_app_list(
             }
             return FlipStoreViewPopup;
         } else {
-            popup_set_text(app->popup, "Failed to received data.", 0, 10, AlignLeft, AlignTop);
+            popup_set_text(app->popup, "Failed to received data.", 0, 50, AlignLeft, AlignTop);
             return FlipStoreViewPopup;
         }
     } else {
         // process the app list
-        if(flip_store_process_app_list()) {
+        if(flip_store_process_app_list() && submenu && flip_catalog) {
             submenu_reset(*submenu);
             // add each app name to submenu
             for(int i = 0; i < MAX_APP_COUNT; i++) {
@@ -470,6 +431,8 @@ int32_t flip_store_handle_app_list(
                         FlipStoreSubmenuIndexStartAppList + i,
                         callback_submenu_choices,
                         app);
+                } else {
+                    break;
                 }
             }
             return success_view;

+ 10 - 5
flip_store/apps/flip_store_apps.h

@@ -6,14 +6,21 @@
 #include <callback/flip_store_callback.h>
 
 // Define maximum limits
-#define MAX_APP_NAME_LENGTH 32
-#define MAX_ID_LENGTH       32
-#define MAX_APP_COUNT       100
+#define MAX_APP_NAME_LENGTH        32
+#define MAX_ID_LENGTH              32
+#define MAX_APP_COUNT              100
+#define MAX_APP_DESCRIPTION_LENGTH 100
+#define MAX_APP_VERSION_LENGTH     5
+
+// define the list of categories
+extern char* categories[];
 
 typedef struct {
     char* app_name;
     char* app_id;
     char* app_build_id;
+    char* app_version;
+    char* app_description;
 } FlipStoreAppInfo;
 
 extern FlipStoreAppInfo* flip_catalog;
@@ -45,8 +52,6 @@ bool flip_store_get_fap_file(
     uint16_t api_major,
     uint16_t api_minor);
 
-void flip_store_request_error(Canvas* canvas);
-
 // function to handle the entire installation process "asynchronously"
 bool flip_store_install_app(Canvas* canvas, char* category);
 

二進制
flip_store/assets/01-main-menu.png


二進制
flip_store/assets/05-browse.png


+ 13 - 2
flip_store/assets/CHANGELOG.md

@@ -1,10 +1,21 @@
+## v0.6
+- Updated app layout
+- Added an option to download Developer Board firmware (Black Magic, FlipperHTTP, and Marauder)
+
+## v0.5
+- Added app descriptions and versioning
+
+## v0.4
+- Added an option to delete apps
+- Edits by Willy-JL
+
 ## v0.3
 - Edits by Willy-JL
 - Improved memory allocation
-- Stability Patch
+- Stability patch
 
 ## v0.2
-- Added categories: Users can now navigate through categories, and when FlipStore installs a selected app, it will download directly to the corresponding category's folder in the apps directory.
+- Added categories: Users can now navigate through categories, and when FlipStore installs a selected app, it downloads directly to the corresponding category folder in the apps directory
 - Improved memory allocation to prevent "Out of Memory" warnings
 - Updated installation messages
 

+ 6 - 4
flip_store/assets/README.md

@@ -4,13 +4,13 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long
 ## Features
 - App Catalog
 - Install Apps
-- Delete Apps (coming soon)
+- Delete Apps 
+- Install Developer Board Flashes
 - Install Custom Apps (coming soon)
-- Install Devboard Flashes (coming soon)
 - Install Official Firmware (coming soon)
 
 ## Installation
-1. Flash your WiFi Devboard: https://github.com/jblanked/FlipperHTTP
+1. Flash your WiFi Dveloper Board or Raspberry Pi Pico W: https://github.com/jblanked/FlipperHTTP
 2. Install the app.
 3. Enjoy :D
 
@@ -47,7 +47,9 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long
 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 the catalog results in an "Out of Memory" error.
+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.

+ 344 - 27
flip_store/callback/flip_store_callback.c

@@ -1,5 +1,8 @@
 #include <callback/flip_store_callback.h>
 
+bool flip_store_app_does_exist = false;
+uint32_t selected_firmware_index = 0;
+
 // Callback for drawing the main screen
 void flip_store_view_draw_callback_main(Canvas* canvas, void* model) {
     UNUSED(model);
@@ -43,16 +46,214 @@ void flip_store_view_draw_callback_main(Canvas* canvas, void* model) {
     }
 }
 
+// Function to draw the firmware download screen
+void flip_store_view_draw_callback_firmware(Canvas* canvas, void* model) {
+    UNUSED(model);
+
+    // Check if the HTTP state is inactive
+    if(fhttp.state == INACTIVE) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected.");
+        canvas_draw_str(canvas, 0, 17, "Please connect to the board.");
+        canvas_draw_str(canvas, 0, 32, "If your board is connected,");
+        canvas_draw_str(canvas, 0, 42, "make sure you have flashed");
+        canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the");
+        canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash.");
+        return;
+    }
+
+    // Set font and clear the canvas for the loading state
+    canvas_set_font(canvas, FontSecondary);
+    canvas_clear(canvas);
+    canvas_draw_str(canvas, 0, 10, "Loading...");
+
+    // Handle first firmware file
+    if(!sent_firmware_request) {
+        sent_firmware_request = true;
+        firmware_request_success = flip_store_get_firmware_file(
+            firmwares[selected_firmware_index].links[0],
+            firmwares[selected_firmware_index].name,
+            strrchr(firmwares[selected_firmware_index].links[0], '/') + 1);
+
+        if(!firmware_request_success) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            flip_store_request_error(canvas);
+        }
+        return;
+    } else if(sent_firmware_request && !firmware_download_success) {
+        if(!firmware_request_success || fhttp.state == ISSUE) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            flip_store_request_error(canvas);
+        } else if(fhttp.state == RECEIVING) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "Downloading file 1...");
+            canvas_draw_str(canvas, 0, 60, "Please wait...");
+        } else if(fhttp.state == IDLE) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "Success");
+            canvas_draw_str(canvas, 0, 60, "Downloading the next file now.");
+            firmware_download_success = true;
+        }
+        return;
+    }
+
+    // Handle second firmware file
+    if(firmware_download_success && !sent_firmware_request_2) {
+        sent_firmware_request_2 = true;
+        firmware_request_success_2 = flip_store_get_firmware_file(
+            firmwares[selected_firmware_index].links[1],
+            firmwares[selected_firmware_index].name,
+            strrchr(firmwares[selected_firmware_index].links[1], '/') + 1);
+
+        if(!firmware_request_success_2) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            flip_store_request_error(canvas);
+        }
+        return;
+    } else if(sent_firmware_request_2 && !firmware_download_success_2) {
+        if(!firmware_request_success_2 || fhttp.state == ISSUE) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            flip_store_request_error(canvas);
+        } else if(fhttp.state == RECEIVING) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "Downloading file 2...");
+            canvas_draw_str(canvas, 0, 60, "Please wait...");
+        } else if(fhttp.state == IDLE) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "Success");
+            canvas_draw_str(canvas, 0, 60, "Downloading the next file now.");
+            firmware_download_success_2 = true;
+        }
+        return;
+    }
+
+    // Handle third firmware file
+    if(firmware_download_success && firmware_download_success_2 && !sent_firmware_request_3) {
+        sent_firmware_request_3 = true;
+        firmware_request_success_3 = flip_store_get_firmware_file(
+            firmwares[selected_firmware_index].links[2],
+            firmwares[selected_firmware_index].name,
+            strrchr(firmwares[selected_firmware_index].links[2], '/') + 1);
+
+        if(!firmware_request_success_3) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            flip_store_request_error(canvas);
+        }
+        return;
+    } else if(sent_firmware_request_3 && !firmware_download_success_3) {
+        if(!firmware_request_success_3 || fhttp.state == ISSUE) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            flip_store_request_error(canvas);
+        } else if(fhttp.state == RECEIVING) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "Downloading file 3...");
+            canvas_draw_str(canvas, 0, 60, "Please wait...");
+        } else if(fhttp.state == IDLE) {
+            canvas_set_font(canvas, FontSecondary);
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "Success");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+            firmware_download_success_3 = true;
+        }
+        return;
+    }
+
+    // All files downloaded successfully
+    if(firmware_download_success && firmware_download_success_2 && firmware_download_success_3) {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_clear(canvas);
+        canvas_draw_str(canvas, 0, 10, "Files downloaded successfully.");
+        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+    }
+}
+
+// Function to draw the message on the canvas with word wrapping
+void draw_description(Canvas* canvas, const char* description, int x, int y) {
+    if(description == NULL || strlen(description) == 0) {
+        FURI_LOG_E(TAG, "User message is NULL.");
+        return;
+    }
+    if(!canvas) {
+        FURI_LOG_E(TAG, "Canvas is NULL.");
+        return;
+    }
+
+    size_t msg_length = strlen(description);
+    size_t start = 0;
+    int line_num = 0;
+    char line[MAX_LINE_LENGTH + 1]; // Buffer for the current line (+1 for null terminator)
+
+    while(start < msg_length && line_num < 4) {
+        size_t remaining = msg_length - start;
+        size_t len = (remaining > MAX_LINE_LENGTH) ? MAX_LINE_LENGTH : remaining;
+
+        if(remaining > MAX_LINE_LENGTH) {
+            // Find the last space within the first 'len' characters
+            size_t last_space = len;
+            while(last_space > 0 && description[start + last_space - 1] != ' ') {
+                last_space--;
+            }
+
+            if(last_space > 0) {
+                len = last_space; // Adjust len to the position of the last space
+            }
+        }
+
+        // Copy the substring to 'line' and null-terminate it
+        memcpy(line, description + start, len);
+        line[len] = '\0'; // Ensure the string is null-terminated
+
+        // Draw the string on the canvas
+        // Adjust the y-coordinate based on the line number
+        canvas_draw_str_aligned(canvas, x, y + line_num * 10, AlignLeft, AlignTop, line);
+
+        // Update the start position for the next line
+        start += len;
+
+        // Skip any spaces to avoid leading spaces on the next line
+        while(start < msg_length && description[start] == ' ') {
+            start++;
+        }
+
+        // Increment the line number
+        line_num++;
+    }
+}
+
 void flip_store_view_draw_callback_app_list(Canvas* canvas, void* model) {
     UNUSED(model);
     canvas_clear(canvas);
     canvas_set_font(canvas, FontPrimary);
-    // Adjusted to access flip_catalog as an array of structures
-    canvas_draw_str(canvas, 0, 10, flip_catalog[app_selected_index].app_name);
-    // canvas_draw_icon(canvas, 0, 53, &I_ButtonLeft_4x7); (future implementation)
-    //  canvas_draw_str_aligned(canvas, 7, 54, AlignLeft, AlignTop, "Delete");  (future implementation)
-    canvas_draw_icon(canvas, 0, 53, &I_ButtonBACK_10x8);
-    canvas_draw_str_aligned(canvas, 12, 54, AlignLeft, AlignTop, "Back");
+    char title[30];
+    snprintf(
+        title,
+        30,
+        "%s (v.%s)",
+        flip_catalog[app_selected_index].app_name,
+        flip_catalog[app_selected_index].app_version);
+    canvas_draw_str(canvas, 0, 10, title);
+    canvas_set_font(canvas, FontSecondary);
+    draw_description(canvas, flip_catalog[app_selected_index].app_description, 0, 13);
+    if(flip_store_app_does_exist) {
+        canvas_draw_icon(canvas, 0, 53, &I_ButtonLeft_4x7);
+        canvas_draw_str_aligned(canvas, 7, 54, AlignLeft, AlignTop, "Delete");
+        canvas_draw_icon(canvas, 45, 53, &I_ButtonBACK_10x8);
+        canvas_draw_str_aligned(canvas, 57, 54, AlignLeft, AlignTop, "Back");
+    } else {
+        canvas_draw_icon(canvas, 0, 53, &I_ButtonBACK_10x8);
+        canvas_draw_str_aligned(canvas, 12, 54, AlignLeft, AlignTop, "Back");
+    }
     canvas_draw_icon(canvas, 90, 53, &I_ButtonRight_4x7);
     canvas_draw_str_aligned(canvas, 97, 54, AlignLeft, AlignTop, "Install");
 }
@@ -64,13 +265,11 @@ bool flip_store_input_callback(InputEvent* event, void* context) {
         return false;
     }
     if(event->type == InputTypeShort) {
-        // Future implementation
-        // if (event->key == InputKeyLeft)
-        //{
-        // Left button clicked, delete the app with DialogEx confirmation
-        // view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppDelete);
-        //    return true;
-        //}
+        if(event->key == InputKeyLeft && flip_store_app_does_exist) {
+            // Left button clicked, delete the app
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppDelete);
+            return true;
+        }
         if(event->key == InputKeyRight) {
             // Right button clicked, download the app
             view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewMain);
@@ -170,9 +369,40 @@ uint32_t callback_to_submenu(void* context) {
         return VIEW_NONE;
     }
     UNUSED(context);
+    firmware_free();
     return FlipStoreViewSubmenu;
 }
 
+uint32_t callback_to_submenu_options(void* context) {
+    if(!context) {
+        FURI_LOG_E(TAG, "Context is NULL");
+        return VIEW_NONE;
+    }
+    UNUSED(context);
+    firmware_free();
+    return FlipStoreViewSubmenuOptions;
+}
+
+uint32_t callback_to_firmware_list(void* context) {
+    if(!context) {
+        FURI_LOG_E(TAG, "Context is NULL");
+        return VIEW_NONE;
+    }
+    UNUSED(context);
+    sent_firmware_request = false;
+    sent_firmware_request_2 = false;
+    sent_firmware_request_3 = false;
+    //
+    firmware_request_success = false;
+    firmware_request_success_2 = false;
+    firmware_request_success_3 = false;
+    //
+    firmware_download_success = false;
+    firmware_download_success_2 = false;
+    firmware_download_success_3 = false;
+    return FlipStoreViewFirmwares;
+}
+
 uint32_t callback_to_app_list(void* context) {
     if(!context) {
         FURI_LOG_E(TAG, "Context is NULL");
@@ -183,6 +413,8 @@ uint32_t callback_to_app_list(void* context) {
     flip_store_success = false;
     flip_store_saved_data = false;
     flip_store_saved_success = false;
+    flip_store_app_does_exist = false;
+    sent_firmware_request = false;
     return FlipStoreViewAppList;
 }
 
@@ -205,7 +437,7 @@ void settings_item_selected(void* context, uint32_t index) {
     }
 }
 
-void dialog_callback(DialogExResult result, void* context) {
+void dialog_delete_callback(DialogExResult result, void* context) {
     furi_assert(context);
     FlipStoreApp* app = (FlipStoreApp*)context;
     if(result == DialogExResultLeft) // No
@@ -213,13 +445,35 @@ void dialog_callback(DialogExResult result, void* context) {
         view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList);
     } else if(result == DialogExResultRight) {
         // delete the app then return to the app list
+        if(!delete_app(
+               flip_catalog[app_selected_index].app_id, categories[flip_store_category_index])) {
+            // pop up a message
+            popup_set_header(app->popup, "[ERROR]", 0, 0, AlignLeft, AlignTop);
+            popup_set_text(app->popup, "Issue deleting app.", 0, 50, AlignLeft, AlignTop);
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup);
+            furi_delay_ms(2000); // delay for 2 seconds
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList);
+        } else {
+            // pop up a message
+            popup_set_header(app->popup, "[SUCCESS]", 0, 0, AlignLeft, AlignTop);
+            popup_set_text(app->popup, "App deleted successfully.", 0, 50, AlignLeft, AlignTop);
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup);
+            furi_delay_ms(2000); // delay for 2 seconds
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList);
+        }
+    }
+}
 
-        // pop up a message
-        popup_set_header(app->popup, "Success", 0, 0, AlignLeft, AlignTop);
-        popup_set_text(app->popup, "App deleted successfully.", 0, 60, AlignLeft, AlignTop);
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup);
-        furi_delay_ms(2000); // delay for 2 seconds
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList);
+void dialog_firmware_callback(DialogExResult result, void* context) {
+    furi_assert(context);
+    FlipStoreApp* app = (FlipStoreApp*)context;
+    if(result == DialogExResultLeft) // No
+    {
+        // switch to the firmware list
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwares);
+    } else if(result == DialogExResultRight) {
+        // download the firmware then return to the firmware list
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwareDownload);
     }
 }
 
@@ -232,11 +486,6 @@ void popup_callback(void* context) {
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenu);
 }
 
-/**
- * @brief Navigation callback for exiting the application
- * @param context The context - unused
- * @return next view id (VIEW_NONE to exit the app)
- */
 uint32_t callback_exit_app(void* context) {
     // Exit the application
     if(!context) {
@@ -263,12 +512,39 @@ void callback_submenu_choices(void* context, uint32_t index) {
     case FlipStoreSubmenuIndexSettings:
         view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings);
         break;
+    case FlipStoreSubmenuIndexOptions:
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenuOptions);
+        break;
     case FlipStoreSubmenuIndexAppList:
         flip_store_category_index = 0;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList);
         break;
+    case FlipStoreSubmenuIndexFirmwares:
+        if(!app->submenu_firmwares) {
+            FURI_LOG_E(TAG, "Submenu firmwares is NULL");
+            return;
+        }
+        firmwares = firmware_alloc();
+        if(firmwares == NULL) {
+            FURI_LOG_E(TAG, "Failed to allocate memory for firmwares");
+            return;
+        }
+        submenu_reset(app->submenu_firmwares);
+        submenu_set_header(app->submenu_firmwares, "ESP32 Firmwares");
+        for(int i = 0; i < FIRMWARE_COUNT; i++) {
+            submenu_add_item(
+                app->submenu_firmwares,
+                firmwares[i].name,
+                FlipStoreSubmenuIndexStartFirmwares + i,
+                callback_submenu_choices,
+                app);
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwares);
+        break;
     case FlipStoreSubmenuIndexAppListBluetooth:
         flip_store_category_index = 0;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -276,6 +552,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListGames:
         flip_store_category_index = 1;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -283,6 +560,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListGPIO:
         flip_store_category_index = 2;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -290,6 +568,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListInfrared:
         flip_store_category_index = 3;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -297,6 +576,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListiButton:
         flip_store_category_index = 4;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -304,6 +584,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListMedia:
         flip_store_category_index = 5;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -311,6 +592,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListNFC:
         flip_store_category_index = 6;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -318,6 +600,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListRFID:
         flip_store_category_index = 7;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -325,6 +608,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListSubGHz:
         flip_store_category_index = 8;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -332,6 +616,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListTools:
         flip_store_category_index = 9;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
@@ -339,15 +624,44 @@ void callback_submenu_choices(void* context, uint32_t index) {
         break;
     case FlipStoreSubmenuIndexAppListUSB:
         flip_store_category_index = 10;
+        flip_store_app_does_exist = false;
         view_dispatcher_switch_to_view(
             app->view_dispatcher,
             flip_store_handle_app_list(
                 app, FlipStoreViewAppListUSB, "USB", &app->submenu_app_list_usb));
         break;
     default:
+        // Check if the index is within the firmwares list range
+        if(index >= FlipStoreSubmenuIndexStartFirmwares &&
+           index < FlipStoreSubmenuIndexStartFirmwares + 3) {
+            // Get the firmware index
+            uint32_t firmware_index = index - FlipStoreSubmenuIndexStartFirmwares;
+
+            // Check if the firmware index is valid
+            if((int)firmware_index >= 0 && firmware_index < FIRMWARE_COUNT) {
+                // Get the firmware name
+                selected_firmware_index = firmware_index;
+
+                // Switch to the firmware download view
+                dialog_ex_set_header(
+                    app->dialog_firmware,
+                    firmwares[firmware_index].name,
+                    0,
+                    0,
+                    AlignLeft,
+                    AlignTop);
+                view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwareDialog);
+            } else {
+                FURI_LOG_E(TAG, "Invalid firmware index");
+                popup_set_header(app->popup, "[ERROR]", 0, 0, AlignLeft, AlignTop);
+                popup_set_text(app->popup, "Issue parsing firmwarex", 0, 50, AlignLeft, AlignTop);
+                view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup);
+            }
+        }
         // Check if the index is within the app list range
-        if(index >= FlipStoreSubmenuIndexStartAppList &&
-           index < FlipStoreSubmenuIndexStartAppList + MAX_APP_COUNT) {
+        else if(
+            index >= FlipStoreSubmenuIndexStartAppList &&
+            index < FlipStoreSubmenuIndexStartAppList + MAX_APP_COUNT) {
             // Get the app index
             uint32_t app_index = index - FlipStoreSubmenuIndexStartAppList;
 
@@ -359,6 +673,9 @@ void callback_submenu_choices(void* context, uint32_t index) {
                 // Check if the app name is valid
                 if(app_name != NULL && strlen(app_name) > 0) {
                     app_selected_index = app_index;
+                    flip_store_app_does_exist = app_exists(
+                        flip_catalog[app_selected_index].app_id,
+                        categories[flip_store_category_index]);
                     view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppInfo);
                 } else {
                     FURI_LOG_E(TAG, "Invalid app name");

+ 18 - 6
flip_store/callback/flip_store_callback.h

@@ -7,11 +7,22 @@
 #include <ctype.h>
 #include <stdbool.h>
 #include <apps/flip_store_apps.h>
+#include <firmwares/flip_store_firmwares.h>
 #include <flip_storage/flip_store_storage.h>
 
+#define MAX_LINE_LENGTH 30
+
+extern bool flip_store_app_does_exist;
+extern uint32_t selected_firmware_index;
+
 // Callback for drawing the main screen
 void flip_store_view_draw_callback_main(Canvas* canvas, void* model);
 
+void flip_store_view_draw_callback_firmware(Canvas* canvas, void* model);
+
+// 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);
@@ -22,18 +33,19 @@ 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_app_list(void* context);
 
 void settings_item_selected(void* context, uint32_t index);
 
-void dialog_callback(DialogExResult result, void* context);
+void dialog_delete_callback(DialogExResult result, void* context);
+void dialog_firmware_callback(DialogExResult result, void* context);
 
 void popup_callback(void* context);
-/**
- * @brief Navigation callback for exiting the application
- * @param context The context - unused
- * @return next view id (VIEW_NONE to exit the app)
- */
+
 uint32_t callback_exit_app(void* context);
 void callback_submenu_choices(void* context, uint32_t index);
 

+ 119 - 0
flip_store/firmwares/flip_store_firmwares.c

@@ -0,0 +1,119 @@
+#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;
+
+Firmware* firmware_alloc() {
+    Firmware* fw = (Firmware*)malloc(FIRMWARE_COUNT * sizeof(Firmware));
+    if(!fw) {
+        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";
+
+    // FlipperHTTP
+    fw[1].name = "FlipperHTTP";
+    fw[1].links[0] =
+        "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/flipper_http_bootloader.bin";
+    fw[1].links[1] =
+        "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/flipper_http_firmware_a.bin";
+    fw[1].links[2] =
+        "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/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_0_0_20240626_flipper.bin";
+
+    // 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
+
+    // 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
+
+    // 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);
+    }
+}
+
+bool flip_store_get_firmware_file(char* link, char* name, char* filename) {
+    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) {
+        fhttp.state = RECEIVING;
+        return true;
+    }
+    fhttp.state = ISSUE;
+    return false;
+}

+ 30 - 0
flip_store/firmwares/flip_store_firmwares.h

@@ -0,0 +1,30 @@
+#ifndef FLIP_STORE_FIRMWARES_H
+#define FLIP_STORE_FIRMWARES_H
+
+#include <flip_store.h>
+#include <flip_storage/flip_store_storage.h>
+#include <callback/flip_store_callback.h>
+
+typedef struct {
+    char* name;
+    char* links[FIRMWARE_LINKS];
+} Firmware;
+
+extern Firmware* firmwares;
+Firmware* firmware_alloc();
+void firmware_free();
+
+// 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;
+
+#endif // FLIP_STORE_FIRMWARES_H

+ 17 - 0
flip_store/flip_storage/flip_store_storage.c

@@ -104,6 +104,23 @@ bool delete_app(const char* app_id, const char* app_category) {
     return true;
 }
 
+bool app_exists(const char* app_id, const char* app_category) {
+    // Check if the app exists
+    char directory_path[128];
+    snprintf(
+        directory_path,
+        sizeof(directory_path),
+        STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap",
+        app_category,
+        app_id);
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    bool exists = storage_common_exists(storage, directory_path);
+    furi_record_close(RECORD_STORAGE);
+
+    return exists;
+}
+
 // Function to parse JSON incrementally from a file
 bool parse_json_incrementally(
     const char* file_path,

+ 4 - 3
flip_store/flip_storage/flip_store_storage.h

@@ -7,16 +7,17 @@
 
 #define SETTINGS_PATH    STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/settings.bin"
 #define BUFFER_SIZE      64
-#define MAX_KEY_LENGTH   32
-#define MAX_VALUE_LENGTH 64
+#define MAX_KEY_LENGTH   16
+#define MAX_VALUE_LENGTH 100
 
 void save_settings(const char* ssid, const char* password);
 
 bool load_settings(char* ssid, size_t ssid_size, char* password, size_t password_size);
 
-// future implenetation because we need the app category
 bool delete_app(const char* app_id, const char* app_category);
 
+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,

+ 45 - 17
flip_store/flip_store.c

@@ -1,20 +1,5 @@
 #include <flip_store.h>
 
-// define the list of categories
-char* categories[] = {
-    "Bluetooth",
-    "Games",
-    "GPIO",
-    "Infrared",
-    "iButton",
-    "Media",
-    "NFC",
-    "RFID",
-    "Sub-GHz",
-    "Tools",
-    "USB",
-};
-
 // Function to free the resources used by FlipStoreApp
 void flip_store_app_free(FlipStoreApp* app) {
     if(!app) {
@@ -31,16 +16,28 @@ void flip_store_app_free(FlipStoreApp* app) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppInfo);
         view_free(app->view_app_info);
     }
+    if(app->view_firmware_download) {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewFirmwareDownload);
+        view_free(app->view_firmware_download);
+    }
 
     // Free Submenu(s)
-    if(app->submenu) {
+    if(app->submenu_main) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSubmenu);
-        submenu_free(app->submenu);
+        submenu_free(app->submenu_main);
+    }
+    if(app->submenu_options) {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSubmenuOptions);
+        submenu_free(app->submenu_options);
     }
     if(app->submenu_app_list) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppList);
         submenu_free(app->submenu_app_list);
     }
+    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);
@@ -119,6 +116,10 @@ void flip_store_app_free(FlipStoreApp* app) {
         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();
@@ -132,3 +133,30 @@ void flip_store_app_free(FlipStoreApp* app) {
     // free the 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.");
+    }
+}

+ 27 - 7
flip_store/flip_store.h

@@ -12,17 +12,20 @@
 #include <dialogs/dialogs.h>
 #include <jsmn/jsmn.h>
 #include <flip_store_icons.h>
-#define TAG "FlipStore"
-
-// define the list of categories
-extern char* categories[];
+#define TAG            "FlipStore"
+#define FIRMWARE_COUNT 3
+#define FIRMWARE_LINKS 3
 
 // Define the submenu items for our FlipStore application
 typedef enum {
     FlipStoreSubmenuIndexMain, // Click to start downloading the selected app
     FlipStoreSubmenuIndexAbout,
     FlipStoreSubmenuIndexSettings,
+    //
+    FlipStoreSubmenuIndexOptions, // Click to view the options
+    //
     FlipStoreSubmenuIndexAppList,
+    FlipStoreSubmenuIndexFirmwares,
     //
     FlipStoreSubmenuIndexAppListBluetooth,
     FlipStoreSubmenuIndexAppListGames,
@@ -36,13 +39,18 @@ typedef enum {
     FlipStoreSubmenuIndexAppListTools,
     FlipStoreSubmenuIndexAppListUSB,
     //
-    FlipStoreSubmenuIndexStartAppList
+    FlipStoreSubmenuIndexStartFirmwares,
+    //
+    FlipStoreSubmenuIndexStartAppList = 100,
 } FlipStoreSubmenuIndex;
 
 // Define a single view for our FlipStore application
 typedef enum {
-    FlipStoreViewMain, // The main screen
+    FlipStoreViewMain, // The main screen for downloading apps
+    //
     FlipStoreViewSubmenu, // The submenu
+    FlipStoreViewSubmenuOptions, // The submenu options
+    //
     FlipStoreViewAbout, // The about screen
     FlipStoreViewSettings, // The settings screen
     FlipStoreViewTextInputSSID, // The text input screen for SSID
@@ -51,6 +59,10 @@ typedef enum {
     FlipStoreViewPopup, // The popup screen
     //
     FlipStoreViewAppList, // The app list screen
+    FlipStoreViewFirmwares, // The firmwares screen (submenu)
+    FlipStoreViewFirmwareDialog, // The firmware view (DialogEx) of the selected firmware
+    FlipStoreViewFirmwareDownload, // The firmware download screen
+    //
     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
@@ -73,9 +85,15 @@ typedef struct {
     ViewDispatcher* view_dispatcher; // Switches between our views
     View* view_main; // The main screen for downloading apps
     View* view_app_info; // The app info screen (view) of the selected app
-    Submenu* submenu; // The submenu (main)
     //
+    DialogEx* dialog_firmware; // The dialog for installing a firmware
+    View* view_firmware_download; // The firmware download screen (view) of the selected firmware
+    //
+    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_app_list_bluetooth; // The submenu (app list) for Bluetooth
     Submenu* submenu_app_list_games; // The submenu (app list) for Games
@@ -109,4 +127,6 @@ typedef struct {
 
 void flip_store_app_free(FlipStoreApp* app);
 
+void flip_store_request_error(Canvas* canvas);
+
 #endif // FLIP_STORE_E_H

+ 78 - 35
flip_store/flipper_http/flipper_http.c

@@ -2,6 +2,7 @@
 FlipperHTTP fhttp;
 char rx_line_buffer[RX_LINE_BUFFER_SIZE];
 uint8_t file_buffer[FILE_BUFFER_SIZE];
+size_t file_buffer_len = 0;
 // Function to append received data to file
 // make sure to initialize the file path before calling this function
 bool flipper_http_append_to_file(
@@ -13,6 +14,15 @@ bool flipper_http_append_to_file(
     File* file = storage_file_alloc(storage);
 
     if(start_new_file) {
+        // Delete the file if it already exists
+        if(storage_file_exists(storage, file_path)) {
+            if(!storage_simply_remove_recursive(storage, file_path)) {
+                FURI_LOG_E(HTTP_TAG, "Failed to delete file: %s", file_path);
+                storage_file_free(file);
+                furi_record_close(RECORD_STORAGE);
+                return false;
+            }
+        }
         // 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);
@@ -131,12 +141,13 @@ FuriString* flipper_http_load_from_file(char* file_path) {
 int32_t flipper_http_worker(void* context) {
     UNUSED(context);
     size_t rx_line_pos = 0;
-    static size_t file_buffer_len = 0;
 
     while(1) {
         uint32_t events = furi_thread_flags_wait(
             WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever);
-        if(events & WorkerEvtStop) break;
+        if(events & WorkerEvtStop) {
+            break;
+        }
         if(events & WorkerEvtRxDone) {
             // Continuously read from the stream buffer until it's empty
             while(!furi_stream_buffer_is_empty(fhttp.flipper_http_stream)) {
@@ -156,10 +167,14 @@ int32_t flipper_http_worker(void* context) {
                     // Write to file if buffer is full
                     if(file_buffer_len >= FILE_BUFFER_SIZE) {
                         if(!flipper_http_append_to_file(
-                               file_buffer, file_buffer_len, false, fhttp.file_path)) {
+                               file_buffer,
+                               file_buffer_len,
+                               fhttp.just_started_bytes,
+                               fhttp.file_path)) {
                             FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
                         }
                         file_buffer_len = 0;
+                        fhttp.just_started_bytes = false;
                     }
                 }
 
@@ -182,36 +197,6 @@ int32_t flipper_http_worker(void* context) {
         }
     }
 
-    if(fhttp.save_bytes) {
-        // Write the remaining data to the file
-        if(file_buffer_len > 0) {
-            if(!flipper_http_append_to_file(file_buffer, file_buffer_len, false, fhttp.file_path)) {
-                FURI_LOG_E(HTTP_TAG, "Failed to append remaining data to file");
-            }
-        }
-    }
-
-    // remove [POST/END] and/or [GET/END] from the file
-    if(fhttp.save_bytes) {
-        char* end = NULL;
-        if((end = strstr(fhttp.file_path, "[POST/END]")) != NULL) {
-            *end = '\0';
-        } else if((end = strstr(fhttp.file_path, "[GET/END]")) != NULL) {
-            *end = '\0';
-        }
-    }
-
-    // remove newline from the from the end of the file
-    if(fhttp.save_bytes) {
-        char* end = NULL;
-        if((end = strstr(fhttp.file_path, "\n")) != NULL) {
-            *end = '\0';
-        }
-    }
-
-    // Reset the file buffer length
-    file_buffer_len = 0;
-
     return 0;
 }
 // Timer callback function
@@ -1006,8 +991,35 @@ void flipper_http_rx_callback(const char* line, void* context) {
             fhttp.just_started_get = false;
             fhttp.state = IDLE;
             fhttp.save_bytes = false;
-            fhttp.is_bytes_request = false;
             fhttp.save_received_data = false;
+
+            if(fhttp.is_bytes_request) {
+                // Search for the binary marker `[GET/END]` in the file buffer
+                const char marker[] = "[GET/END]";
+                const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator
+
+                for(size_t i = 0; i <= file_buffer_len - marker_len; i++) {
+                    // Check if the marker is found
+                    if(memcmp(&file_buffer[i], marker, marker_len) == 0) {
+                        // Remove the marker by shifting the remaining data left
+                        size_t remaining_len = file_buffer_len - (i + marker_len);
+                        memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len);
+                        file_buffer_len -= marker_len;
+                        break;
+                    }
+                }
+
+                // If there is data left in the buffer, append it to the file
+                if(file_buffer_len > 0) {
+                    if(!flipper_http_append_to_file(
+                           file_buffer, file_buffer_len, false, fhttp.file_path)) {
+                        FURI_LOG_E(HTTP_TAG, "Failed to append data to file.");
+                    }
+                    file_buffer_len = 0;
+                }
+            }
+
+            fhttp.is_bytes_request = false;
             return;
         }
 
@@ -1041,8 +1053,35 @@ void flipper_http_rx_callback(const char* line, void* context) {
             fhttp.just_started_post = false;
             fhttp.state = IDLE;
             fhttp.save_bytes = false;
-            fhttp.is_bytes_request = false;
             fhttp.save_received_data = false;
+
+            if(fhttp.is_bytes_request) {
+                // Search for the binary marker `[POST/END]` in the file buffer
+                const char marker[] = "[POST/END]";
+                const size_t marker_len = sizeof(marker) - 1; // Exclude null terminator
+
+                for(size_t i = 0; i <= file_buffer_len - marker_len; i++) {
+                    // Check if the marker is found
+                    if(memcmp(&file_buffer[i], marker, marker_len) == 0) {
+                        // Remove the marker by shifting the remaining data left
+                        size_t remaining_len = file_buffer_len - (i + marker_len);
+                        memmove(&file_buffer[i], &file_buffer[i + marker_len], remaining_len);
+                        file_buffer_len -= marker_len;
+                        break;
+                    }
+                }
+
+                // If there is data left in the buffer, append it to the file
+                if(file_buffer_len > 0) {
+                    if(!flipper_http_append_to_file(
+                           file_buffer, file_buffer_len, false, fhttp.file_path)) {
+                        FURI_LOG_E(HTTP_TAG, "Failed to append data to file.");
+                    }
+                    file_buffer_len = 0;
+                }
+            }
+
+            fhttp.is_bytes_request = false;
             return;
         }
 
@@ -1149,6 +1188,8 @@ void flipper_http_rx_callback(const char* line, void* context) {
         fhttp.state = RECEIVING;
         // for GET request, save data only if it's a bytes request
         fhttp.save_bytes = fhttp.is_bytes_request;
+        fhttp.just_started_bytes = true;
+        file_buffer_len = 0;
         return;
     } else if(strstr(line, "[POST/SUCCESS]") != NULL) {
         FURI_LOG_I(HTTP_TAG, "POST request succeeded.");
@@ -1157,6 +1198,8 @@ void flipper_http_rx_callback(const char* line, void* context) {
         fhttp.state = RECEIVING;
         // for POST request, save data only if it's a bytes request
         fhttp.save_bytes = fhttp.is_bytes_request;
+        fhttp.just_started_bytes = true;
+        file_buffer_len = 0;
         return;
     } else if(strstr(line, "[PUT/SUCCESS]") != NULL) {
         FURI_LOG_I(HTTP_TAG, "PUT request succeeded.");

+ 5 - 2
flip_store/flipper_http/flipper_http.h

@@ -16,10 +16,10 @@
 #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_BUF_SIZE            2048 // 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 FILE_BUFFER_SIZE       512 // File buffer size
+#define FILE_BUFFER_SIZE       1024 // File buffer size
 
 // Forward declaration for callback
 typedef void (*FlipperHTTP_Callback)(const char* line, void* context);
@@ -74,12 +74,15 @@ typedef struct {
     bool is_bytes_request; // Flag to indicate if the request is for bytes
     bool save_bytes; // Flag to save the received data to a file
     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;
 
 // fhttp.last_response holds the last received data from the UART