Jelajahi Sumber

FlipStore - v0.7

- Improved memory allocation
- Added updates from Derek Jamison
- Updated Marauder to the latest version
jblanked 1 tahun lalu
induk
melakukan
b2309f8ede

+ 2 - 2
README.md

@@ -35,10 +35,10 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long
 - Download flash firmware (Marauder, Black Magic, FlipperHTTP)
 
 **v0.7**
-- Download custom apps from a GitHub URL
+- UX Improvements
 
 **v0.8**
-- App Icons
+- Download custom apps/assets from a GitHub URL
 
 **1.0**
 - Download Official Firmware/Firmware Updates

+ 9 - 49
alloc/flip_store_alloc.c

@@ -37,17 +37,20 @@ FlipStoreApp *flip_store_app_alloc()
     {
         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_main, FlipStoreViewMain, flip_store_view_draw_callback_main, NULL, callback_to_app_list, &app->view_dispatcher, app))
+    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;
     }
-    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))
+    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;
     }
-    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))
+
+    // 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;
     }
@@ -129,7 +132,7 @@ FlipStoreApp *flip_store_app_alloc()
     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.6", callback_exit_app, &app->view_dispatcher))
+    if (!easy_flipper_set_submenu(&app->submenu_main, FlipStoreViewSubmenu, "FlipStore v0.7", callback_exit_app, &app->view_dispatcher))
     {
         return NULL;
     }
@@ -145,50 +148,7 @@ FlipStoreApp *flip_store_app_alloc()
     {
         return NULL;
     }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_bluetooth, FlipStoreViewAppListBluetooth, "Bluetooth", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_games, FlipStoreViewAppListGames, "Games", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_gpio, FlipStoreViewAppListGPIO, "GPIO", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_infrared, FlipStoreViewAppListInfrared, "Infrared", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_ibutton, FlipStoreViewAppListiButton, "iButton", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_media, FlipStoreViewAppListMedia, "Media", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_nfc, FlipStoreViewAppListNFC, "NFC", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_rfid, FlipStoreViewAppListRFID, "RFID", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_subghz, FlipStoreViewAppListSubGHz, "Sub-GHz", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_tools, FlipStoreViewAppListTools, "Tools", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
-    if (!easy_flipper_set_submenu(&app->submenu_app_list_usb, FlipStoreViewAppListUSB, "USB", callback_to_app_list, &app->view_dispatcher))
-    {
-        return NULL;
-    }
+
     //
     submenu_add_item(app->submenu_main, "Browse", FlipStoreSubmenuIndexOptions, callback_submenu_choices, app);
     submenu_add_item(app->submenu_main, "About", FlipStoreSubmenuIndexAbout, callback_submenu_choices, app);

+ 4 - 4
app.c

@@ -9,8 +9,8 @@ int32_t main_flip_store(void *p)
     UNUSED(p);
 
     // Initialize the Hello World application
-    FlipStoreApp *app = flip_store_app_alloc();
-    if (!app)
+    app_instance = flip_store_app_alloc();
+    if (!app_instance)
     {
         FURI_LOG_E(TAG, "Failed to allocate FlipStoreApp");
         return -1;
@@ -23,10 +23,10 @@ int32_t main_flip_store(void *p)
     }
 
     // Run the view dispatcher
-    view_dispatcher_run(app->view_dispatcher);
+    view_dispatcher_run(app_instance->view_dispatcher);
 
     // Free the resources used by the Hello World application
-    flip_store_app_free(app);
+    flip_store_app_free(app_instance);
     flip_catalog_free();
 
     // Return 0 to indicate success

+ 1 - 1
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.6.1",
+    fap_version="0.7",
 )

+ 5 - 147
apps/flip_store_apps.c

@@ -69,32 +69,9 @@ FlipStoreAppInfo *flip_catalog_alloc()
 }
 void flip_catalog_free()
 {
-    if (!flip_catalog)
+    if (flip_catalog)
     {
-        return;
-    }
-    for (int i = 0; i < MAX_APP_COUNT; i++)
-    {
-        if (flip_catalog[i].app_name)
-        {
-            free(flip_catalog[i].app_name);
-        }
-        if (flip_catalog[i].app_id)
-        {
-            free(flip_catalog[i].app_id);
-        }
-        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);
-        }
+        free(flip_catalog);
     }
 }
 
@@ -322,8 +299,7 @@ bool flip_store_get_fap_file(char *build_id, uint8_t target, uint16_t api_major,
     return sent_request;
 }
 
-// function to handle the entire installation process "asynchronously"
-bool flip_store_install_app(Canvas *canvas, char *category)
+bool flip_store_install_app(char *category)
 {
     // create /apps/FlipStore directory if it doesn't exist
     char directory_path[128];
@@ -333,20 +309,15 @@ bool flip_store_install_app(Canvas *canvas, char *category)
     Storage *storage = furi_record_open(RECORD_STORAGE);
     storage_common_mkdir(storage, directory_path);
 
-    // Adjusted to access flip_catalog as an array of structures
-    char installation_text[64];
-    snprintf(installation_text, sizeof(installation_text), "Installing %s", flip_catalog[app_selected_index].app_name);
     snprintf(fhttp.file_path, sizeof(fhttp.file_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", category, flip_catalog[app_selected_index].app_id);
-    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;
     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))
     {
-        canvas_draw_str(canvas, 0, 30, "Request sent.");
         fhttp.state = RECEIVING;
-        canvas_draw_str(canvas, 0, 40, "Receiving...");
+        return true;
     }
     else
     {
@@ -354,117 +325,4 @@ bool flip_store_install_app(Canvas *canvas, char *category)
         flip_store_success = false;
         return false;
     }
-    while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0)
-    {
-        // Wait for the feed to be received
-        furi_delay_ms(10);
-    }
-    // furi_timer_stop(fhttp.get_timeout_timer);
-    if (fhttp.state == ISSUE)
-    {
-        flip_store_request_error(canvas);
-        flip_store_success = false;
-        return false;
-    }
-    flip_store_success = true;
-    return true;
-}
-
-// process the app list and return view
-int32_t flip_store_handle_app_list(FlipStoreApp *app, int32_t success_view, char *category, Submenu **submenu)
-{
-    // reset the flip_catalog
-    flip_catalog_free();
-
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
-        return FlipStoreViewPopup;
-    }
-    snprintf(
-        fhttp.file_path,
-        sizeof(fhttp.file_path),
-        STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s.json", category);
-
-    fhttp.save_received_data = true;
-    fhttp.is_bytes_request = false;
-    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, "{\"Content-Type\":\"application/json\"}"))
-    {
-        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
-        fhttp.state = RECEIVING;
-    }
-    else
-    {
-        FURI_LOG_E(TAG, "Failed to send the request");
-        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(10);
-    }
-    furi_timer_stop(fhttp.get_timeout_timer);
-    if (fhttp.state == ISSUE)
-    {
-        FURI_LOG_E(TAG, "Failed to receive data");
-        if (fhttp.last_response == NULL)
-        {
-            if (fhttp.last_response != NULL)
-            {
-                if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
-                {
-                    popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop);
-                }
-                else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL)
-                {
-                    popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop);
-                }
-                else
-                {
-                    popup_set_text(app->popup, fhttp.last_response, 0, 50, AlignLeft, AlignTop);
-                }
-            }
-            else
-            {
-                popup_set_text(app->popup, "[ERROR] Unknown Error.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop);
-            }
-            return FlipStoreViewPopup;
-        }
-        else
-        {
-            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() && submenu && flip_catalog)
-        {
-            submenu_reset(*submenu);
-            // add each app name to submenu
-            for (int i = 0; i < MAX_APP_COUNT; i++)
-            {
-                if (strlen(flip_catalog[i].app_name) > 0)
-                {
-                    submenu_add_item(*submenu, flip_catalog[i].app_name, FlipStoreSubmenuIndexStartAppList + i, callback_submenu_choices, app);
-                }
-                else
-                {
-                    break;
-                }
-            }
-            return success_view;
-        }
-        else
-        {
-            FURI_LOG_E(TAG, "Failed to process the app list");
-            popup_set_text(app->popup, "Failed to process the app list", 0, 10, AlignLeft, AlignTop);
-            return FlipStoreViewPopup;
-        }
-    }
 }

+ 1 - 5
apps/flip_store_apps.h

@@ -51,9 +51,5 @@ 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);
 
 // function to handle the entire installation process "asynchronously"
-bool flip_store_install_app(Canvas *canvas, char *category);
-
-// process the app list and return view
-int32_t flip_store_handle_app_list(FlipStoreApp *app, int32_t success_view, char *category, Submenu **submenu);
-
+bool flip_store_install_app(char *category);
 #endif // FLIP_STORE_APPS_H

TEMPAT SAMPAH
assets/01-main-menu.png


+ 5 - 0
assets/CHANGELOG.md

@@ -1,3 +1,8 @@
+## v0.7
+- Improved memory allocation
+- Added updates from Derek Jamison
+- Updated Marauder to the latest version
+
 ## v0.6
 - Updated app layout
 - Added an option to download Developer Board firmware (Black Magic, FlipperHTTP, and Marauder)

+ 3 - 3
assets/README.md

@@ -1,7 +1,7 @@
 # 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
 
-## Features
+## Features 
 - App Catalog
 - Install Apps
 - Delete Apps 
@@ -35,10 +35,10 @@ Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no long
 - Download flash firmware (Marauder, Black Magic, FlipperHTTP)
 
 **v0.7**
-- Download custom apps from a GitHub URL
+- UX Improvements
 
 **v0.8**
-- App Icons
+- Download custom apps from a GitHub URL
 
 **1.0**
 - Download Official Firmware/Firmware Updates

+ 632 - 190
callback/flip_store_callback.c

@@ -1,223 +1,209 @@
 #include <callback/flip_store_callback.h>
 
+// Below added by Derek Jamison
+// FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port.
+#ifdef DEVELOPMENT
+#define FURI_LOG_DEV(tag, format, ...) furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__)
+#define DEV_CRASH() furi_crash()
+#else
+#define FURI_LOG_DEV(tag, format, ...)
+#define DEV_CRASH()
+#endif
+
 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)
+static bool flip_store_dl_app_fetch(DataLoaderModel *model)
 {
     UNUSED(model);
-    canvas_set_font(canvas, FontSecondary);
-
-    if (fhttp.state == INACTIVE)
+    return flip_store_install_app(categories[flip_store_category_index]);
+}
+static char *flip_store_dl_app_parse(DataLoaderModel *model)
+{
+    UNUSED(model);
+    if (fhttp.state != IDLE)
     {
-        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;
+        return NULL;
     }
-
-    if (!flip_store_sent_request)
+    return "App installed successfully.";
+}
+static void flip_store_dl_app_switch_to_view(FlipStoreApp *app)
+{
+    flip_store_generic_switch_to_view(app, flip_catalog[app_selected_index].app_name, flip_store_dl_app_fetch, flip_store_dl_app_parse, 1, callback_to_app_list, FlipStoreViewLoader);
+}
+//
+static bool flip_store_fetch_app_list(DataLoaderModel *model)
+{
+    UNUSED(model);
+    flip_catalog_free();
+    snprintf(
+        fhttp.file_path,
+        sizeof(fhttp.file_path),
+        STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s.json", categories[flip_store_category_index]);
+    fhttp.save_received_data = true;
+    fhttp.is_bytes_request = false;
+    char url[128];
+    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/flipper/apps/%s/max/", categories[flip_store_category_index]);
+    return fhttp.state != INACTIVE && flipper_http_get_request_with_headers(url, "{\"Content-Type\":\"application/json\"}");
+}
+static char *flip_store_parse_app_list(DataLoaderModel *model)
+{
+    UNUSED(model);
+    if (!app_instance)
     {
-        flip_store_sent_request = true;
-
-        if (!flip_store_install_app(canvas, categories[flip_store_category_index]))
-        {
-            canvas_clear(canvas);
-            canvas_draw_str(canvas, 0, 10, "Failed to install app.");
-            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
-        }
+        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
+        return "Failed to fetch app list.";
     }
-    else
+    Submenu **submenu = NULL;
+    uint32_t view_id = 0;
+    switch (flip_store_category_index)
     {
-        if (flip_store_success)
+    case 0:
+        submenu = &app_instance->submenu_app_list_bluetooth;
+        view_id = FlipStoreViewAppListBluetooth;
+        break;
+    case 1:
+        submenu = &app_instance->submenu_app_list_games;
+        view_id = FlipStoreViewAppListGames;
+        break;
+    case 2:
+        submenu = &app_instance->submenu_app_list_gpio;
+        view_id = FlipStoreViewAppListGPIO;
+        break;
+    case 3:
+        submenu = &app_instance->submenu_app_list_infrared;
+        view_id = FlipStoreViewAppListInfrared;
+        break;
+    case 4:
+        submenu = &app_instance->submenu_app_list_ibutton;
+        view_id = FlipStoreViewAppListiButton;
+        break;
+    case 5:
+        submenu = &app_instance->submenu_app_list_media;
+        view_id = FlipStoreViewAppListMedia;
+        break;
+    case 6:
+        submenu = &app_instance->submenu_app_list_nfc;
+        view_id = FlipStoreViewAppListNFC;
+        break;
+    case 7:
+        submenu = &app_instance->submenu_app_list_rfid;
+        view_id = FlipStoreViewAppListRFID;
+        break;
+    case 8:
+        submenu = &app_instance->submenu_app_list_subghz;
+        view_id = FlipStoreViewAppListSubGHz;
+        break;
+    case 9:
+        submenu = &app_instance->submenu_app_list_tools;
+        view_id = FlipStoreViewAppListTools;
+        break;
+    case 10:
+        submenu = &app_instance->submenu_app_list_usb;
+        view_id = FlipStoreViewAppListUSB;
+        break;
+    }
+    if (!submenu)
+    {
+        FURI_LOG_E(TAG, "Submenu is NULL");
+        return "Failed to fetch app list.";
+    }
+    if (!easy_flipper_set_submenu(submenu, view_id, categories[flip_store_category_index], callback_to_app_list, &app_instance->view_dispatcher))
+    {
+        return NULL;
+    }
+
+    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++)
         {
-            if (fhttp.state == RECEIVING)
+            if (strlen(flip_catalog[i].app_name) > 0)
             {
-                canvas_clear(canvas);
-                canvas_draw_str(canvas, 0, 10, "Downloading app...");
-                canvas_draw_str(canvas, 0, 60, "Please wait...");
-                return;
+                submenu_add_item(*submenu, flip_catalog[i].app_name, FlipStoreSubmenuIndexStartAppList + i, callback_submenu_choices, app_instance);
             }
-            else if (fhttp.state == IDLE)
+            else
             {
-                canvas_clear(canvas);
-                canvas_draw_str(canvas, 0, 10, "App installed successfully.");
-                canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+                break;
             }
         }
-        else
-        {
-            canvas_clear(canvas);
-            canvas_draw_str(canvas, 0, 10, "Failed to install app.");
-            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
-        }
+        view_dispatcher_switch_to_view(app_instance->view_dispatcher, view_id);
+        return "Fetched app list successfully.";
     }
-}
-
-// 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)
+    else
     {
-        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;
+        FURI_LOG_E(TAG, "Failed to process the app list");
+        return "Failed to fetch app list.";
     }
-
-    // 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)
+}
+static void flip_store_switch_to_app_list(FlipStoreApp *app)
+{
+    flip_store_generic_switch_to_view(app, categories[flip_store_category_index], flip_store_fetch_app_list, flip_store_parse_app_list, 1, callback_to_submenu, FlipStoreViewLoader);
+}
+//
+static bool flip_store_fetch_firmware(DataLoaderModel *model)
+{
+    if (model->request_index == 0)
     {
-        sent_firmware_request = true;
+        firmware_free();
+        firmwares = firmware_alloc();
+        if (!firmwares)
+        {
+            return false;
+        }
         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;
+        return firmware_request_success;
     }
-    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)
+    else if (model->request_index == 1)
     {
-        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;
+        return firmware_request_success_2;
     }
-
-    // Handle third firmware file
-    if (firmware_download_success && firmware_download_success_2 && !sent_firmware_request_3)
+    else if (model->request_index == 2)
     {
-        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;
+        return firmware_request_success_3;
     }
-    else if (sent_firmware_request_3 && !firmware_download_success_3)
+    return false;
+}
+static char *flip_store_parse_firmware(DataLoaderModel *model)
+{
+    if (model->request_index == 0)
     {
-        if (!firmware_request_success_3 || fhttp.state == ISSUE)
+        if (firmware_request_success)
         {
-            canvas_set_font(canvas, FontSecondary);
-            canvas_clear(canvas);
-            flip_store_request_error(canvas);
+            return "File 1 installed.";
         }
-        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)
+    }
+    else if (model->request_index == 1)
+    {
+        if (firmware_request_success_2)
         {
-            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 "File 2 installed.";
         }
-        return;
     }
-
-    // All files downloaded successfully
-    if (firmware_download_success && firmware_download_success_2 && firmware_download_success_3)
+    else if (model->request_index == 2)
     {
-        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.");
+        if (firmware_request_success_3)
+        {
+            return "Firmware downloaded successfully";
+        }
     }
+    return NULL;
+}
+static void flip_store_switch_to_firmware_list(FlipStoreApp *app)
+{
+    flip_store_generic_switch_to_view(app, firmwares[selected_firmware_index].name, flip_store_fetch_firmware, flip_store_parse_firmware, FIRMWARE_LINKS, callback_to_submenu, FlipStoreViewLoader);
 }
 
 // Function to draw the message on the canvas with word wrapping
@@ -326,7 +312,7 @@ bool flip_store_input_callback(InputEvent *event, void *context)
         if (event->key == InputKeyRight)
         {
             // Right button clicked, download the app
-            view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewMain);
+            flip_store_dl_app_switch_to_view(app);
             return true;
         }
     }
@@ -547,7 +533,7 @@ void dialog_firmware_callback(DialogExResult result, void *context)
     else if (result == DialogExResultRight)
     {
         // download the firmware then return to the firmware list
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewFirmwareDownload);
+        flip_store_switch_to_firmware_list(app);
     }
 }
 
@@ -584,9 +570,6 @@ void callback_submenu_choices(void *context, uint32_t index)
     }
     switch (index)
     {
-    case FlipStoreSubmenuIndexMain:
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewMain);
-        break;
     case FlipStoreSubmenuIndexAbout:
         view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAbout);
         break;
@@ -624,57 +607,57 @@ void callback_submenu_choices(void *context, uint32_t index)
     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(app, FlipStoreViewAppListBluetooth, "Bluetooth", &app->submenu_app_list_bluetooth));
+        flip_store_switch_to_app_list(app);
         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(app, FlipStoreViewAppListGames, "Games", &app->submenu_app_list_games));
+        flip_store_switch_to_app_list(app);
         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(app, FlipStoreViewAppListGPIO, "GPIO", &app->submenu_app_list_gpio));
+        flip_store_switch_to_app_list(app);
         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(app, FlipStoreViewAppListInfrared, "Infrared", &app->submenu_app_list_infrared));
+        flip_store_switch_to_app_list(app);
         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(app, FlipStoreViewAppListiButton, "iButton", &app->submenu_app_list_ibutton));
+        flip_store_switch_to_app_list(app);
         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(app, FlipStoreViewAppListMedia, "Media", &app->submenu_app_list_media));
+        flip_store_switch_to_app_list(app);
         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(app, FlipStoreViewAppListNFC, "NFC", &app->submenu_app_list_nfc));
+        flip_store_switch_to_app_list(app);
         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(app, FlipStoreViewAppListRFID, "RFID", &app->submenu_app_list_rfid));
+        flip_store_switch_to_app_list(app);
         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(app, FlipStoreViewAppListSubGHz, "Sub-GHz", &app->submenu_app_list_subghz));
+        flip_store_switch_to_app_list(app);
         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(app, FlipStoreViewAppListTools, "Tools", &app->submenu_app_list_tools));
+        flip_store_switch_to_app_list(app);
         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));
+        flip_store_switch_to_app_list(app);
         break;
     default:
         // Check if the index is within the firmwares list range
@@ -736,4 +719,463 @@ void callback_submenu_choices(void *context, uint32_t index)
         }
         break;
     }
-}
+}
+
+static void flip_store_widget_set_text(char *message, Widget **widget)
+{
+    if (widget == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_set_widget_text - widget is NULL");
+        DEV_CRASH();
+        return;
+    }
+    if (message == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_set_widget_text - message is NULL");
+        DEV_CRASH();
+        return;
+    }
+    widget_reset(*widget);
+
+    uint32_t message_length = strlen(message); // Length of the message
+    uint32_t i = 0;                            // Index tracker
+    uint32_t formatted_index = 0;              // Tracker for where we are in the formatted message
+    char *formatted_message;                   // Buffer to hold the final formatted message
+
+    // Allocate buffer with double the message length plus one for safety
+    if (!easy_flipper_set_buffer(&formatted_message, message_length * 2 + 1))
+    {
+        return;
+    }
+
+    while (i < message_length)
+    {
+        uint32_t max_line_length = 31;                  // Maximum characters per line
+        uint32_t remaining_length = message_length - i; // Remaining characters
+        uint32_t line_length = (remaining_length < max_line_length) ? remaining_length : max_line_length;
+
+        // Check for newline character within the current segment
+        uint32_t newline_pos = i;
+        bool found_newline = false;
+        for (; newline_pos < i + line_length && newline_pos < message_length; newline_pos++)
+        {
+            if (message[newline_pos] == '\n')
+            {
+                found_newline = true;
+                break;
+            }
+        }
+
+        if (found_newline)
+        {
+            // If newline found, set line_length up to the newline
+            line_length = newline_pos - i;
+        }
+
+        // Temporary buffer to hold the current line
+        char line[32];
+        strncpy(line, message + i, line_length);
+        line[line_length] = '\0';
+
+        // If newline was found, skip it for the next iteration
+        if (found_newline)
+        {
+            i += line_length + 1; // +1 to skip the '\n' character
+        }
+        else
+        {
+            // Check if the line ends in the middle of a word and adjust accordingly
+            if (line_length == max_line_length && message[i + line_length] != '\0' && message[i + line_length] != ' ')
+            {
+                // Find the last space within the current line to avoid breaking a word
+                char *last_space = strrchr(line, ' ');
+                if (last_space != NULL)
+                {
+                    // Adjust the line_length to avoid cutting the word
+                    line_length = last_space - line;
+                    line[line_length] = '\0'; // Null-terminate at the space
+                }
+            }
+
+            // Move the index forward by the determined line_length
+            i += line_length;
+
+            // Skip any spaces at the beginning of the next line
+            while (i < message_length && message[i] == ' ')
+            {
+                i++;
+            }
+        }
+
+        // Manually copy the fixed line into the formatted_message buffer
+        for (uint32_t j = 0; j < line_length; j++)
+        {
+            formatted_message[formatted_index++] = line[j];
+        }
+
+        // Add a newline character for line spacing
+        formatted_message[formatted_index++] = '\n';
+    }
+
+    // Null-terminate the formatted_message
+    formatted_message[formatted_index] = '\0';
+
+    // Add the formatted message to the widget
+    widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message);
+}
+
+void flip_store_loader_draw_callback(Canvas *canvas, void *model)
+{
+    if (!canvas || !model)
+    {
+        FURI_LOG_E(TAG, "flip_store_loader_draw_callback - canvas or model is NULL");
+        return;
+    }
+
+    SerialState http_state = fhttp.state;
+    DataLoaderModel *data_loader_model = (DataLoaderModel *)model;
+    DataState data_state = data_loader_model->data_state;
+    char *title = data_loader_model->title;
+
+    canvas_set_font(canvas, FontSecondary);
+
+    if (http_state == INACTIVE)
+    {
+        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;
+    }
+
+    if (data_state == DataStateError || data_state == DataStateParseError)
+    {
+        flip_store_request_error(canvas);
+        return;
+    }
+
+    canvas_draw_str(canvas, 0, 7, title);
+    canvas_draw_str(canvas, 0, 17, "Loading...");
+
+    if (data_state == DataStateInitial)
+    {
+        return;
+    }
+
+    if (http_state == SENDING)
+    {
+        canvas_draw_str(canvas, 0, 27, "Fetching...");
+        return;
+    }
+
+    if (http_state == RECEIVING || data_state == DataStateRequested)
+    {
+        canvas_draw_str(canvas, 0, 27, "Receiving...");
+        return;
+    }
+
+    if (http_state == IDLE && data_state == DataStateReceived)
+    {
+        canvas_draw_str(canvas, 0, 27, "Processing...");
+        return;
+    }
+
+    if (http_state == IDLE && data_state == DataStateParsed)
+    {
+        canvas_draw_str(canvas, 0, 27, "Processed...");
+        return;
+    }
+}
+
+static void flip_store_loader_process_callback(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_loader_process_callback - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    View *view = app->view_loader;
+
+    DataState current_data_state;
+    with_view_model(view, DataLoaderModel * model, { current_data_state = model->data_state; }, false);
+
+    if (current_data_state == DataStateInitial)
+    {
+        with_view_model(
+            view,
+            DataLoaderModel * model,
+            {
+                model->data_state = DataStateRequested;
+                DataLoaderFetch fetch = model->fetcher;
+                if (fetch == NULL)
+                {
+                    FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned.");
+                    model->data_state = DataStateError;
+                    return;
+                }
+
+                // Clear any previous responses
+                strncpy(fhttp.last_response, "", 1);
+                bool request_status = fetch(model);
+                if (!request_status)
+                {
+                    model->data_state = DataStateError;
+                }
+            },
+            true);
+    }
+    else if (current_data_state == DataStateRequested || current_data_state == DataStateError)
+    {
+        if (fhttp.state == IDLE && fhttp.last_response != NULL)
+        {
+            if (strstr(fhttp.last_response, "[PONG]") != NULL)
+            {
+                FURI_LOG_DEV(TAG, "PONG received.");
+            }
+            else if (strncmp(fhttp.last_response, "[SUCCESS]", 9) == 0)
+            {
+                FURI_LOG_DEV(TAG, "SUCCESS received. %s", fhttp.last_response ? fhttp.last_response : "NULL");
+            }
+            else if (strncmp(fhttp.last_response, "[ERROR]", 9) == 0)
+            {
+                FURI_LOG_DEV(TAG, "ERROR received. %s", fhttp.last_response ? fhttp.last_response : "NULL");
+            }
+            else if (strlen(fhttp.last_response) == 0)
+            {
+                // Still waiting on response
+            }
+            else
+            {
+                with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateReceived; }, true);
+            }
+        }
+        else if (fhttp.state == SENDING || fhttp.state == RECEIVING)
+        {
+            // continue waiting
+        }
+        else if (fhttp.state == INACTIVE)
+        {
+            // inactive. try again
+        }
+        else if (fhttp.state == ISSUE)
+        {
+            with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateError; }, true);
+        }
+        else
+        {
+            FURI_LOG_DEV(TAG, "Unexpected state: %d lastresp: %s", fhttp.state, fhttp.last_response ? fhttp.last_response : "NULL");
+            DEV_CRASH();
+        }
+    }
+    else if (current_data_state == DataStateReceived)
+    {
+        with_view_model(
+            view,
+            DataLoaderModel * model,
+            {
+                char *data_text;
+                if (model->parser == NULL)
+                {
+                    data_text = NULL;
+                    FURI_LOG_DEV(TAG, "Parser is NULL");
+                    DEV_CRASH();
+                }
+                else
+                {
+                    data_text = model->parser(model);
+                }
+                FURI_LOG_DEV(TAG, "Parsed data: %s\r\ntext: %s", fhttp.last_response ? fhttp.last_response : "NULL", data_text ? data_text : "NULL");
+                model->data_text = data_text;
+                if (data_text == NULL)
+                {
+                    model->data_state = DataStateParseError;
+                }
+                else
+                {
+                    model->data_state = DataStateParsed;
+                }
+            },
+            true);
+    }
+    else if (current_data_state == DataStateParsed)
+    {
+        with_view_model(
+            view,
+            DataLoaderModel * model,
+            {
+                if (++model->request_index < model->request_count)
+                {
+                    model->data_state = DataStateInitial;
+                }
+                else
+                {
+                    flip_store_widget_set_text(model->data_text != NULL ? model->data_text : "Empty result", &app->widget_result);
+                    if (model->data_text != NULL)
+                    {
+                        free(model->data_text);
+                        model->data_text = NULL;
+                    }
+                    view_set_previous_callback(widget_get_view(app->widget_result), model->back_callback);
+                    view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewWidgetResult);
+                }
+            },
+            true);
+    }
+}
+
+static void flip_store_loader_timer_callback(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_loader_timer_callback - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, FlipStoreCustomEventProcess);
+}
+
+static void flip_store_loader_on_enter(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_loader_on_enter - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    View *view = app->view_loader;
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            view_set_previous_callback(view, model->back_callback);
+            if (model->timer == NULL)
+            {
+                model->timer = furi_timer_alloc(flip_store_loader_timer_callback, FuriTimerTypePeriodic, app);
+            }
+            furi_timer_start(model->timer, 250);
+        },
+        true);
+}
+
+static void flip_store_loader_on_exit(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_loader_on_exit - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    View *view = app->view_loader;
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            if (model->timer)
+            {
+                furi_timer_stop(model->timer);
+            }
+        },
+        false);
+}
+
+void flip_store_loader_init(View *view)
+{
+    if (view == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_loader_init - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+    view_allocate_model(view, ViewModelTypeLocking, sizeof(DataLoaderModel));
+    view_set_enter_callback(view, flip_store_loader_on_enter);
+    view_set_exit_callback(view, flip_store_loader_on_exit);
+}
+
+void flip_store_loader_free_model(View *view)
+{
+    if (view == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_loader_free_model - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            if (model->timer)
+            {
+                furi_timer_free(model->timer);
+                model->timer = NULL;
+            }
+            if (model->parser_context)
+            {
+                free(model->parser_context);
+                model->parser_context = NULL;
+            }
+        },
+        false);
+}
+
+bool flip_store_custom_event_callback(void *context, uint32_t index)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_custom_event_callback - context is NULL");
+        DEV_CRASH();
+        return false;
+    }
+
+    switch (index)
+    {
+    case FlipStoreCustomEventProcess:
+        flip_store_loader_process_callback(context);
+        return true;
+    default:
+        FURI_LOG_DEV(TAG, "flip_store_custom_event_callback. Unknown index: %ld", index);
+        return false;
+    }
+}
+
+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)
+{
+    if (app == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_generic_switch_to_view - app is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    View *view = app->view_loader;
+    if (view == NULL)
+    {
+        FURI_LOG_E(TAG, "flip_store_generic_switch_to_view - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            model->title = title;
+            model->fetcher = fetcher;
+            model->parser = parser;
+            model->request_index = 0;
+            model->request_count = request_count;
+            model->back_callback = back;
+            model->data_state = DataStateInitial;
+            model->data_text = NULL;
+        },
+        true);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, view_id);
+}

+ 44 - 5
callback/flip_store_callback.h

@@ -15,11 +15,6 @@
 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);
 
@@ -49,4 +44,48 @@ void popup_callback(void *context);
 uint32_t callback_exit_app(void *context);
 void callback_submenu_choices(void *context, uint32_t index);
 
+// Add edits by Derek Jamison
+typedef enum DataState DataState;
+enum DataState
+{
+    DataStateInitial,
+    DataStateRequested,
+    DataStateReceived,
+    DataStateParsed,
+    DataStateParseError,
+    DataStateError,
+};
+
+typedef enum FlipStoreCustomEvent FlipStoreCustomEvent;
+enum FlipStoreCustomEvent
+{
+    FlipStoreCustomEventProcess,
+};
+
+typedef struct DataLoaderModel DataLoaderModel;
+typedef bool (*DataLoaderFetch)(DataLoaderModel *model);
+typedef char *(*DataLoaderParser)(DataLoaderModel *model);
+struct DataLoaderModel
+{
+    char *title;
+    char *data_text;
+    DataState data_state;
+    DataLoaderFetch fetcher;
+    DataLoaderParser parser;
+    void *parser_context;
+    size_t request_index;
+    size_t request_count;
+    ViewNavigationCallback back_callback;
+    FuriTimer *timer;
+};
+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);
+
+void flip_store_loader_draw_callback(Canvas *canvas, void *model);
+
+void flip_store_loader_init(View *view);
+
+void flip_store_loader_free_model(View *view);
+
+bool flip_store_custom_event_callback(void *context, uint32_t index);
+
 #endif // FLIP_STORE_CALLBACK_H

+ 1 - 1
firmwares/flip_store_firmwares.c

@@ -62,7 +62,7 @@ Firmware *firmware_alloc()
     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";
+    fw[2].links[2] = "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/CURRENT/esp32_marauder_v1_1_0_20241128_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

+ 15 - 8
flip_store.c

@@ -1,5 +1,8 @@
 #include <flip_store.h>
 
+void flip_store_loader_free_model(View *view);
+FlipStoreApp *app_instance = NULL;
+
 // Function to free the resources used by FlipStoreApp
 void flip_store_app_free(FlipStoreApp *app)
 {
@@ -9,22 +12,26 @@ void flip_store_app_free(FlipStoreApp *app)
         return;
     }
 
+    // Free Widget(s)
+    if (app->widget_result)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewWidgetResult);
+        widget_free(app->widget_result);
+    }
+
     // Free View(s)
-    if (app->view_main)
+    if (app->view_loader)
     {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewMain);
-        view_free(app->view_main);
+        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);
     }
-    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_main)

+ 23 - 18
flip_store.h

@@ -49,27 +49,25 @@ typedef enum
 // Define a single view for our FlipStore application
 typedef enum
 {
-    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
-    FlipStoreViewTextInputPass, // The text input screen for password
-    //
-    FlipStoreViewPopup, // The popup screen
     //
+    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
-    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
-    //
+                                   //
+    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
@@ -81,13 +79,19 @@ typedef enum
     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
 } FlipStoreView;
 
 // Each screen will have its own view
 typedef struct
 {
+    View *view_loader;
+    Widget *widget_result;
+    //
     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
     //
     DialogEx *dialog_firmware;    // The dialog for installing a firmware
@@ -132,5 +136,6 @@ typedef struct
 void flip_store_app_free(FlipStoreApp *app);
 
 void flip_store_request_error(Canvas *canvas);
+extern FlipStoreApp *app_instance;
 
 #endif // FLIP_STORE_E_H

+ 49 - 0
flipper_http/flipper_http.c

@@ -1466,4 +1466,53 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars
         return false;
     }
     return true;
+}
+
+/**
+ * @brief Perform a task while displaying a loading screen
+ * @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
+ * @param failure_view_id The view ID to switch to on failure
+ * @param view_dispatcher The view dispatcher to use
+ * @return
+ */
+void flipper_http_loading_task(bool (*http_request)(void),
+                               bool (*parse_response)(void),
+                               uint32_t success_view_id,
+                               uint32_t failure_view_id,
+                               ViewDispatcher **view_dispatcher)
+{
+    Loading *loading;
+    int32_t loading_view_id = 987654321; // Random ID
+
+    loading = loading_alloc();
+    if (!loading)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate loading");
+        view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id);
+
+        return;
+    }
+
+    view_dispatcher_add_view(*view_dispatcher, loading_view_id, loading_get_view(loading));
+
+    // Switch to the loading view
+    view_dispatcher_switch_to_view(*view_dispatcher, loading_view_id);
+
+    // Make the request
+    if (!flipper_http_process_response_async(http_request, parse_response))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to make request");
+        view_dispatcher_switch_to_view(*view_dispatcher, failure_view_id);
+        view_dispatcher_remove_view(*view_dispatcher, loading_view_id);
+        loading_free(loading);
+
+        return;
+    }
+
+    // Switch to the success view
+    view_dispatcher_switch_to_view(*view_dispatcher, success_view_id);
+    view_dispatcher_remove_view(*view_dispatcher, loading_view_id);
+    loading_free(loading);
 }

+ 21 - 2
flipper_http/flipper_http.h

@@ -2,6 +2,10 @@
 #ifndef FLIPPER_HTTP_H
 #define FLIPPER_HTTP_H
 
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/loading.h>
 #include <furi.h>
 #include <furi_hal.h>
 #include <furi_hal_gpio.h>
@@ -15,10 +19,10 @@
 #define UART_CH (FuriHalSerialIdUsart)    // UART channel
 #define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds
 #define BAUDRATE (115200)                 // UART baudrate
-#define RX_BUF_SIZE 2048                  // UART RX buffer size
+#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 FILE_BUFFER_SIZE 1024             // File buffer size
+#define FILE_BUFFER_SIZE 512              // File buffer size
 
 // Forward declaration for callback
 typedef void (*FlipperHTTP_Callback)(const char *line, void *context);
@@ -363,4 +367,19 @@ char *trim(const char *str);
  */
 bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void));
 
+/**
+ * @brief Perform a task while displaying a loading screen
+ * @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
+ * @param failure_view_id The view ID to switch to on failure
+ * @param view_dispatcher The view dispatcher to use
+ * @return
+ */
+void flipper_http_loading_task(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