Sfoglia il codice sorgente

Add web_crawler from https://github.com/jblanked/WebCrawler-FlipperZero

git-subtree-dir: web_crawler
git-subtree-mainline: 826fdbda71fe9c0689858de07c71d99676d437c2
git-subtree-split: e63e2503e946a050abf3e8d1efc69fb74880e465
Willy-JL 1 anno fa
parent
commit
271d02af4f

BIN
web_crawler/.DS_Store


+ 2 - 0
web_crawler/.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 1 - 0
web_crawler/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/jblanked/WebCrawler-FlipperZero main /

+ 46 - 0
web_crawler/CHANGELOG.md

@@ -0,0 +1,46 @@
+## 0.7
+- Improved memory allocation
+
+## 0.6
+- Added a DOWNLOAD method, which downloads the file at the specified path. This is best used for downloading images, binary files, and other non-text files.
+- Increased buffer size to handle larger responses.
+- Fixed the variable item lists to display as intended.
+
+## 0.5 (Cleanup)
+- Refactored using the easy flipper library
+- New python library for the FlipperHTTP flash
+- Removed unnecessary GPIO initialization
+- Removed unnecessary logging
+
+## 0.4 (New Keyboard and HTTP Methods)
+- Updated the text input to match the text input from the UART Terminal app (big thanks to xMasterx)
+- Added POST, DELETE, and PUT requests (with payloads)
+- Added headers
+- Added more error handling
+
+## 0.3 (New Features)
+- Updated the progress messages displayed after sending a HTTP request.
+- Renamed the "Config" main submenu option to "Settings."
+- Added a submenu (WiFi, File, Request) that appears when users click the "Settings" option.
+
+Clicking **WiFi** displays a variable item list for SSID and Password. If you click:
+- **SSID**: A text input screen will appear with the current saved SSID pre-filled. You can change the SSID by editing the text and clicking "Save."
+- **Password**: A text input screen will appear with the current saved password pre-filled. You can change the password by editing the text and clicking "Save."
+
+Clicking **File** displays a variable item list with options to Read File, Set File Type, Rename File, and Delete File. If you click:
+- **Read File**: The contents of the saved file will be displayed.
+- **Set File Type**: A text input screen will appear with the current saved file type pre-filled. You can change the file type by editing the text and clicking "Save."
+- **Rename File**: A text input screen will appear with the current saved file name pre-filled. You can change the file name by editing the text and clicking "Save."
+- **Delete File**: The saved file will be deleted.
+
+Clicking **Request** displays a variable item list with a single text input option called **Path**. If you click **Path**, you can change the URL path that the GET request will be sent to.
+
+## 0.2 (Stability Patch)
+- Changed serial functions from synchronous to asynchronous.
+- Added error handling for HTTP requests and WiFi Dev Board connections.
+- Updated the WiFi Dev Board firmware (FlipperHTTP) to blink the green LED three times when the board is first connected, remain solid green when the board receives a serial command, and stay solid green while processing.
+- Added auto-connect and auto-disconnect for WiFi when a user enters or exits the app.
+- Added an option to view saved data.
+
+## 0.1
+- Initial release

+ 21 - 0
web_crawler/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 jblanked
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 84 - 0
web_crawler/README.md

@@ -0,0 +1,84 @@
+## Overview
+
+**Web Crawler** is a custom application designed for the Flipper Zero device, allowing users to configure and manage HTTP requests directly from their Flipper Zero.
+
+## Requirements
+- WiFi Dev Board or Raspberry Pi Pico W for Flipper Zero with FlipperHTTP Flash: https://github.com/jblanked/FlipperHTTP
+- WiFi Access Point
+
+
+## Installation
+- Download from the Official Flipper App Store: https://lab.flipper.net/apps/web_crawler
+- Video tutorial: https://www.youtube.com/watch?v=lkui2Smckq4
+
+## Features
+- **Configurable Request**: Specify the URL of the website you want to send a HTTP request to.
+- **Wi-Fi Configuration**: Enter your Wi-Fi SSID and password to enable network communication.
+- **File Management**: Automatically saves and manages received data on the device's storage, allowing users to view, rename, and delete the received data at any time.
+
+## Usage
+1. **Connection**: After installing the app, turn off your Flipper, connect the WiFi Dev Board, then turn your Flipper back on.
+
+2. **Launch the Web Crawler App**: Navigate to the Apps menu on your Flipper Zero, select GPIO, then scroll down and select **Web Crawler**.
+
+3. **Main Menu**: Upon launching, you'll see a submenu containing the following options:
+   - **Run**: Initiate the HTTP request.
+   - **About**: View information about the Web Crawler app.
+   - **Settings**: Set up parameters or perform file operations.
+
+4. **Settings**: Select **Settings** and navigate to WiFi Settings. Use the Flipper Zero's navigation buttons to input and confirm your settings. Do the same for the Request settings. Once configured, these settings will be saved and used for subsequent HTTP request operations.
+
+   For testing purposes:
+   - https://httpbin.org/get Returns GET data.
+   - https://httpbin.org/post Returns POST data.
+   - https://httpbin.org/put Returns PUT data.
+   - https://httpbin.org/delete Returns DELETE data.
+
+5. **Running the Request**: Select **Run** from the main submenu to start the HTTP request process. The app will:
+   - **Send Request**: Transmit the HTTP request via serial to the WiFi Dev Board.
+   - **Receive Data**: Listen for incoming data.
+   - **Store Data**: Save the received data to the device's storage for later retrieval.
+   - **Log**: Display detailed analysis of the operation status on the screen.
+
+6. **Accessing Received Data**: After the HTTP request operation completes, you can access the received data by either:
+   - Navigating to File Settings and selecting Read File (preferred method)
+   - Connecting to Flipper and opening the SD/apps_data/web_crawler_app/ storage directory to access the received_data.txt file (or the file name/type customized in the settings).
+
+## Setting Up Parameters
+1. **Path (URL)**
+   - Enter the complete URL of the website you intend to crawl (e.g., https://www.example.com/).
+
+2. **HTTP Method**
+   - Choose between GET, POST, DELETE, and PUT.
+
+3. **Headers**
+   - Add your required headers to be used in your HTTP requests
+
+4. **Payload**
+   - Type in the JSON content to be sent with your POST or PUT requests.
+
+5. **SSID (WiFi Network)**
+   - Provide the name of your WiFi network to enable the Flipper Zero to communicate over the network.
+
+6. **Password (WiFi Network)**
+   - Input the corresponding password for your WiFi network.
+
+7. **Set File Type**
+   - Enter your desired file extension. After saving, the app will rename your file, applying the new extension.
+
+8. **Rename File**
+   - Provide your desired file name. After saving, the app will rename your file with the new name.
+
+
+## Saving Settings
+After entering the desired configuration parameters, the app automatically saves these settings for use during the HTTP request process. You can update these settings at any time by navigating back to the **Settings** menu.
+
+## Logging and Debugging
+The Web Crawler app uses logging to help identify issues:
+
+- **Info Logs**: Provide general information about the app's operations (e.g., UART initialization, sending settings).
+- **Error Logs**: Indicate problems encountered during execution (e.g., failed to open settings file).
+
+Connect your Flipper Zero to a computer and use a serial terminal to view these logs for detailed troubleshooting.
+
+*Happy Crawling! 🕷️* 

+ 37 - 0
web_crawler/app.c

@@ -0,0 +1,37 @@
+#include <uart_text_input.h>
+#include <web_crawler_e.h>
+#include <flipper_http.h>
+#include <web_crawler_storage.h>
+#include <web_crawler_free.h>
+#include <web_crawler_callback.h>
+#include <web_crawler_i.h>
+/**
+ * @brief      Entry point for the WebCrawler application.
+ * @param      p  Input parameter - unused
+ * @return     0 to indicate success, -1 on failure
+ */
+int32_t web_crawler_app(void *p)
+{
+    UNUSED(p);
+
+    WebCrawlerApp *app = web_crawler_app_alloc();
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate WebCrawlerApp");
+        return -1;
+    }
+
+    if (!flipper_http_ping())
+    {
+        FURI_LOG_E(TAG, "Failed to ping the device");
+        return -1;
+    }
+
+    // Run the application
+    view_dispatcher_run(app->view_dispatcher);
+
+    // Free resources after the application loop ends
+    web_crawler_app_free(app);
+
+    return 0;
+}

BIN
web_crawler/app.png


+ 14 - 0
web_crawler/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="web_crawler",
+    name="Web Crawler",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="web_crawler_app",
+    stack_size=4 * 1024,
+    fap_icon="app.png",
+    fap_category="GPIO",
+    fap_icon_assets="assets",
+    fap_description="Use Wi-Fi to access the internet and scrape data from the web.",
+    fap_author="JBlanked",
+    fap_weburl="https://github.com/jblanked/WebCrawler-FlipperZero",
+    fap_version = "0.7",
+)

BIN
web_crawler/assets/.DS_Store


BIN
web_crawler/assets/01-request.png


BIN
web_crawler/assets/02-main.png


BIN
web_crawler/assets/03-path.png


BIN
web_crawler/assets/FlipperHTTP/.DS_Store


+ 4 - 0
web_crawler/assets/FlipperHTTP/README.md

@@ -0,0 +1,4 @@
+# FlipperHTTP
+HTTP library for Flipper Zero. Compatible with Wifi Dev Board for Flipper Zero (ESP32S2 Dev Module) and the Raspberry Pi Pico W. 
+
+This library has been moved to: https://www.github.com/jblanked/FlipperHTTP

BIN
web_crawler/assets/KeyBackspaceSelected_16x9.png


BIN
web_crawler/assets/KeyBackspace_16x9.png


BIN
web_crawler/assets/KeySaveSelected_24x11.png


BIN
web_crawler/assets/KeySave_24x11.png


BIN
web_crawler/assets/WarningDolphin_45x42.png


+ 593 - 0
web_crawler/easy_flipper.h

@@ -0,0 +1,593 @@
+#ifndef EASY_FLIPPER_H
+#define EASY_FLIPPER_H
+
+#include <malloc.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/modules/submenu.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/menu.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/dialog_ex.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/loading.h>
+#include <uart_text_input.h>
+
+#define EASY_TAG "EasyFlipper"
+
+/**
+ * @brief Navigation callback for exiting the application
+ * @param context The context - unused
+ * @return next view id (VIEW_NONE to exit the app)
+ */
+uint32_t easy_flipper_callback_exit_app(void *context)
+{
+    // Exit the application
+    if (!context)
+    {
+        FURI_LOG_E(EASY_TAG, "Context is NULL");
+        return VIEW_NONE;
+    }
+    UNUSED(context);
+    return VIEW_NONE; // Return VIEW_NONE to exit the app
+}
+
+/**
+ * @brief Initialize a buffer
+ * @param buffer The buffer to initialize
+ * @param buffer_size The size of the buffer
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_buffer(char **buffer, uint32_t buffer_size)
+{
+    if (!buffer)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_buffer");
+        return false;
+    }
+    *buffer = (char *)malloc(buffer_size);
+    if (!*buffer)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate buffer");
+        return false;
+    }
+    *buffer[0] = '\0';
+    return true;
+}
+
+/**
+ * @brief Initialize a View object
+ * @param view The View object to initialize
+ * @param view_id The ID/Index of the view
+ * @param draw_callback The draw callback function (set to NULL if not needed)
+ * @param input_callback The input callback function (set to NULL if not needed)
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_view(
+    View **view,
+    int32_t view_id,
+    void draw_callback(Canvas *, void *),
+    bool input_callback(InputEvent *, void *),
+    uint32_t (*previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!view || !view_dispatcher)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_view");
+        return false;
+    }
+    *view = view_alloc();
+    if (!*view)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate View");
+        return false;
+    }
+    if (draw_callback)
+    {
+        view_set_draw_callback(*view, draw_callback);
+    }
+    if (input_callback)
+    {
+        view_set_input_callback(*view, input_callback);
+    }
+    if (context)
+    {
+        view_set_context(*view, context);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(*view, previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, *view);
+    return true;
+}
+
+/**
+ * @brief Initialize a ViewDispatcher object
+ * @param view_dispatcher The ViewDispatcher object to initialize
+ * @param gui The GUI object
+ * @param context The context to pass to the event callback
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_view_dispatcher(ViewDispatcher **view_dispatcher, Gui *gui, void *context)
+{
+    if (!view_dispatcher)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_view_dispatcher");
+        return false;
+    }
+    *view_dispatcher = view_dispatcher_alloc();
+    if (!*view_dispatcher)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate ViewDispatcher");
+        return false;
+    }
+    view_dispatcher_attach_to_gui(*view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+    if (context)
+    {
+        view_dispatcher_set_event_callback_context(*view_dispatcher, context);
+    }
+    return true;
+}
+
+/**
+ * @brief Initialize a Submenu object
+ * @note This does not set the items in the submenu
+ * @param submenu The Submenu object to initialize
+ * @param view_id The ID/Index of the view
+ * @param title The title/header of the submenu
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_submenu(
+    Submenu **submenu,
+    int32_t view_id,
+    char *title,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!submenu)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_submenu");
+        return false;
+    }
+    *submenu = submenu_alloc();
+    if (!*submenu)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Submenu");
+        return false;
+    }
+    if (title)
+    {
+        submenu_set_header(*submenu, title);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(submenu_get_view(*submenu), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, submenu_get_view(*submenu));
+    return true;
+}
+/**
+ * @brief Initialize a Menu object
+ * @note This does not set the items in the menu
+ * @param menu The Menu object to initialize
+ * @param view_id The ID/Index of the view
+ * @param item_callback The item callback function
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_menu(
+    Menu **menu,
+    int32_t view_id,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!menu)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_menu");
+        return false;
+    }
+    *menu = menu_alloc();
+    if (!*menu)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Menu");
+        return false;
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(menu_get_view(*menu), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, menu_get_view(*menu));
+    return true;
+}
+
+/**
+ * @brief Initialize a Widget object
+ * @param widget The Widget object to initialize
+ * @param view_id The ID/Index of the view
+ * @param text The text to display in the widget
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_widget(
+    Widget **widget,
+    int32_t view_id,
+    char *text,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!widget)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_widget");
+        return false;
+    }
+    *widget = widget_alloc();
+    if (!*widget)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Widget");
+        return false;
+    }
+    if (text)
+    {
+        widget_add_text_scroll_element(*widget, 0, 0, 128, 64, text);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(widget_get_view(*widget), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, widget_get_view(*widget));
+    return true;
+}
+
+/**
+ * @brief Initialize a VariableItemList object
+ * @note This does not set the items in the VariableItemList
+ * @param variable_item_list The VariableItemList object to initialize
+ * @param view_id The ID/Index of the view
+ * @param enter_callback The enter callback function (can be set to NULL)
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @param context The context to pass to the enter callback (usually the app)
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_variable_item_list(
+    VariableItemList **variable_item_list,
+    int32_t view_id,
+    void (*enter_callback)(void *, uint32_t),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!variable_item_list)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_variable_item_list");
+        return false;
+    }
+    *variable_item_list = variable_item_list_alloc();
+    if (!*variable_item_list)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate VariableItemList");
+        return false;
+    }
+    if (enter_callback)
+    {
+        variable_item_list_set_enter_callback(*variable_item_list, enter_callback, context);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(variable_item_list_get_view(*variable_item_list), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, variable_item_list_get_view(*variable_item_list));
+    return true;
+}
+
+/**
+ * @brief Initialize a TextInput object
+ * @param text_input The TextInput object to initialize
+ * @param view_id The ID/Index of the view
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_text_input(
+    TextInput **text_input,
+    int32_t view_id,
+    char *header_text,
+    char *text_input_temp_buffer,
+    uint32_t text_input_buffer_size,
+    void (*result_callback)(void *),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!text_input)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_text_input");
+        return false;
+    }
+    *text_input = text_input_alloc();
+    if (!*text_input)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate TextInput");
+        return false;
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(text_input_get_view(*text_input), previous_callback);
+    }
+    if (header_text)
+    {
+        text_input_set_header_text(*text_input, header_text);
+    }
+    if (text_input_temp_buffer && text_input_buffer_size && result_callback)
+    {
+        text_input_set_result_callback(*text_input, result_callback, context, text_input_temp_buffer, text_input_buffer_size, false);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, text_input_get_view(*text_input));
+    return true;
+}
+
+/**
+ * @brief Initialize a UART_TextInput object
+ * @param uart_text_input The UART_TextInput object to initialize
+ * @param view_id The ID/Index of the view
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_uart_text_input(
+    UART_TextInput **uart_text_input,
+    int32_t view_id,
+    char *header_text,
+    char *uart_text_input_temp_buffer,
+    uint32_t uart_text_input_buffer_size,
+    void (*result_callback)(void *),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!uart_text_input)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_uart_text_input");
+        return false;
+    }
+    *uart_text_input = uart_text_input_alloc();
+    if (!*uart_text_input)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate UART_TextInput");
+        return false;
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(uart_text_input_get_view(*uart_text_input), previous_callback);
+    }
+    if (header_text)
+    {
+        uart_text_input_set_header_text(*uart_text_input, header_text);
+    }
+    if (uart_text_input_temp_buffer && uart_text_input_buffer_size && result_callback)
+    {
+        uart_text_input_set_result_callback(*uart_text_input, result_callback, context, uart_text_input_temp_buffer, uart_text_input_buffer_size, false);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, uart_text_input_get_view(*uart_text_input));
+    return true;
+}
+
+/**
+ * @brief Initialize a DialogEx object
+ * @param dialog_ex The DialogEx object to initialize
+ * @param view_id The ID/Index of the view
+ * @param header The header of the dialog
+ * @param header_x The x coordinate of the header
+ * @param header_y The y coordinate of the header
+ * @param text The text of the dialog
+ * @param text_x The x coordinate of the dialog
+ * @param text_y The y coordinate of the dialog
+ * @param left_button_text The text of the left button
+ * @param right_button_text The text of the right button
+ * @param center_button_text The text of the center button
+ * @param result_callback The result callback function
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @param context The context to pass to the result callback
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_dialog_ex(
+    DialogEx **dialog_ex,
+    int32_t view_id,
+    char *header,
+    uint16_t header_x,
+    uint16_t header_y,
+    char *text,
+    uint16_t text_x,
+    uint16_t text_y,
+    char *left_button_text,
+    char *right_button_text,
+    char *center_button_text,
+    void (*result_callback)(DialogExResult, void *),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!dialog_ex)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_dialog_ex");
+        return false;
+    }
+    *dialog_ex = dialog_ex_alloc();
+    if (!*dialog_ex)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate DialogEx");
+        return false;
+    }
+    if (header)
+    {
+        dialog_ex_set_header(*dialog_ex, header, header_x, header_y, AlignLeft, AlignTop);
+    }
+    if (text)
+    {
+        dialog_ex_set_text(*dialog_ex, text, text_x, text_y, AlignLeft, AlignTop);
+    }
+    if (left_button_text)
+    {
+        dialog_ex_set_left_button_text(*dialog_ex, left_button_text);
+    }
+    if (right_button_text)
+    {
+        dialog_ex_set_right_button_text(*dialog_ex, right_button_text);
+    }
+    if (center_button_text)
+    {
+        dialog_ex_set_center_button_text(*dialog_ex, center_button_text);
+    }
+    if (result_callback)
+    {
+        dialog_ex_set_result_callback(*dialog_ex, result_callback);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(dialog_ex_get_view(*dialog_ex), previous_callback);
+    }
+    if (context)
+    {
+        dialog_ex_set_context(*dialog_ex, context);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, dialog_ex_get_view(*dialog_ex));
+    return true;
+}
+
+/**
+ * @brief Initialize a Popup object
+ * @param popup The Popup object to initialize
+ * @param view_id The ID/Index of the view
+ * @param header The header of the dialog
+ * @param header_x The x coordinate of the header
+ * @param header_y The y coordinate of the header
+ * @param text The text of the dialog
+ * @param text_x The x coordinate of the dialog
+ * @param text_y The y coordinate of the dialog
+ * @param result_callback The result callback function
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @param context The context to pass to the result callback
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_popup(
+    Popup **popup,
+    int32_t view_id,
+    char *header,
+    uint16_t header_x,
+    uint16_t header_y,
+    char *text,
+    uint16_t text_x,
+    uint16_t text_y,
+    void (*result_callback)(void *),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!popup)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_popup");
+        return false;
+    }
+    *popup = popup_alloc();
+    if (!*popup)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Popup");
+        return false;
+    }
+    if (header)
+    {
+        popup_set_header(*popup, header, header_x, header_y, AlignLeft, AlignTop);
+    }
+    if (text)
+    {
+        popup_set_text(*popup, text, text_x, text_y, AlignLeft, AlignTop);
+    }
+    if (result_callback)
+    {
+        popup_set_callback(*popup, result_callback);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(popup_get_view(*popup), previous_callback);
+    }
+    if (context)
+    {
+        popup_set_context(*popup, context);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, popup_get_view(*popup));
+    return true;
+}
+
+/**
+ * @brief Initialize a Loading object
+ * @param loading The Loading object to initialize
+ * @param view_id The ID/Index of the view
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_loading(
+    Loading **loading,
+    int32_t view_id,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!loading)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_loading");
+        return false;
+    }
+    *loading = loading_alloc();
+    if (!*loading)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Loading");
+        return false;
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(loading_get_view(*loading), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, loading_get_view(*loading));
+    return true;
+}
+
+/**
+ * @brief Set a char butter to a FuriString
+ * @param furi_string The FuriString object
+ * @param buffer The buffer to copy the string to
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer)
+{
+    if (!furi_string)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_buffer_to_furi_string");
+        return false;
+    }
+    *furi_string = furi_string_alloc();
+    if (!furi_string)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate FuriString");
+        return false;
+    }
+    furi_string_set_str(*furi_string, buffer);
+    return true;
+}
+
+#endif // EASY_FLIPPER_H

+ 1562 - 0
web_crawler/flipper_http.h

@@ -0,0 +1,1562 @@
+// flipper_http.h
+#ifndef FLIPPER_HTTP_H
+#define FLIPPER_HTTP_H
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_serial.h>
+#include <storage/storage.h>
+
+// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext
+
+#define HTTP_TAG "WebCrawler"             // change this to your app name
+#define http_tag "web_crawler"            // change this to your app id
+#define UART_CH (FuriHalSerialIdUsart)    // UART channel
+#define TIMEOUT_DURATION_TICKS (3 * 1000) // 3 seconds
+#define BAUDRATE (115200)                 // UART baudrate
+#define RX_BUF_SIZE 2048                  // UART RX buffer size
+#define RX_LINE_BUFFER_SIZE 2048          // UART RX line buffer size (increase for large responses)
+#define MAX_FILE_SHOW 5000                // Maximum data from file to show
+
+// Forward declaration for callback
+typedef void (*FlipperHTTP_Callback)(const char *line, void *context);
+
+// Functions
+bool flipper_http_init(FlipperHTTP_Callback callback, void *context);
+void flipper_http_deinit();
+//---
+void flipper_http_rx_callback(const char *line, void *context);
+bool flipper_http_send_data(const char *data);
+//---
+bool flipper_http_connect_wifi();
+bool flipper_http_disconnect_wifi();
+bool flipper_http_ping();
+bool flipper_http_scan_wifi();
+bool flipper_http_save_wifi(const char *ssid, const char *password);
+bool flipper_http_ip_wifi();
+bool flipper_http_ip_address();
+//---
+bool flipper_http_list_commands();
+bool flipper_http_led_on();
+bool flipper_http_led_off();
+bool flipper_http_parse_json(const char *key, const char *json_data);
+bool flipper_http_parse_json_array(const char *key, int index, const char *json_data);
+//---
+bool flipper_http_get_request(const char *url);
+bool flipper_http_get_request_with_headers(const char *url, const char *headers);
+bool flipper_http_post_request_with_headers(
+    const char *url,
+    const char *headers,
+    const char *payload);
+bool flipper_http_put_request_with_headers(
+    const char *url,
+    const char *headers,
+    const char *payload);
+bool flipper_http_delete_request_with_headers(
+    const char *url,
+    const char *headers,
+    const char *payload);
+//---
+bool flipper_http_get_request_bytes(const char *url, const char *headers);
+bool flipper_http_post_request_bytes(const char *url, const char *headers, const char *payload);
+//
+bool flipper_http_append_to_file(
+    const void *data,
+    size_t data_size,
+    bool start_new_file,
+    char *file_path);
+
+FuriString *flipper_http_load_from_file(char *file_path);
+static char *trim(const char *str);
+//
+bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void));
+
+// State variable to track the UART state
+typedef enum
+{
+    INACTIVE,  // Inactive state
+    IDLE,      // Default state
+    RECEIVING, // Receiving data
+    SENDING,   // Sending data
+    ISSUE,     // Issue with connection
+} SerialState;
+
+// Event Flags for UART Worker Thread
+typedef enum
+{
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+} WorkerEvtFlags;
+
+// FlipperHTTP Structure
+typedef struct
+{
+    FuriStreamBuffer *flipper_http_stream;  // Stream buffer for UART communication
+    FuriHalSerialHandle *serial_handle;     // Serial handle for UART communication
+    FuriThread *rx_thread;                  // Worker thread for UART
+    FuriThreadId rx_thread_id;              // Worker thread ID
+    FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines
+    void *callback_context;                 // Context for the callback
+    SerialState state;                      // State of the UART
+
+    // variable to store the last received data from the UART
+    char *last_response;
+    char file_path[256]; // Path to save the received data
+
+    // Timer-related members
+    FuriTimer *get_timeout_timer; // Timer for HTTP request timeout
+
+    bool started_receiving_get; // Indicates if a GET request has started
+    bool just_started_get;      // Indicates if GET data reception has just started
+
+    bool started_receiving_post; // Indicates if a POST request has started
+    bool just_started_post;      // Indicates if POST data reception has just started
+
+    bool started_receiving_put; // Indicates if a PUT request has started
+    bool just_started_put;      // Indicates if PUT data reception has just started
+
+    bool started_receiving_delete; // Indicates if a DELETE request has started
+    bool just_started_delete;      // Indicates if DELETE data reception has just started
+
+    // Buffer to hold the raw bytes received from the UART
+    uint8_t *received_bytes;
+    size_t received_bytes_len; // Length of the received bytes
+    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
+} FlipperHTTP;
+
+static FlipperHTTP fhttp;
+// Global static array for the line buffer
+static char rx_line_buffer[RX_LINE_BUFFER_SIZE];
+#define FILE_BUFFER_SIZE 512
+static uint8_t file_buffer[FILE_BUFFER_SIZE];
+
+// fhttp.last_response holds the last received data from the UART, which could be [GET/END], [POST/END], [PUT/END], [DELETE/END], etc
+
+// Function to append received data to file
+// make sure to initialize the file path before calling this function
+bool flipper_http_append_to_file(
+    const void *data,
+    size_t data_size,
+    bool start_new_file,
+    char *file_path)
+{
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    if (start_new_file)
+    {
+        // Open the file in write mode
+        if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+        {
+            FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path);
+            storage_file_free(file);
+            furi_record_close(RECORD_STORAGE);
+            return false;
+        }
+    }
+    else
+    {
+        // Open the file in append mode
+        if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_OPEN_APPEND))
+        {
+            FURI_LOG_E(HTTP_TAG, "Failed to open file for appending: %s", file_path);
+            storage_file_free(file);
+            furi_record_close(RECORD_STORAGE);
+            return false;
+        }
+    }
+
+    // Write the data to the file
+    if (storage_file_write(file, data, data_size) != data_size)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+    return true;
+}
+
+FuriString *flipper_http_load_from_file(char *file_path)
+{
+    // Open the storage record
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    if (!storage)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open storage record");
+        return NULL;
+    }
+
+    // Allocate a file handle
+    File *file = storage_file_alloc(storage);
+    if (!file)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate storage file");
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Open the file for reading
+    if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL; // Return false if the file does not exist
+    }
+
+    // Allocate a FuriString to hold the received data
+    FuriString *str_result = furi_string_alloc();
+    if (!str_result)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Reset the FuriString to ensure it's empty before reading
+    furi_string_reset(str_result);
+
+    // Define a buffer to hold the read data
+    uint8_t *buffer = (uint8_t *)malloc(MAX_FILE_SHOW);
+    if (!buffer)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer");
+        furi_string_free(str_result);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Read data into the buffer
+    size_t read_count = storage_file_read(file, buffer, MAX_FILE_SHOW);
+    if (storage_file_get_error(file) != FSE_OK)
+    {
+        FURI_LOG_E(HTTP_TAG, "Error reading from file.");
+        furi_string_free(str_result);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Append each byte to the FuriString
+    for (size_t i = 0; i < read_count; i++)
+    {
+        furi_string_push_back(str_result, buffer[i]);
+    }
+
+    // Check if there is more data beyond the maximum size
+    char extra_byte;
+    storage_file_read(file, &extra_byte, 1);
+
+    // Clean up
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+    free(buffer);
+    return str_result;
+}
+
+// UART worker thread
+/**
+ * @brief      Worker thread to handle UART data asynchronously.
+ * @return     0
+ * @param      context   The context to pass to the callback.
+ * @note       This function will handle received data asynchronously via the callback.
+ */
+// UART worker thread
+static 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 & WorkerEvtRxDone)
+        {
+            // Continuously read from the stream buffer until it's empty
+            while (!furi_stream_buffer_is_empty(fhttp.flipper_http_stream))
+            {
+                // Read one byte at a time
+                char c = 0;
+                size_t received = furi_stream_buffer_receive(fhttp.flipper_http_stream, &c, 1, 0);
+
+                if (received == 0)
+                {
+                    // No more data to read
+                    break;
+                }
+
+                // Append the received byte to the file if saving is enabled
+                if (fhttp.save_bytes)
+                {
+                    // Add byte to the buffer
+                    file_buffer[file_buffer_len++] = c;
+                    // 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))
+                        {
+                            FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+                        }
+                        file_buffer_len = 0;
+                    }
+                }
+
+                // Handle line buffering only if callback is set (text data)
+                if (fhttp.handle_rx_line_cb)
+                {
+                    // Handle line buffering
+                    if (c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1)
+                    {
+                        rx_line_buffer[rx_line_pos] = '\0'; // Null-terminate the line
+
+                        // Invoke the callback with the complete line
+                        fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context);
+
+                        // save the received data
+
+                        // Reset the line buffer position
+                        rx_line_pos = 0;
+                    }
+                    else
+                    {
+                        rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer
+                    }
+                }
+            }
+        }
+    }
+
+    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
+/**
+ * @brief      Callback function for the GET timeout timer.
+ * @return     0
+ * @param      context   The context to pass to the callback.
+ * @note       This function will be called when the GET request times out.
+ */
+void get_timeout_timer_callback(void *context)
+{
+    UNUSED(context);
+    FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving the end.");
+
+    // Reset the state
+    fhttp.started_receiving_get = false;
+    fhttp.started_receiving_post = false;
+    fhttp.started_receiving_put = false;
+    fhttp.started_receiving_delete = false;
+
+    // Update UART state
+    fhttp.state = ISSUE;
+}
+
+// UART RX Handler Callback (Interrupt Context)
+/**
+ * @brief      A private callback function to handle received data asynchronously.
+ * @return     void
+ * @param      handle    The UART handle.
+ * @param      event     The event type.
+ * @param      context   The context to pass to the callback.
+ * @note       This function will handle received data asynchronously via the callback.
+ */
+static void _flipper_http_rx_callback(
+    FuriHalSerialHandle *handle,
+    FuriHalSerialRxEvent event,
+    void *context)
+{
+    UNUSED(context);
+    if (event == FuriHalSerialRxEventData)
+    {
+        uint8_t data = furi_hal_serial_async_rx(handle);
+        furi_stream_buffer_send(fhttp.flipper_http_stream, &data, 1, 0);
+        furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtRxDone);
+    }
+}
+
+// UART initialization function
+/**
+ * @brief      Initialize UART.
+ * @return     true if the UART was initialized successfully, false otherwise.
+ * @param      callback  The callback function to handle received data (ex. flipper_http_rx_callback).
+ * @param      context   The context to pass to the callback.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_init(FlipperHTTP_Callback callback, void *context)
+{
+    if (!context)
+    {
+        FURI_LOG_E(HTTP_TAG, "Invalid context provided to flipper_http_init.");
+        return false;
+    }
+    if (!callback)
+    {
+        FURI_LOG_E(HTTP_TAG, "Invalid callback provided to flipper_http_init.");
+        return false;
+    }
+    fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1);
+    if (!fhttp.flipper_http_stream)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate UART stream buffer.");
+        return false;
+    }
+
+    fhttp.rx_thread = furi_thread_alloc();
+    if (!fhttp.rx_thread)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread.");
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    furi_thread_set_name(fhttp.rx_thread, "FlipperHTTP_RxThread");
+    furi_thread_set_stack_size(fhttp.rx_thread, 1024);
+    furi_thread_set_context(fhttp.rx_thread, &fhttp);
+    furi_thread_set_callback(fhttp.rx_thread, flipper_http_worker);
+
+    fhttp.handle_rx_line_cb = callback;
+    fhttp.callback_context = context;
+
+    furi_thread_start(fhttp.rx_thread);
+    fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread);
+
+    // handle when the UART control is busy to avoid furi_check failed
+    if (furi_hal_serial_control_is_busy(UART_CH))
+    {
+        FURI_LOG_E(HTTP_TAG, "UART control is busy.");
+        return false;
+    }
+
+    fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH);
+    if (!fhttp.serial_handle)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL");
+        // Cleanup resources
+        furi_thread_free(fhttp.rx_thread);
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    // Initialize UART with acquired handle
+    furi_hal_serial_init(fhttp.serial_handle, BAUDRATE);
+
+    // Enable RX direction
+    furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+
+    // Start asynchronous RX with the callback
+    furi_hal_serial_async_rx_start(fhttp.serial_handle, _flipper_http_rx_callback, &fhttp, false);
+
+    // Wait for the TX to complete to ensure UART is ready
+    furi_hal_serial_tx_wait_complete(fhttp.serial_handle);
+
+    // Allocate the timer for handling timeouts
+    fhttp.get_timeout_timer = furi_timer_alloc(
+        get_timeout_timer_callback, // Callback function
+        FuriTimerTypeOnce,          // One-shot timer
+        &fhttp                      // Context passed to callback
+    );
+
+    if (!fhttp.get_timeout_timer)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate HTTP request timeout timer.");
+        // Cleanup resources
+        furi_hal_serial_async_rx_stop(fhttp.serial_handle);
+        furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+        furi_hal_serial_control_release(fhttp.serial_handle);
+        furi_hal_serial_deinit(fhttp.serial_handle);
+        furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop);
+        furi_thread_join(fhttp.rx_thread);
+        furi_thread_free(fhttp.rx_thread);
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    // Set the timer thread priority if needed
+    furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+
+    fhttp.last_response = (char *)malloc(RX_BUF_SIZE);
+    if (!fhttp.last_response)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate memory for last_response.");
+        return false;
+    }
+
+    // FURI_LOG_I(HTTP_TAG, "UART initialized successfully.");
+    return true;
+}
+
+// Deinitialize UART
+/**
+ * @brief      Deinitialize UART.
+ * @return     void
+ * @note       This function will stop the asynchronous RX, release the serial handle, and free the resources.
+ */
+void flipper_http_deinit()
+{
+    if (fhttp.serial_handle == NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?");
+        return;
+    }
+    // Stop asynchronous RX
+    furi_hal_serial_async_rx_stop(fhttp.serial_handle);
+
+    // Release and deinitialize the serial handle
+    furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+    furi_hal_serial_control_release(fhttp.serial_handle);
+    furi_hal_serial_deinit(fhttp.serial_handle);
+
+    // Signal the worker thread to stop
+    furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop);
+    // Wait for the thread to finish
+    furi_thread_join(fhttp.rx_thread);
+    // Free the thread resources
+    furi_thread_free(fhttp.rx_thread);
+
+    // Free the stream buffer
+    furi_stream_buffer_free(fhttp.flipper_http_stream);
+
+    // Free the timer
+    if (fhttp.get_timeout_timer)
+    {
+        furi_timer_free(fhttp.get_timeout_timer);
+        fhttp.get_timeout_timer = NULL;
+    }
+
+    // Free the last response
+    if (fhttp.last_response)
+    {
+        free(fhttp.last_response);
+        fhttp.last_response = NULL;
+    }
+
+    // FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully.");
+}
+
+// Function to send data over UART with newline termination
+/**
+ * @brief      Send data over UART with newline termination.
+ * @return     true if the data was sent successfully, false otherwise.
+ * @param      data  The data to send over UART.
+ * @note       The data will be sent over UART with a newline character appended.
+ */
+bool flipper_http_send_data(const char *data)
+{
+    size_t data_length = strlen(data);
+    if (data_length == 0)
+    {
+        FURI_LOG_E("FlipperHTTP", "Attempted to send empty data.");
+        return false;
+    }
+
+    // Create a buffer with data + '\n'
+    size_t send_length = data_length + 1; // +1 for '\n'
+    if (send_length > 256)
+    { // Ensure buffer size is sufficient
+        FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP.");
+        return false;
+    }
+
+    char send_buffer[257]; // 256 + 1 for safety
+    strncpy(send_buffer, data, 256);
+    send_buffer[data_length] = '\n';     // Append newline
+    send_buffer[data_length + 1] = '\0'; // Null-terminate
+
+    if (fhttp.state == INACTIVE && ((strstr(send_buffer, "[PING]") == NULL) &&
+                                    (strstr(send_buffer, "[WIFI/CONNECT]") == NULL)))
+    {
+        FURI_LOG_E("FlipperHTTP", "Cannot send data while INACTIVE.");
+        fhttp.last_response = "Cannot send data while INACTIVE.";
+        return false;
+    }
+
+    fhttp.state = SENDING;
+    furi_hal_serial_tx(fhttp.serial_handle, (const uint8_t *)send_buffer, send_length);
+
+    // Uncomment below line to log the data sent over UART
+    // FURI_LOG_I("FlipperHTTP", "Sent data over UART: %s", send_buffer);
+    fhttp.state = IDLE;
+    return true;
+}
+
+// Function to send a PING request
+/**
+ * @brief      Send a PING request to check if the Wifi Dev Board is connected.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ * @note       This is best used to check if the Wifi Dev Board is connected.
+ * @note       The state will remain INACTIVE until a PONG is received.
+ */
+bool flipper_http_ping()
+{
+    const char *command = "[PING]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send PING command.");
+        return false;
+    }
+    // set state as INACTIVE to be made IDLE if PONG is received
+    fhttp.state = INACTIVE;
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to list available commands
+/**
+ * @brief      Send a command to list available commands.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_list_commands()
+{
+    const char *command = "[LIST]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send LIST command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to turn on the LED
+/**
+ * @brief      Allow the LED to display while processing.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_led_on()
+{
+    const char *command = "[LED/ON]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send LED ON command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to turn off the LED
+/**
+ * @brief      Disable the LED from displaying while processing.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_led_off()
+{
+    const char *command = "[LED/OFF]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send LED OFF command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to parse JSON data
+/**
+ * @brief      Parse JSON data.
+ * @return     true if the JSON data was parsed successfully, false otherwise.
+ * @param      key       The key to parse from the JSON data.
+ * @param      json_data The JSON data to parse.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_parse_json(const char *key, const char *json_data)
+{
+    if (!key || !json_data)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json.");
+        return false;
+    }
+
+    char buffer[256];
+    int ret =
+        snprintf(buffer, sizeof(buffer), "[PARSE]{\"key\":\"%s\",\"json\":%s}", key, json_data);
+    if (ret < 0 || ret >= (int)sizeof(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format JSON parse command.");
+        return false;
+    }
+
+    if (!flipper_http_send_data(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to parse JSON array data
+/**
+ * @brief      Parse JSON array data.
+ * @return     true if the JSON array data was parsed successfully, false otherwise.
+ * @param      key       The key to parse from the JSON array data.
+ * @param      index     The index to parse from the JSON array data.
+ * @param      json_data The JSON array data to parse.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_parse_json_array(const char *key, int index, const char *json_data)
+{
+    if (!key || !json_data)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_parse_json_array.");
+        return false;
+    }
+
+    char buffer[256];
+    int ret = snprintf(
+        buffer,
+        sizeof(buffer),
+        "[PARSE/ARRAY]{\"key\":\"%s\",\"index\":%d,\"json\":%s}",
+        key,
+        index,
+        json_data);
+    if (ret < 0 || ret >= (int)sizeof(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format JSON parse array command.");
+        return false;
+    }
+
+    if (!flipper_http_send_data(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send JSON parse array command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to scan for WiFi networks
+/**
+ * @brief      Send a command to scan for WiFi networks.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_scan_wifi()
+{
+    const char *command = "[WIFI/SCAN]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi scan command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to save WiFi settings (returns true if successful)
+/**
+ * @brief      Send a command to save WiFi settings.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_save_wifi(const char *ssid, const char *password)
+{
+    if (!ssid || !password)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_save_wifi.");
+        return false;
+    }
+    char buffer[256];
+    int ret = snprintf(
+        buffer, sizeof(buffer), "[WIFI/SAVE]{\"ssid\":\"%s\",\"password\":\"%s\"}", ssid, password);
+    if (ret < 0 || ret >= (int)sizeof(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format WiFi save command.");
+        return false;
+    }
+
+    if (!flipper_http_send_data(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to get IP address of WiFi Devboard
+/**
+ * @brief      Send a command to get the IP address of the WiFi Devboard
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_ip_address()
+{
+    const char *command = "[IP/ADDRESS]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send IP address command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to get IP address of the connected WiFi network
+/**
+ * @brief      Send a command to get the IP address of the connected WiFi network.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_ip_wifi()
+{
+    const char *command = "[WIFI/IP]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi IP command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to disconnect from WiFi (returns true if successful)
+/**
+ * @brief      Send a command to disconnect from WiFi.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_disconnect_wifi()
+{
+    const char *command = "[WIFI/DISCONNECT]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to connect to WiFi (returns true if successful)
+/**
+ * @brief      Send a command to connect to WiFi.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_connect_wifi()
+{
+    const char *command = "[WIFI/CONNECT]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to send a GET request
+/**
+ * @brief      Send a GET request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the GET request to.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_get_request(const char *url)
+{
+    if (!url)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request.");
+        return false;
+    }
+
+    // Prepare GET request command
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[GET]%s", url);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format GET request command.");
+        return false;
+    }
+
+    // Send GET request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send GET request command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a GET request with headers
+/**
+ * @brief      Send a GET request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the GET request to.
+ * @param      headers  The headers to send with the GET request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_get_request_with_headers(const char *url, const char *headers)
+{
+    if (!url || !headers)
+    {
+        FURI_LOG_E(
+            "FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_with_headers.");
+        return false;
+    }
+
+    // Prepare GET request command with headers
+    char command[256];
+    int ret = snprintf(
+        command, sizeof(command), "[GET/HTTP]{\"url\":\"%s\",\"headers\":%s}", url, headers);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers.");
+        return false;
+    }
+
+    // Send GET request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a GET request with headers and return bytes
+/**
+ * @brief      Send a GET request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the GET request to.
+ * @param      headers  The headers to send with the GET request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_get_request_bytes(const char *url, const char *headers)
+{
+    if (!url || !headers)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_bytes.");
+        return false;
+    }
+
+    // Prepare GET request command with headers
+    char command[256];
+    int ret = snprintf(
+        command, sizeof(command), "[GET/BYTES]{\"url\":\"%s\",\"headers\":%s}", url, headers);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers.");
+        return false;
+    }
+
+    // Send GET request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a POST request with headers
+/**
+ * @brief      Send a POST request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the POST request to.
+ * @param      headers  The headers to send with the POST request.
+ * @param      data  The data to send with the POST request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_post_request_with_headers(
+    const char *url,
+    const char *headers,
+    const char *payload)
+{
+    if (!url || !headers || !payload)
+    {
+        FURI_LOG_E(
+            "FlipperHTTP",
+            "Invalid arguments provided to flipper_http_post_request_with_headers.");
+        return false;
+    }
+
+    // Prepare POST request command with headers and data
+    char command[256];
+    int ret = snprintf(
+        command,
+        sizeof(command),
+        "[POST/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}",
+        url,
+        headers,
+        payload);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data.");
+        return false;
+    }
+
+    // Send POST request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a POST request with headers and return bytes
+/**
+ * @brief      Send a POST request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the POST request to.
+ * @param      headers  The headers to send with the POST request.
+ * @param      payload  The data to send with the POST request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_post_request_bytes(const char *url, const char *headers, const char *payload)
+{
+    if (!url || !headers || !payload)
+    {
+        FURI_LOG_E(
+            "FlipperHTTP", "Invalid arguments provided to flipper_http_post_request_bytes.");
+        return false;
+    }
+
+    // Prepare POST request command with headers and data
+    char command[256];
+    int ret = snprintf(
+        command,
+        sizeof(command),
+        "[POST/BYTES]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}",
+        url,
+        headers,
+        payload);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data.");
+        return false;
+    }
+
+    // Send POST request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a PUT request with headers
+/**
+ * @brief      Send a PUT request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the PUT request to.
+ * @param      headers  The headers to send with the PUT request.
+ * @param      data  The data to send with the PUT request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_put_request_with_headers(
+    const char *url,
+    const char *headers,
+    const char *payload)
+{
+    if (!url || !headers || !payload)
+    {
+        FURI_LOG_E(
+            "FlipperHTTP", "Invalid arguments provided to flipper_http_put_request_with_headers.");
+        return false;
+    }
+
+    // Prepare PUT request command with headers and data
+    char command[256];
+    int ret = snprintf(
+        command,
+        sizeof(command),
+        "[PUT/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}",
+        url,
+        headers,
+        payload);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format PUT request command with headers and data.");
+        return false;
+    }
+
+    // Send PUT request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a DELETE request with headers
+/**
+ * @brief      Send a DELETE request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the DELETE request to.
+ * @param      headers  The headers to send with the DELETE request.
+ * @param      data  The data to send with the DELETE request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_delete_request_with_headers(
+    const char *url,
+    const char *headers,
+    const char *payload)
+{
+    if (!url || !headers || !payload)
+    {
+        FURI_LOG_E(
+            "FlipperHTTP",
+            "Invalid arguments provided to flipper_http_delete_request_with_headers.");
+        return false;
+    }
+
+    // Prepare DELETE request command with headers and data
+    char command[256];
+    int ret = snprintf(
+        command,
+        sizeof(command),
+        "[DELETE/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}",
+        url,
+        headers,
+        payload);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E(
+            "FlipperHTTP", "Failed to format DELETE request command with headers and data.");
+        return false;
+    }
+
+    // Send DELETE request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to handle received data asynchronously
+/**
+ * @brief      Callback function to handle received data asynchronously.
+ * @return     void
+ * @param      line     The received line.
+ * @param      context  The context passed to the callback.
+ * @note       The received data will be handled asynchronously via the callback and handles the state of the UART.
+ */
+void flipper_http_rx_callback(const char *line, void *context)
+{
+    if (!line || !context)
+    {
+        FURI_LOG_E(HTTP_TAG, "Invalid arguments provided to flipper_http_rx_callback.");
+        return;
+    }
+
+    // Trim the received line to check if it's empty
+    char *trimmed_line = trim(line);
+    if (trimmed_line != NULL && trimmed_line[0] != '\0')
+    {
+        // if the line is not [GET/END] or [POST/END] or [PUT/END] or [DELETE/END]
+        if (strstr(trimmed_line, "[GET/END]") == NULL &&
+            strstr(trimmed_line, "[POST/END]") == NULL &&
+            strstr(trimmed_line, "[PUT/END]") == NULL &&
+            strstr(trimmed_line, "[DELETE/END]") == NULL)
+        {
+            strncpy(fhttp.last_response, trimmed_line, RX_BUF_SIZE);
+        }
+    }
+    free(trimmed_line); // Free the allocated memory for trimmed_line
+
+    if (fhttp.state != INACTIVE && fhttp.state != ISSUE)
+    {
+        fhttp.state = RECEIVING;
+    }
+
+    // Uncomment below line to log the data received over UART
+    FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line);
+
+    // Check if we've started receiving data from a GET request
+    if (fhttp.started_receiving_get)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[GET/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "GET request completed.");
+            // Stop the timer since we've completed the GET request
+            furi_timer_stop(fhttp.get_timeout_timer);
+            fhttp.started_receiving_get = false;
+            fhttp.just_started_get = false;
+            fhttp.state = IDLE;
+            fhttp.save_bytes = false;
+            fhttp.is_bytes_request = false;
+            fhttp.save_received_data = false;
+            return;
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.save_received_data &&
+            !flipper_http_append_to_file(
+                line, strlen(line), !fhttp.just_started_get, fhttp.file_path))
+        {
+            FURI_LOG_E(HTTP_TAG, "Failed to append data to file.");
+            fhttp.started_receiving_get = false;
+            fhttp.just_started_get = false;
+            fhttp.state = IDLE;
+            return;
+        }
+
+        if (!fhttp.just_started_get)
+        {
+            fhttp.just_started_get = true;
+        }
+        return;
+    }
+
+    // Check if we've started receiving data from a POST request
+    else if (fhttp.started_receiving_post)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[POST/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "POST request completed.");
+            // Stop the timer since we've completed the POST request
+            furi_timer_stop(fhttp.get_timeout_timer);
+            fhttp.started_receiving_post = false;
+            fhttp.just_started_post = false;
+            fhttp.state = IDLE;
+            fhttp.save_bytes = false;
+            fhttp.is_bytes_request = false;
+            fhttp.save_received_data = false;
+            return;
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.save_received_data &&
+            !flipper_http_append_to_file(
+                line, strlen(line), !fhttp.just_started_post, fhttp.file_path))
+        {
+            FURI_LOG_E(HTTP_TAG, "Failed to append data to file.");
+            fhttp.started_receiving_post = false;
+            fhttp.just_started_post = false;
+            fhttp.state = IDLE;
+            return;
+        }
+
+        if (!fhttp.just_started_post)
+        {
+            fhttp.just_started_post = true;
+        }
+        return;
+    }
+
+    // Check if we've started receiving data from a PUT request
+    else if (fhttp.started_receiving_put)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[PUT/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "PUT request completed.");
+            // Stop the timer since we've completed the PUT request
+            furi_timer_stop(fhttp.get_timeout_timer);
+            fhttp.started_receiving_put = false;
+            fhttp.just_started_put = false;
+            fhttp.state = IDLE;
+            fhttp.save_bytes = false;
+            fhttp.is_bytes_request = false;
+            fhttp.save_received_data = false;
+            return;
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.save_received_data &&
+            !flipper_http_append_to_file(
+                line, strlen(line), !fhttp.just_started_put, fhttp.file_path))
+        {
+            FURI_LOG_E(HTTP_TAG, "Failed to append data to file.");
+            fhttp.started_receiving_put = false;
+            fhttp.just_started_put = false;
+            fhttp.state = IDLE;
+            return;
+        }
+
+        if (!fhttp.just_started_put)
+        {
+            fhttp.just_started_put = true;
+        }
+        return;
+    }
+
+    // Check if we've started receiving data from a DELETE request
+    else if (fhttp.started_receiving_delete)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[DELETE/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "DELETE request completed.");
+            // Stop the timer since we've completed the DELETE request
+            furi_timer_stop(fhttp.get_timeout_timer);
+            fhttp.started_receiving_delete = false;
+            fhttp.just_started_delete = false;
+            fhttp.state = IDLE;
+            fhttp.save_bytes = false;
+            fhttp.is_bytes_request = false;
+            fhttp.save_received_data = false;
+            return;
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.save_received_data &&
+            !flipper_http_append_to_file(
+                line, strlen(line), !fhttp.just_started_delete, fhttp.file_path))
+        {
+            FURI_LOG_E(HTTP_TAG, "Failed to append data to file.");
+            fhttp.started_receiving_delete = false;
+            fhttp.just_started_delete = false;
+            fhttp.state = IDLE;
+            return;
+        }
+
+        if (!fhttp.just_started_delete)
+        {
+            fhttp.just_started_delete = true;
+        }
+        return;
+    }
+
+    // Handle different types of responses
+    if (strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Operation succeeded.");
+    }
+    else if (strstr(line, "[INFO]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Received info: %s", line);
+
+        if (fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL)
+        {
+            fhttp.state = IDLE;
+        }
+    }
+    else if (strstr(line, "[GET/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "GET request succeeded.");
+        fhttp.started_receiving_get = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+        // for GET request, save data only if it's a bytes request
+        fhttp.save_bytes = fhttp.is_bytes_request;
+        return;
+    }
+    else if (strstr(line, "[POST/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "POST request succeeded.");
+        fhttp.started_receiving_post = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+        // for POST request, save data only if it's a bytes request
+        fhttp.save_bytes = fhttp.is_bytes_request;
+        return;
+    }
+    else if (strstr(line, "[PUT/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "PUT request succeeded.");
+        fhttp.started_receiving_put = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+        return;
+    }
+    else if (strstr(line, "[DELETE/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "DELETE request succeeded.");
+        fhttp.started_receiving_delete = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+        return;
+    }
+    else if (strstr(line, "[DISCONNECTED]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully.");
+    }
+    else if (strstr(line, "[ERROR]") != NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "Received error: %s", line);
+        fhttp.state = ISSUE;
+        return;
+    }
+    else if (strstr(line, "[PONG]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Received PONG response: Wifi Dev Board is still alive.");
+
+        // send command to connect to WiFi
+        if (fhttp.state == INACTIVE)
+        {
+            fhttp.state = IDLE;
+            return;
+        }
+    }
+
+    if (fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL)
+    {
+        fhttp.state = IDLE;
+    }
+    else if (fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL)
+    {
+        fhttp.state = INACTIVE;
+    }
+    else
+    {
+        fhttp.state = IDLE;
+    }
+}
+
+// Function to trim leading and trailing spaces and newlines from a constant string
+char *trim(const char *str)
+{
+    const char *end;
+    char *trimmed_str;
+    size_t len;
+
+    // Trim leading space
+    while (isspace((unsigned char)*str))
+        str++;
+
+    // All spaces?
+    if (*str == 0)
+        return strdup(""); // Return an empty string if all spaces
+
+    // Trim trailing space
+    end = str + strlen(str) - 1;
+    while (end > str && isspace((unsigned char)*end))
+        end--;
+
+    // Set length for the trimmed string
+    len = end - str + 1;
+
+    // Allocate space for the trimmed string and null terminator
+    trimmed_str = (char *)malloc(len + 1);
+    if (trimmed_str == NULL)
+    {
+        return NULL; // Handle memory allocation failure
+    }
+
+    // Copy the trimmed part of the string into trimmed_str
+    strncpy(trimmed_str, str, len);
+    trimmed_str[len] = '\0'; // Null terminate the string
+
+    return trimmed_str;
+}
+
+/**
+ * @brief Process requests and parse JSON data asynchronously
+ * @param http_request The function to send the request
+ * @param parse_json The function to parse the JSON
+ * @return true if successful, false otherwise
+ */
+bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void))
+{
+    if (http_request()) // start the async request
+    {
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+    }
+    else
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to send request");
+        return false;
+    }
+    while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0)
+    {
+        // Wait for the request to be received
+        furi_delay_ms(100);
+    }
+    furi_timer_stop(fhttp.get_timeout_timer);
+    if (!parse_json()) // parse the JSON before switching to the view (synchonous)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to parse the JSON...");
+        return false;
+    }
+    return true;
+}
+
+#endif // FLIPPER_HTTP_H

+ 803 - 0
web_crawler/uart_text_input.h

@@ -0,0 +1,803 @@
+// from https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/uart_terminal/uart_text_input.c
+// all credits to xMasterX for the code
+#ifndef UART_TEXT_INPUT_H
+#define UART_TEXT_INPUT_H
+
+#include <gui/elements.h>
+#include "web_crawler_icons.h"
+#include <furi.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <core/common_defines.h>
+
+/** Text input anonymous structure */
+typedef struct UART_TextInput UART_TextInput;
+typedef void (*UART_TextInputCallback)(void *context);
+typedef bool (*UART_TextInputValidatorCallback)(const char *text, FuriString *error, void *context);
+
+UART_TextInputValidatorCallback
+uart_text_input_get_validator_callback(UART_TextInput *uart_text_input);
+
+void uart_text_input_reset(UART_TextInput *uart_text_input);
+
+struct UART_TextInput
+{
+    View *view;
+    FuriTimer *timer;
+};
+
+typedef struct
+{
+    const char text;
+    const uint8_t x;
+    const uint8_t y;
+} UART_TextInputKey;
+
+typedef struct
+{
+    const char *header;
+    char *text_buffer;
+    size_t text_buffer_size;
+    bool clear_default_text;
+
+    UART_TextInputCallback callback;
+    void *callback_context;
+
+    uint8_t selected_row;
+    uint8_t selected_column;
+
+    UART_TextInputValidatorCallback validator_callback;
+    void *validator_callback_context;
+    FuriString *validator_text;
+    bool valadator_message_visible;
+} UART_TextInputModel;
+
+static const uint8_t keyboard_origin_x = 1;
+static const uint8_t keyboard_origin_y = 29;
+static const uint8_t keyboard_row_count = 4;
+
+#define mode_AT "Send AT command to UART"
+
+#define ENTER_KEY '\r'
+#define BACKSPACE_KEY '\b'
+
+static const UART_TextInputKey keyboard_keys_row_1[] = {
+    {'{', 1, 0},
+    {'(', 9, 0},
+    {'[', 17, 0},
+    {'|', 25, 0},
+    {'@', 33, 0},
+    {'&', 41, 0},
+    {'#', 49, 0},
+    {';', 57, 0},
+    {'^', 65, 0},
+    {'*', 73, 0},
+    {'`', 81, 0},
+    {'"', 89, 0},
+    {'~', 97, 0},
+    {'\'', 105, 0},
+    {'.', 113, 0},
+    {'/', 120, 0},
+};
+
+static const UART_TextInputKey keyboard_keys_row_2[] = {
+    {'q', 1, 10},
+    {'w', 9, 10},
+    {'e', 17, 10},
+    {'r', 25, 10},
+    {'t', 33, 10},
+    {'y', 41, 10},
+    {'u', 49, 10},
+    {'i', 57, 10},
+    {'o', 65, 10},
+    {'p', 73, 10},
+    {'0', 81, 10},
+    {'1', 89, 10},
+    {'2', 97, 10},
+    {'3', 105, 10},
+    {'=', 113, 10},
+    {'-', 120, 10},
+};
+
+static const UART_TextInputKey keyboard_keys_row_3[] = {
+    {'a', 1, 21},
+    {'s', 9, 21},
+    {'d', 18, 21},
+    {'f', 25, 21},
+    {'g', 33, 21},
+    {'h', 41, 21},
+    {'j', 49, 21},
+    {'k', 57, 21},
+    {'l', 65, 21},
+    {BACKSPACE_KEY, 72, 13},
+    {'4', 89, 21},
+    {'5', 97, 21},
+    {'6', 105, 21},
+    {'$', 113, 21},
+    {'%', 120, 21},
+
+};
+
+static const UART_TextInputKey keyboard_keys_row_4[] = {
+    {'z', 1, 33},
+    {'x', 9, 33},
+    {'c', 18, 33},
+    {'v', 25, 33},
+    {'b', 33, 33},
+    {'n', 41, 33},
+    {'m', 49, 33},
+    {'_', 57, 33},
+    {ENTER_KEY, 64, 24},
+    {'7', 89, 33},
+    {'8', 97, 33},
+    {'9', 105, 33},
+    {'!', 113, 33},
+    {'+', 120, 33},
+};
+
+static uint8_t get_row_size(uint8_t row_index)
+{
+    uint8_t row_size = 0;
+
+    switch (row_index + 1)
+    {
+    case 1:
+        row_size = sizeof(keyboard_keys_row_1) / sizeof(UART_TextInputKey);
+        break;
+    case 2:
+        row_size = sizeof(keyboard_keys_row_2) / sizeof(UART_TextInputKey);
+        break;
+    case 3:
+        row_size = sizeof(keyboard_keys_row_3) / sizeof(UART_TextInputKey);
+        break;
+    case 4:
+        row_size = sizeof(keyboard_keys_row_4) / sizeof(UART_TextInputKey);
+        break;
+    }
+
+    return row_size;
+}
+
+static const UART_TextInputKey *get_row(uint8_t row_index)
+{
+    const UART_TextInputKey *row = NULL;
+
+    switch (row_index + 1)
+    {
+    case 1:
+        row = keyboard_keys_row_1;
+        break;
+    case 2:
+        row = keyboard_keys_row_2;
+        break;
+    case 3:
+        row = keyboard_keys_row_3;
+        break;
+    case 4:
+        row = keyboard_keys_row_4;
+        break;
+    }
+
+    return row;
+}
+
+static char get_selected_char(UART_TextInputModel *model)
+{
+    return get_row(model->selected_row)[model->selected_column].text;
+}
+
+static bool char_is_lowercase(char letter)
+{
+    return (letter >= 0x61 && letter <= 0x7A);
+}
+
+static bool char_is_uppercase(char letter)
+{
+    return (letter >= 0x41 && letter <= 0x5A);
+}
+
+static char char_to_lowercase(const char letter)
+{
+    switch (letter)
+    {
+    case ' ':
+        return 0x5f;
+        break;
+    case ')':
+        return 0x28;
+        break;
+    case '}':
+        return 0x7b;
+        break;
+    case ']':
+        return 0x5b;
+        break;
+    case '\\':
+        return 0x2f;
+        break;
+    case ':':
+        return 0x3b;
+        break;
+    case ',':
+        return 0x2e;
+        break;
+    case '?':
+        return 0x21;
+        break;
+    case '>':
+        return 0x3c;
+        break;
+    }
+    if (char_is_uppercase(letter))
+    {
+        return (letter + 0x20);
+    }
+    else
+    {
+        return letter;
+    }
+}
+
+static char char_to_uppercase(const char letter)
+{
+    switch (letter)
+    {
+    case '_':
+        return 0x20;
+        break;
+    case '(':
+        return 0x29;
+        break;
+    case '{':
+        return 0x7d;
+        break;
+    case '[':
+        return 0x5d;
+        break;
+    case '/':
+        return 0x5c;
+        break;
+    case ';':
+        return 0x3a;
+        break;
+    case '.':
+        return 0x2c;
+        break;
+    case '!':
+        return 0x3f;
+        break;
+    case '<':
+        return 0x3e;
+        break;
+    }
+    if (char_is_lowercase(letter))
+    {
+        return (letter - 0x20);
+    }
+    else
+    {
+        return letter;
+    }
+}
+
+static void uart_text_input_backspace_cb(UART_TextInputModel *model)
+{
+    uint8_t text_length = model->clear_default_text ? 1 : strlen(model->text_buffer);
+    if (text_length > 0)
+    {
+        model->text_buffer[text_length - 1] = 0;
+    }
+}
+
+static void uart_text_input_view_draw_callback(Canvas *canvas, void *_model)
+{
+    UART_TextInputModel *model = _model;
+    // uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0;
+    uint8_t needed_string_width = canvas_width(canvas) - 8;
+    uint8_t start_pos = 4;
+
+    const char *text = model->text_buffer;
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_draw_str(canvas, 2, 7, model->header);
+    elements_slightly_rounded_frame(canvas, 1, 8, 126, 12);
+
+    if (canvas_string_width(canvas, text) > needed_string_width)
+    {
+        canvas_draw_str(canvas, start_pos, 17, "...");
+        start_pos += 6;
+        needed_string_width -= 8;
+    }
+
+    while (text != 0 && canvas_string_width(canvas, text) > needed_string_width)
+    {
+        text++;
+    }
+
+    if (model->clear_default_text)
+    {
+        elements_slightly_rounded_box(
+            canvas, start_pos - 1, 14, canvas_string_width(canvas, text) + 2, 10);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    else
+    {
+        canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 1, 18, "|");
+        canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 2, 18, "|");
+    }
+    canvas_draw_str(canvas, start_pos, 17, text);
+
+    canvas_set_font(canvas, FontKeyboard);
+
+    for (uint8_t row = 0; row <= keyboard_row_count; row++)
+    {
+        const uint8_t column_count = get_row_size(row);
+        const UART_TextInputKey *keys = get_row(row);
+
+        for (size_t column = 0; column < column_count; column++)
+        {
+            if (keys[column].text == ENTER_KEY)
+            {
+                canvas_set_color(canvas, ColorBlack);
+                if (model->selected_row == row && model->selected_column == column)
+                {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySaveSelected_24x11);
+                }
+                else
+                {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySave_24x11);
+                }
+            }
+            else if (keys[column].text == BACKSPACE_KEY)
+            {
+                canvas_set_color(canvas, ColorBlack);
+                if (model->selected_row == row && model->selected_column == column)
+                {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspaceSelected_16x9);
+                }
+                else
+                {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspace_16x9);
+                }
+            }
+            else
+            {
+                if (model->selected_row == row && model->selected_column == column)
+                {
+                    canvas_set_color(canvas, ColorBlack);
+                    canvas_draw_box(
+                        canvas,
+                        keyboard_origin_x + keys[column].x - 1,
+                        keyboard_origin_y + keys[column].y - 8,
+                        7,
+                        10);
+                    canvas_set_color(canvas, ColorWhite);
+                }
+                else
+                {
+                    canvas_set_color(canvas, ColorBlack);
+                }
+                if (0 == strcmp(model->header, mode_AT))
+                {
+                    canvas_draw_glyph(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        char_to_uppercase(keys[column].text));
+                }
+                else
+                {
+                    canvas_draw_glyph(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        keys[column].text);
+                }
+            }
+        }
+    }
+    if (model->valadator_message_visible)
+    {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_set_color(canvas, ColorWhite);
+        canvas_draw_box(canvas, 8, 10, 110, 48);
+        canvas_set_color(canvas, ColorBlack);
+        canvas_draw_icon(canvas, 10, 14, &I_WarningDolphin_45x42);
+        canvas_draw_rframe(canvas, 8, 8, 112, 50, 3);
+        canvas_draw_rframe(canvas, 9, 9, 110, 48, 2);
+        elements_multiline_text(canvas, 62, 20, furi_string_get_cstr(model->validator_text));
+        canvas_set_font(canvas, FontKeyboard);
+    }
+}
+
+static void
+uart_text_input_handle_up(UART_TextInput *uart_text_input, UART_TextInputModel *model)
+{
+    UNUSED(uart_text_input);
+    if (model->selected_row > 0)
+    {
+        model->selected_row--;
+        if (model->selected_column > get_row_size(model->selected_row) - 6)
+        {
+            model->selected_column = model->selected_column + 1;
+        }
+    }
+}
+
+static void
+uart_text_input_handle_down(UART_TextInput *uart_text_input, UART_TextInputModel *model)
+{
+    UNUSED(uart_text_input);
+    if (model->selected_row < keyboard_row_count - 1)
+    {
+        model->selected_row++;
+        if (model->selected_column > get_row_size(model->selected_row) - 4)
+        {
+            model->selected_column = model->selected_column - 1;
+        }
+    }
+}
+
+static void
+uart_text_input_handle_left(UART_TextInput *uart_text_input, UART_TextInputModel *model)
+{
+    UNUSED(uart_text_input);
+    if (model->selected_column > 0)
+    {
+        model->selected_column--;
+    }
+    else
+    {
+        model->selected_column = get_row_size(model->selected_row) - 1;
+    }
+}
+
+static void
+uart_text_input_handle_right(UART_TextInput *uart_text_input, UART_TextInputModel *model)
+{
+    UNUSED(uart_text_input);
+    if (model->selected_column < get_row_size(model->selected_row) - 1)
+    {
+        model->selected_column++;
+    }
+    else
+    {
+        model->selected_column = 0;
+    }
+}
+
+static void uart_text_input_handle_ok(
+    UART_TextInput *uart_text_input,
+    UART_TextInputModel *model,
+    bool shift)
+{
+    char selected = get_selected_char(model);
+    uint8_t text_length = strlen(model->text_buffer);
+
+    if (0 == strcmp(model->header, mode_AT))
+    {
+        selected = char_to_uppercase(selected);
+    }
+
+    if (shift)
+    {
+        if (0 == strcmp(model->header, mode_AT))
+        {
+            selected = char_to_lowercase(selected);
+        }
+        else
+        {
+            selected = char_to_uppercase(selected);
+        }
+    }
+
+    if (selected == ENTER_KEY)
+    {
+        if (model->validator_callback &&
+            (!model->validator_callback(
+                model->text_buffer, model->validator_text, model->validator_callback_context)))
+        {
+            model->valadator_message_visible = true;
+            furi_timer_start(uart_text_input->timer, furi_kernel_get_tick_frequency() * 4);
+        }
+        else if (model->callback != 0 && text_length > 0)
+        {
+            model->callback(model->callback_context);
+        }
+    }
+    else if (selected == BACKSPACE_KEY)
+    {
+        uart_text_input_backspace_cb(model);
+    }
+    else
+    {
+        if (model->clear_default_text)
+        {
+            text_length = 0;
+        }
+        if (text_length < (model->text_buffer_size - 1))
+        {
+            model->text_buffer[text_length] = selected;
+            model->text_buffer[text_length + 1] = 0;
+        }
+    }
+    model->clear_default_text = false;
+}
+
+static bool uart_text_input_view_input_callback(InputEvent *event, void *context)
+{
+    UART_TextInput *uart_text_input = context;
+    furi_assert(uart_text_input);
+
+    bool consumed = false;
+
+    // Acquire model
+    UART_TextInputModel *model = view_get_model(uart_text_input->view);
+
+    if ((!(event->type == InputTypePress) && !(event->type == InputTypeRelease)) &&
+        model->valadator_message_visible)
+    {
+        model->valadator_message_visible = false;
+        consumed = true;
+    }
+    else if (event->type == InputTypeShort)
+    {
+        consumed = true;
+        switch (event->key)
+        {
+        case InputKeyUp:
+            uart_text_input_handle_up(uart_text_input, model);
+            break;
+        case InputKeyDown:
+            uart_text_input_handle_down(uart_text_input, model);
+            break;
+        case InputKeyLeft:
+            uart_text_input_handle_left(uart_text_input, model);
+            break;
+        case InputKeyRight:
+            uart_text_input_handle_right(uart_text_input, model);
+            break;
+        case InputKeyOk:
+            uart_text_input_handle_ok(uart_text_input, model, false);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+    else if (event->type == InputTypeLong)
+    {
+        consumed = true;
+        switch (event->key)
+        {
+        case InputKeyUp:
+            uart_text_input_handle_up(uart_text_input, model);
+            break;
+        case InputKeyDown:
+            uart_text_input_handle_down(uart_text_input, model);
+            break;
+        case InputKeyLeft:
+            uart_text_input_handle_left(uart_text_input, model);
+            break;
+        case InputKeyRight:
+            uart_text_input_handle_right(uart_text_input, model);
+            break;
+        case InputKeyOk:
+            uart_text_input_handle_ok(uart_text_input, model, true);
+            break;
+        case InputKeyBack:
+            uart_text_input_backspace_cb(model);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+    else if (event->type == InputTypeRepeat)
+    {
+        consumed = true;
+        switch (event->key)
+        {
+        case InputKeyUp:
+            uart_text_input_handle_up(uart_text_input, model);
+            break;
+        case InputKeyDown:
+            uart_text_input_handle_down(uart_text_input, model);
+            break;
+        case InputKeyLeft:
+            uart_text_input_handle_left(uart_text_input, model);
+            break;
+        case InputKeyRight:
+            uart_text_input_handle_right(uart_text_input, model);
+            break;
+        case InputKeyBack:
+            uart_text_input_backspace_cb(model);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+
+    // Commit model
+    view_commit_model(uart_text_input->view, consumed);
+
+    return consumed;
+}
+
+void uart_text_input_timer_callback(void *context)
+{
+    furi_assert(context);
+    UART_TextInput *uart_text_input = context;
+
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { model->valadator_message_visible = false; },
+        true);
+}
+
+UART_TextInput *uart_text_input_alloc()
+{
+    UART_TextInput *uart_text_input = malloc(sizeof(UART_TextInput));
+    uart_text_input->view = view_alloc();
+    view_set_context(uart_text_input->view, uart_text_input);
+    view_allocate_model(uart_text_input->view, ViewModelTypeLocking, sizeof(UART_TextInputModel));
+    view_set_draw_callback(uart_text_input->view, uart_text_input_view_draw_callback);
+    view_set_input_callback(uart_text_input->view, uart_text_input_view_input_callback);
+
+    uart_text_input->timer =
+        furi_timer_alloc(uart_text_input_timer_callback, FuriTimerTypeOnce, uart_text_input);
+
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { model->validator_text = furi_string_alloc(); },
+        false);
+
+    uart_text_input_reset(uart_text_input);
+
+    return uart_text_input;
+}
+
+void uart_text_input_free(UART_TextInput *uart_text_input)
+{
+    furi_assert(uart_text_input);
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { furi_string_free(model->validator_text); },
+        false);
+
+    // Send stop command
+    furi_timer_stop(uart_text_input->timer);
+    // Release allocated memory
+    furi_timer_free(uart_text_input->timer);
+
+    view_free(uart_text_input->view);
+
+    free(uart_text_input);
+}
+
+void uart_text_input_reset(UART_TextInput *uart_text_input)
+{
+    furi_assert(uart_text_input);
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        {
+            model->text_buffer_size = 0;
+            model->header = "";
+            model->selected_row = 0;
+            model->selected_column = 0;
+            model->clear_default_text = false;
+            model->text_buffer = NULL;
+            model->text_buffer_size = 0;
+            model->callback = NULL;
+            model->callback_context = NULL;
+            model->validator_callback = NULL;
+            model->validator_callback_context = NULL;
+            furi_string_reset(model->validator_text);
+            model->valadator_message_visible = false;
+        },
+        true);
+}
+
+View *uart_text_input_get_view(UART_TextInput *uart_text_input)
+{
+    furi_assert(uart_text_input);
+    return uart_text_input->view;
+}
+
+void uart_text_input_set_result_callback(
+    UART_TextInput *uart_text_input,
+    UART_TextInputCallback callback,
+    void *callback_context,
+    char *text_buffer,
+    size_t text_buffer_size,
+    bool clear_default_text)
+{
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        {
+            model->callback = callback;
+            model->callback_context = callback_context;
+            model->text_buffer = text_buffer;
+            model->text_buffer_size = text_buffer_size;
+            model->clear_default_text = clear_default_text;
+            if (text_buffer && text_buffer[0] != '\0')
+            {
+                // Set focus on Save
+                model->selected_row = 2;
+                model->selected_column = 8;
+            }
+        },
+        true);
+}
+
+void uart_text_input_set_validator(
+    UART_TextInput *uart_text_input,
+    UART_TextInputValidatorCallback callback,
+    void *callback_context)
+{
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        {
+            model->validator_callback = callback;
+            model->validator_callback_context = callback_context;
+        },
+        true);
+}
+
+UART_TextInputValidatorCallback
+uart_text_input_get_validator_callback(UART_TextInput *uart_text_input)
+{
+    UART_TextInputValidatorCallback validator_callback = NULL;
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { validator_callback = model->validator_callback; },
+        false);
+    return validator_callback;
+}
+
+void *uart_text_input_get_validator_callback_context(UART_TextInput *uart_text_input)
+{
+    void *validator_callback_context = NULL;
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { validator_callback_context = model->validator_callback_context; },
+        false);
+    return validator_callback_context;
+}
+
+void uart_text_input_set_header_text(UART_TextInput *uart_text_input, const char *text)
+{
+    with_view_model(
+        uart_text_input->view, UART_TextInputModel * model, { model->header = text; }, true);
+}
+
+#endif // UART_TEXT_INPUT_H

+ 1105 - 0
web_crawler/web_crawler_callback.h

@@ -0,0 +1,1105 @@
+// web_crawler_callback.h
+static bool sent_http_request = false;
+static bool get_success = false;
+static bool already_success = false;
+static WebCrawlerApp *app_instance = NULL;
+
+// Forward declaration of callback functions
+static void web_crawler_setting_item_path_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_headers_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_payload_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_ssid_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_password_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_file_type_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_file_rename_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_file_delete_clicked(void *context, uint32_t index);
+static void web_crawler_setting_item_file_read_clicked(void *context, uint32_t index);
+
+static void web_crawler_http_method_change(VariableItem *item)
+{
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, http_method_names[index]);
+
+    // save the http method
+    if (app_instance)
+    {
+        strncpy(app_instance->http_method, http_method_names[index], strlen(http_method_names[index]) + 1);
+
+        // save the settings
+        save_settings(
+            app_instance->path,
+            app_instance->ssid,
+            app_instance->password,
+            app_instance->file_rename,
+            app_instance->file_type,
+            app_instance->http_method,
+            app_instance->headers,
+            app_instance->payload);
+    }
+}
+
+static void web_crawler_view_draw_callback(Canvas *canvas, void *context)
+{
+    UNUSED(context);
+    if (!app_instance)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    if (!canvas)
+    {
+        FURI_LOG_E(TAG, "Canvas is NULL");
+        return;
+    }
+
+    canvas_clear(canvas);
+    canvas_set_font(canvas, FontSecondary);
+
+    if (fhttp.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 (app_instance->path)
+    {
+        if (!sent_http_request)
+        {
+            snprintf(
+                fhttp.file_path,
+                sizeof(fhttp.file_path),
+                STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler/received_data.txt");
+
+            fhttp.save_received_data = true;
+
+            if (strstr(app_instance->http_method, "GET") != NULL)
+            {
+                canvas_draw_str(canvas, 0, 10, "Sending GET request...");
+
+                fhttp.is_bytes_request = false;
+
+                // Perform GET request and handle the response
+                if (app_instance->headers == NULL || app_instance->headers[0] == '\0' || strstr(app_instance->headers, " ") == NULL)
+                {
+                    get_success = flipper_http_get_request(app_instance->path);
+                }
+                else
+                {
+                    get_success = flipper_http_get_request_with_headers(app_instance->path, app_instance->headers);
+                }
+            }
+            else if (strstr(app_instance->http_method, "POST") != NULL)
+            {
+                canvas_draw_str(canvas, 0, 10, "Sending POST request...");
+
+                fhttp.is_bytes_request = false;
+
+                // Perform POST request and handle the response
+                get_success = flipper_http_post_request_with_headers(app_instance->path, app_instance->headers, app_instance->payload);
+            }
+            else if (strstr(app_instance->http_method, "PUT") != NULL)
+            {
+                canvas_draw_str(canvas, 0, 10, "Sending PUT request...");
+
+                fhttp.is_bytes_request = false;
+
+                // Perform PUT request and handle the response
+                get_success = flipper_http_put_request_with_headers(app_instance->path, app_instance->headers, app_instance->payload);
+            }
+            else if (strstr(app_instance->http_method, "DELETE") != NULL)
+            {
+                canvas_draw_str(canvas, 0, 10, "Sending DELETE request...");
+
+                fhttp.is_bytes_request = false;
+
+                // Perform DELETE request and handle the response
+                get_success = flipper_http_delete_request_with_headers(app_instance->path, app_instance->headers, app_instance->payload);
+            }
+            else
+            {
+                // download file
+                canvas_draw_str(canvas, 0, 10, "Downloading file...");
+
+                fhttp.is_bytes_request = true;
+
+                // Perform GET request and handle the response
+                get_success = flipper_http_get_request_bytes(app_instance->path, app_instance->headers);
+            }
+
+            canvas_draw_str(canvas, 0, 20, "Sent!");
+
+            if (get_success)
+            {
+                canvas_draw_str(canvas, 0, 30, "Receiving data...");
+                // Set the state to RECEIVING to ensure we continue to see the receiving message
+                fhttp.state = RECEIVING;
+            }
+            else
+            {
+                canvas_draw_str(canvas, 0, 30, "Failed.");
+            }
+
+            sent_http_request = true;
+        }
+        else
+        {
+            // print state
+            if (get_success && fhttp.state == RECEIVING)
+            {
+                canvas_draw_str(canvas, 0, 10, "Receiving and parsing data...");
+            }
+            else if (get_success && fhttp.state == IDLE)
+            {
+                canvas_draw_str(canvas, 0, 10, "Data saved to file.");
+                canvas_draw_str(canvas, 0, 20, "Press BACK to return.");
+            }
+            else
+            {
+                if (fhttp.state == ISSUE)
+                {
+                    if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
+                    {
+                        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_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] GET request failed with error: connection refused") != NULL)
+                    {
+                        canvas_draw_str(canvas, 0, 10, "[ERROR] Connection refused.");
+                        canvas_draw_str(canvas, 0, 50, "Choose another URL.");
+                        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+                    }
+                    else
+                    {
+                        canvas_draw_str(canvas, 0, 10, "[ERROR] Failed to sync data.");
+                        canvas_draw_str(canvas, 0, 30, "If this is your third attempt,");
+                        canvas_draw_str(canvas, 0, 40, "it's likely your URL is not");
+                        canvas_draw_str(canvas, 0, 50, "compabilbe or correct.");
+                        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+                    }
+                }
+                else
+                {
+                    canvas_draw_str(canvas, 0, 10, "HTTP request failed.");
+                    canvas_draw_str(canvas, 0, 20, "Press BACK to return.");
+                }
+                get_success = false;
+            }
+        }
+    }
+    else
+    {
+        canvas_draw_str(canvas, 0, 10, "URL not set.");
+    }
+}
+
+/**
+ * @brief      Navigation callback to handle exiting from other views to the submenu.
+ * @param      context   The context - WebCrawlerApp object.
+ * @return     WebCrawlerViewSubmenu
+ */
+static uint32_t web_crawler_back_to_configure_callback(void *context)
+{
+    UNUSED(context);
+    // free file read widget if it exists
+    if (app_instance->widget_file_read)
+    {
+        widget_reset(app_instance->widget_file_read);
+    }
+    return WebCrawlerViewSubmenuConfig; // Return to the configure screen
+}
+
+/**
+ * @brief      Navigation callback to handle returning to the Wifi Settings screen.
+ * @param      context   The context - WebCrawlerApp object.
+ * @return     WebCrawlerViewSubmenu
+ */
+static uint32_t web_crawler_back_to_main_callback(void *context)
+{
+    UNUSED(context);
+    // reset GET request flags
+    sent_http_request = false;
+    get_success = false;
+    already_success = false;
+    // free file read widget if it exists
+    if (app_instance->widget_file_read)
+    {
+        widget_reset(app_instance->widget_file_read);
+    }
+    return WebCrawlerViewSubmenuMain; // Return to the main submenu
+}
+
+static uint32_t web_crawler_back_to_file_callback(void *context)
+{
+    UNUSED(context);
+    return WebCrawlerViewVariableItemListFile; // Return to the file submenu
+}
+
+static uint32_t web_crawler_back_to_wifi_callback(void *context)
+{
+    UNUSED(context);
+    return WebCrawlerViewVariableItemListWifi; // Return to the wifi submenu
+}
+
+static uint32_t web_crawler_back_to_request_callback(void *context)
+{
+    UNUSED(context);
+    return WebCrawlerViewVariableItemListRequest; // Return to the request submenu
+}
+
+/**
+ * @brief      Navigation callback to handle exiting the app from the main submenu.
+ * @param      context   The context - unused
+ * @return     VIEW_NONE to exit the app
+ */
+static uint32_t web_crawler_exit_app_callback(void *context)
+{
+    UNUSED(context);
+    return VIEW_NONE;
+}
+
+/**
+ * @brief      Handle submenu item selection.
+ * @param      context   The context - WebCrawlerApp object.
+ * @param      index     The WebCrawlerSubmenuIndex item that was clicked.
+ */
+static void web_crawler_submenu_callback(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+
+    if (app->view_dispatcher)
+    {
+        switch (index)
+        {
+        case WebCrawlerSubmenuIndexRun:
+            sent_http_request = false; // Reset the flag
+            view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewRun);
+            break;
+        case WebCrawlerSubmenuIndexAbout:
+            view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewAbout);
+            break;
+        case WebCrawlerSubmenuIndexConfig:
+            view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewSubmenuConfig);
+            break;
+        case WebCrawlerSubmenuIndexWifi:
+            view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListWifi);
+            break;
+        case WebCrawlerSubmenuIndexRequest:
+            view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListRequest);
+            break;
+        case WebCrawlerSubmenuIndexFile:
+            view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListFile);
+            break;
+        default:
+            FURI_LOG_E(TAG, "Unknown submenu index");
+            break;
+        }
+    }
+}
+
+/**
+ * @brief      Configuration enter callback to handle different items.
+ * @param      context   The context - WebCrawlerApp object.
+ * @param      index     The index of the item that was clicked.
+ */
+static void web_crawler_wifi_enter_callback(void *context, uint32_t index)
+{
+    switch (index)
+    {
+    case 0: // SSID
+        web_crawler_setting_item_ssid_clicked(context, index);
+        break;
+    case 1: // Password
+        web_crawler_setting_item_password_clicked(context, index);
+        break;
+    default:
+        FURI_LOG_E(TAG, "Unknown configuration item index");
+        break;
+    }
+}
+
+/**
+ * @brief      Configuration enter callback to handle different items.
+ * @param      context   The context - WebCrawlerApp object.
+ * @param      index     The index of the item that was clicked.
+ */
+static void web_crawler_file_enter_callback(void *context, uint32_t index)
+{
+    switch (index)
+    {
+    case 0: // File Read
+        web_crawler_setting_item_file_read_clicked(context, index);
+        break;
+    case 1: // FIle Type
+        web_crawler_setting_item_file_type_clicked(context, index);
+        break;
+    case 2: // File Rename
+        web_crawler_setting_item_file_rename_clicked(context, index);
+        break;
+    case 3: // File Delete
+        web_crawler_setting_item_file_delete_clicked(context, index);
+        break;
+    default:
+        FURI_LOG_E(TAG, "Unknown configuration item index");
+        break;
+    }
+}
+
+/**
+ * @brief      Configuration enter callback to handle different items.
+ * @param      context   The context - WebCrawlerApp object.
+ * @param      index     The index of the item that was clicked.
+ */
+static void web_crawler_request_enter_callback(void *context, uint32_t index)
+{
+    switch (index)
+    {
+    case 0: // URL
+        web_crawler_setting_item_path_clicked(context, index);
+        break;
+    case 1:
+        // HTTP Method
+        break;
+    case 2:
+        // Headers
+        web_crawler_setting_item_headers_clicked(context, index);
+        break;
+    case 3:
+        // Payload
+        web_crawler_setting_item_payload_clicked(context, index);
+        break;
+    default:
+        FURI_LOG_E(TAG, "Unknown configuration item index");
+        break;
+    }
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the URL.
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_path_updated(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    if (!app->path || !app->temp_buffer_path || !app->temp_buffer_size_path || !app->path_item)
+    {
+        FURI_LOG_E(TAG, "Invalid path buffer");
+        return;
+    }
+    // Store the entered URL from temp_buffer_path to path
+    strncpy(app->path, app->temp_buffer_path, app->temp_buffer_size_path - 1);
+
+    if (app->path_item)
+    {
+        variable_item_set_current_value_text(app->path_item, app->path);
+
+        // Save the URL to the settings
+        save_settings(app->path, app->ssid, app->password, app->file_rename, app->file_type, app->http_method, app->headers, app->payload);
+    }
+
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListRequest);
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the headers
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_headers_updated(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    if (!app->temp_buffer_headers || !app->temp_buffer_size_headers || !app->headers_item)
+    {
+        FURI_LOG_E(TAG, "Invalid headers buffer");
+        return;
+    }
+    // Store the entered headers from temp_buffer_headers to headers
+    strncpy(app->headers, app->temp_buffer_headers, app->temp_buffer_size_headers - 1);
+
+    if (app->headers_item)
+    {
+        variable_item_set_current_value_text(app->headers_item, app->headers);
+
+        // Save the headers to the settings
+        save_settings(app->path, app->ssid, app->password, app->file_rename, app->file_type, app->http_method, app->headers, app->payload);
+    }
+
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListRequest);
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the payload.
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_payload_updated(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    if (!app->temp_buffer_payload || !app->temp_buffer_size_payload || !app->payload_item)
+    {
+        FURI_LOG_E(TAG, "Invalid payload buffer");
+        return;
+    }
+    // Store the entered payload from temp_buffer_payload to payload
+    strncpy(app->payload, app->temp_buffer_payload, app->temp_buffer_size_payload - 1);
+
+    if (app->payload_item)
+    {
+        variable_item_set_current_value_text(app->payload_item, app->payload);
+
+        // Save the payload to the settings
+        save_settings(app->path, app->ssid, app->password, app->file_rename, app->file_type, app->http_method, app->headers, app->payload);
+    }
+
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListRequest);
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the SSID.
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_ssid_updated(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    if (!app->temp_buffer_ssid || !app->temp_buffer_size_ssid || !app->ssid || !app->ssid_item)
+    {
+        FURI_LOG_E(TAG, "Invalid SSID buffer");
+        return;
+    }
+    // Store the entered SSID from temp_buffer_ssid to ssid
+    strncpy(app->ssid, app->temp_buffer_ssid, app->temp_buffer_size_ssid - 1);
+
+    if (app->ssid_item)
+    {
+        variable_item_set_current_value_text(app->ssid_item, app->ssid);
+
+        // Save the SSID to the settings
+        save_settings(app->path, app->ssid, app->password, app->file_rename, app->file_type, app->http_method, app->headers, app->payload);
+
+        // send to UART
+        if (!flipper_http_save_wifi(app->ssid, app->password))
+        {
+            FURI_LOG_E(TAG, "Failed to save wifi settings via UART");
+            FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board");
+        }
+    }
+
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListWifi);
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the Password.
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_password_update(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    if (!app->temp_buffer_password || !app->temp_buffer_size_password || !app->password || !app->password_item)
+    {
+        FURI_LOG_E(TAG, "Invalid password buffer");
+        return;
+    }
+    // Store the entered Password from temp_buffer_password to password
+    strncpy(app->password, app->temp_buffer_password, app->temp_buffer_size_password - 1);
+
+    if (app->password_item)
+    {
+        variable_item_set_current_value_text(app->password_item, app->password);
+
+        // Save the Password to the settings
+        save_settings(app->path, app->ssid, app->password, app->file_rename, app->file_type, app->http_method, app->headers, app->payload);
+
+        // send to UART
+        if (!flipper_http_save_wifi(app->ssid, app->password))
+        {
+            FURI_LOG_E(TAG, "Failed to save wifi settings via UART");
+            FURI_LOG_E(TAG, "Make sure the Flipper is connected to the Wifi Dev Board");
+        }
+    }
+
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListWifi);
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the File Type.
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_file_type_update(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    if (!app->temp_buffer_file_type || !app->temp_buffer_size_file_type || !app->file_type || !app->file_type_item)
+    {
+        FURI_LOG_E(TAG, "Invalid file type buffer");
+        return;
+    }
+    // Temporary buffer to store the old name
+    char old_file_type[256];
+
+    strncpy(old_file_type, app->file_type, sizeof(old_file_type) - 1);
+    old_file_type[sizeof(old_file_type) - 1] = '\0'; // Null-terminate
+    strncpy(app->file_type, app->temp_buffer_file_type, app->temp_buffer_size_file_type - 1);
+
+    if (app->file_type_item)
+    {
+        variable_item_set_current_value_text(app->file_type_item, app->file_type);
+
+        // Save the File Type to the settings
+        save_settings(app->path, app->ssid, app->password, app->file_rename, app->file_type, app->http_method, app->headers, app->payload);
+    }
+
+    rename_received_data(app->file_rename, app->file_rename, app->file_type, old_file_type);
+
+    // set the file path for fhttp.file_path
+    if (app->file_rename && app->file_type)
+    {
+        char file_path[256];
+        snprintf(file_path, sizeof(file_path), "%s%s%s", RECEIVED_DATA_PATH, app->file_rename, app->file_type);
+        file_path[sizeof(file_path) - 1] = '\0'; // Null-terminate
+        strncpy(fhttp.file_path, file_path, sizeof(fhttp.file_path) - 1);
+        fhttp.file_path[sizeof(fhttp.file_path) - 1] = '\0'; // Null-terminate
+    }
+
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListFile);
+}
+
+/**
+ * @brief      Callback for when the user finishes entering the File Rename.
+ * @param      context   The context - WebCrawlerApp object.
+ */
+static void web_crawler_set_file_rename_update(void *context)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    if (!app->temp_buffer_file_rename || !app->temp_buffer_size_file_rename || !app->file_rename || !app->file_rename_item)
+    {
+        FURI_LOG_E(TAG, "Invalid file rename buffer");
+        return;
+    }
+
+    // Temporary buffer to store the old name
+    char old_name[256];
+
+    // Ensure that app->file_rename is null-terminated
+    strncpy(old_name, app->file_rename, sizeof(old_name) - 1);
+    old_name[sizeof(old_name) - 1] = '\0'; // Null-terminate
+
+    // Store the entered File Rename from temp_buffer_file_rename to file_rename
+    strncpy(app->file_rename, app->temp_buffer_file_rename, app->temp_buffer_size_file_rename - 1);
+
+    if (app->file_rename_item)
+    {
+        variable_item_set_current_value_text(app->file_rename_item, app->file_rename);
+
+        // Save the File Rename to the settings
+        save_settings(app->path, app->ssid, app->password, app->file_rename, app->file_type, app->http_method, app->headers, app->payload);
+    }
+
+    rename_received_data(old_name, app->file_rename, app->file_type, app->file_type);
+
+    // set the file path for fhttp.file_path
+    if (app->file_rename && app->file_type)
+    {
+        char file_path[256];
+        snprintf(file_path, sizeof(file_path), "%s%s%s", RECEIVED_DATA_PATH, app->file_rename, app->file_type);
+        file_path[sizeof(file_path) - 1] = '\0'; // Null-terminate
+        strncpy(fhttp.file_path, file_path, sizeof(fhttp.file_path) - 1);
+        fhttp.file_path[sizeof(fhttp.file_path) - 1] = '\0'; // Null-terminate
+    }
+
+    // Return to the Configure view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewVariableItemListFile);
+}
+
+/**
+ * @brief      Handler for Path configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_path_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    if (!app->text_input_path)
+    {
+        FURI_LOG_E(TAG, "Text input is NULL");
+        return;
+    }
+
+    UNUSED(index);
+
+    // Initialize temp_buffer with existing path
+    if (app->path && strlen(app->path) > 0)
+    {
+        strncpy(app->temp_buffer_path, app->path, app->temp_buffer_size_path - 1);
+    }
+    else
+    {
+        strncpy(app->temp_buffer_path, "https://httpbin.org/get", app->temp_buffer_size_path - 1);
+    }
+
+    app->temp_buffer_path[app->temp_buffer_size_path - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    uart_text_input_set_result_callback(
+        app->text_input_path,
+        web_crawler_set_path_updated,
+        app,
+        app->temp_buffer_path,
+        app->temp_buffer_size_path,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        uart_text_input_get_view(app->text_input_path),
+        web_crawler_back_to_request_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInput);
+}
+
+/**
+ * @brief      Handler for headers configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_headers_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    UNUSED(index);
+    if (!app->text_input_headers)
+    {
+        FURI_LOG_E(TAG, "Text input is NULL");
+        return;
+    }
+    if (!app->headers)
+    {
+        FURI_LOG_E(TAG, "Headers is NULL");
+        return;
+    }
+    if (!app->temp_buffer_headers)
+    {
+        FURI_LOG_E(TAG, "Temp buffer headers is NULL");
+        return;
+    }
+
+    // Initialize temp_buffer with existing headers
+    if (app->headers && strlen(app->headers) > 0)
+    {
+        strncpy(app->temp_buffer_headers, app->headers, app->temp_buffer_size_headers - 1);
+    }
+    else
+    {
+        strncpy(app->temp_buffer_headers, "{\"Content-Type\":\"application/json\",\"key\":\"value\"}", app->temp_buffer_size_headers - 1);
+    }
+
+    app->temp_buffer_headers[app->temp_buffer_size_headers - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    uart_text_input_set_result_callback(
+        app->text_input_headers,
+        web_crawler_set_headers_updated,
+        app,
+        app->temp_buffer_headers,
+        app->temp_buffer_size_headers,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        uart_text_input_get_view(app->text_input_headers),
+        web_crawler_back_to_request_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputHeaders);
+}
+
+/**
+ * @brief      Handler for payload configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_payload_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    UNUSED(index);
+    if (!app->text_input_payload)
+    {
+        FURI_LOG_E(TAG, "Text input is NULL");
+        return;
+    }
+
+    // Initialize temp_buffer with existing payload
+    if (app->payload && strlen(app->payload) > 0)
+    {
+        strncpy(app->temp_buffer_payload, app->payload, app->temp_buffer_size_payload - 1);
+    }
+    else
+    {
+        strncpy(app->temp_buffer_payload, "{\"key\":\"value\"}", app->temp_buffer_size_payload - 1);
+    }
+
+    app->temp_buffer_payload[app->temp_buffer_size_payload - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    uart_text_input_set_result_callback(
+        app->text_input_payload,
+        web_crawler_set_payload_updated,
+        app,
+        app->temp_buffer_payload,
+        app->temp_buffer_size_payload,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        uart_text_input_get_view(app->text_input_payload),
+        web_crawler_back_to_request_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputPayload);
+}
+
+/**
+ * @brief      Handler for SSID configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_ssid_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    UNUSED(index);
+    if (!app->text_input_ssid)
+    {
+        FURI_LOG_E(TAG, "Text input is NULL");
+        return;
+    }
+
+    // Initialize temp_buffer with existing SSID
+    if (app->ssid && strlen(app->ssid) > 0)
+    {
+        strncpy(app->temp_buffer_ssid, app->ssid, app->temp_buffer_size_ssid - 1);
+    }
+    else
+    {
+        strncpy(app->temp_buffer_ssid, "", app->temp_buffer_size_ssid - 1);
+    }
+
+    app->temp_buffer_ssid[app->temp_buffer_size_ssid - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    uart_text_input_set_result_callback(
+        app->text_input_ssid,
+        web_crawler_set_ssid_updated,
+        app,
+        app->temp_buffer_ssid,
+        app->temp_buffer_size_ssid,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        uart_text_input_get_view(app->text_input_ssid),
+        web_crawler_back_to_wifi_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputSSID);
+}
+
+/**
+ * @brief      Handler for Password configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_password_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    UNUSED(index);
+    if (!app->text_input_password)
+    {
+        FURI_LOG_E(TAG, "Text input is NULL");
+        return;
+    }
+
+    // Initialize temp_buffer with existing password
+    strncpy(app->temp_buffer_password, app->password, app->temp_buffer_size_password - 1);
+    app->temp_buffer_password[app->temp_buffer_size_password - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    uart_text_input_set_result_callback(
+        app->text_input_password,
+        web_crawler_set_password_update,
+        app,
+        app->temp_buffer_password,
+        app->temp_buffer_size_password,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        uart_text_input_get_view(app->text_input_password),
+        web_crawler_back_to_wifi_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputPassword);
+}
+
+/**
+ * @brief      Handler for File Type configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_file_type_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    UNUSED(index);
+    if (!app->text_input_file_type)
+    {
+        FURI_LOG_E(TAG, "Text input is NULL");
+        return;
+    }
+
+    // Initialize temp_buffer with existing file_type
+    if (app->file_type && strlen(app->file_type) > 0)
+    {
+        strncpy(app->temp_buffer_file_type, app->file_type, app->temp_buffer_size_file_type - 1);
+    }
+    else
+    {
+        strncpy(app->temp_buffer_file_type, ".txt", app->temp_buffer_size_file_type - 1);
+    }
+
+    app->temp_buffer_file_type[app->temp_buffer_size_file_type - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    uart_text_input_set_result_callback(
+        app->text_input_file_type,
+        web_crawler_set_file_type_update,
+        app,
+        app->temp_buffer_file_type,
+        app->temp_buffer_size_file_type,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        uart_text_input_get_view(app->text_input_file_type),
+        web_crawler_back_to_file_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputFileType);
+}
+
+/**
+ * @brief      Handler for File Rename configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_file_rename_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    UNUSED(index);
+    if (!app->text_input_file_rename)
+    {
+        FURI_LOG_E(TAG, "Text input is NULL");
+        return;
+    }
+
+    // Initialize temp_buffer with existing file_rename
+    if (app->file_rename && strlen(app->file_rename) > 0)
+    {
+        strncpy(app->temp_buffer_file_rename, app->file_rename, app->temp_buffer_size_file_rename - 1);
+    }
+    else
+    {
+        strncpy(app->temp_buffer_file_rename, "received_data", app->temp_buffer_size_file_rename - 1);
+    }
+
+    app->temp_buffer_file_rename[app->temp_buffer_size_file_rename - 1] = '\0';
+
+    // Configure the text input
+    bool clear_previous_text = false;
+    uart_text_input_set_result_callback(
+        app->text_input_file_rename,
+        web_crawler_set_file_rename_update,
+        app,
+        app->temp_buffer_file_rename,
+        app->temp_buffer_size_file_rename,
+        clear_previous_text);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        uart_text_input_get_view(app->text_input_file_rename),
+        web_crawler_back_to_file_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewTextInputFileRename);
+}
+
+/**
+ * @brief      Handler for File Delete configuration item click.
+ * @param      context  The context - WebCrawlerApp object.
+ * @param      index    The index of the item that was clicked.
+ */
+static void web_crawler_setting_item_file_delete_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    UNUSED(index);
+
+    if (!delete_received_data(app))
+    {
+        FURI_LOG_E(TAG, "Failed to delete file");
+    }
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        widget_get_view(app->widget_file_delete),
+        web_crawler_back_to_file_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewFileDelete);
+}
+
+static void web_crawler_setting_item_file_read_clicked(void *context, uint32_t index)
+{
+    WebCrawlerApp *app = (WebCrawlerApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return;
+    }
+    UNUSED(index);
+    widget_reset(app->widget_file_read);
+    // load the received data from the saved file
+    FuriString *received_data = flipper_http_load_from_file(fhttp.file_path);
+    if (received_data == NULL)
+    {
+        FURI_LOG_E(TAG, "Failed to load received data from file.");
+        if (app->widget_file_read)
+        {
+            widget_add_text_scroll_element(
+                app->widget_file_read,
+                0,
+                0,
+                128,
+                64, "File is empty.");
+            view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewFileRead);
+        }
+        return;
+    }
+    const char *data_cstr = furi_string_get_cstr(received_data);
+    if (data_cstr == NULL)
+    {
+        FURI_LOG_E(TAG, "Failed to get C-string from FuriString.");
+        furi_string_free(received_data);
+        if (app->widget_file_read)
+        {
+            widget_add_text_scroll_element(
+                app->widget_file_read,
+                0,
+                0,
+                128,
+                64, "File is empty.");
+            view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewFileRead);
+        }
+        return;
+    }
+    widget_add_text_scroll_element(app_instance->widget_file_read, 0, 0, 128, 64, data_cstr);
+    furi_string_free(received_data);
+
+    // Set the previous callback to return to Configure screen
+    view_set_previous_callback(
+        widget_get_view(app->widget_file_read),
+        web_crawler_back_to_file_callback);
+
+    // Show text input dialog
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewFileRead);
+}

+ 114 - 0
web_crawler/web_crawler_e.h

@@ -0,0 +1,114 @@
+// web_crawler_e.h
+#ifndef WEB_CRAWLER_E
+#define WEB_CRAWLER_E
+
+#include <easy_flipper.h>
+#include <storage/storage.h>
+
+#define TAG "WebCrawler"
+static char *http_method_names[] = {"GET", "POST", "PUT", "DELETE", "DOWNLOAD"};
+
+// Define the submenu items for our WebCrawler application
+typedef enum
+{
+    WebCrawlerSubmenuIndexRun,     // click to go to Run the GET request
+    WebCrawlerSubmenuIndexAbout,   // click to go to About screen
+    WebCrawlerSubmenuIndexConfig,  // click to go to Config submenu (Wifi, File)
+    WebCrawlerSubmenuIndexRequest, // click to go to Request submenu (Set URL, HTTP Method, Headers)
+    WebCrawlerSubmenuIndexWifi,    // click to go to Wifi submenu (SSID, Password)
+    WebCrawlerSubmenuIndexFile,    // click to go to file submenu (Read, File Type, Rename, Delete)
+} WebCrawlerSubmenuIndex;
+
+typedef enum
+{
+    WebCrawlerViewRun,                     // Run the GET request
+    WebCrawlerViewAbout,                   // About screen
+    WebCrawlerViewSubmenuConfig,           // Submenu Config view for App (Wifi, File)
+    WebCrawlerViewVariableItemListRequest, // Submenu for URL (Set URL, HTTP Method, Headers)
+    WebCrawlerViewVariableItemListWifi,    // Wifi Configuration screen (Submenu for SSID, Password)
+    WebCrawlerViewVariableItemListFile,    // Submenu for File (Read, File Type, Rename, Delete)
+    WebCrawlerViewMain,                    // Main view for App
+    WebCrawlerViewSubmenuMain,             // Submenu Main view for App (Run, About, Config)
+    WebCrawlerViewTextInput,               // Text input for Path
+    WebCrawlerViewTextInputSSID,           // Text input for SSID
+    WebCrawlerViewTextInputPassword,       // Text input for Password
+    WebCrawlerViewFileRead,                // File Read
+    WebCrawlerViewTextInputFileType,       // Text input for File Type
+    WebCrawlerViewTextInputFileRename,     // Text input for File Rename
+    WebCrawlerViewTextInputHeaders,        // Text input for Headers
+    WebCrawlerViewTextInputPayload,        // Text input for Payload
+    WebCrawlerViewFileDelete,              // File Delete
+} WebCrawlerViewIndex;
+
+// Define the application structure
+typedef struct
+{
+    ViewDispatcher *view_dispatcher;
+    View *view_main;
+    View *view_run;
+    Submenu *submenu_main;
+    Submenu *submenu_config;
+    Widget *widget_about;
+
+    UART_TextInput *text_input_path;
+    UART_TextInput *text_input_ssid;
+    UART_TextInput *text_input_password;
+    UART_TextInput *text_input_file_type;
+    UART_TextInput *text_input_file_rename;
+    //
+    UART_TextInput *text_input_headers;
+    UART_TextInput *text_input_payload;
+
+    Widget *widget_file_read;
+    Widget *widget_file_delete;
+
+    VariableItemList *variable_item_list_wifi;
+    VariableItemList *variable_item_list_file;
+    VariableItemList *variable_item_list_request;
+
+    VariableItem *path_item;
+    VariableItem *ssid_item;
+    VariableItem *password_item;
+    VariableItem *file_type_item;
+    VariableItem *file_rename_item;
+    VariableItem *file_read_item;
+    VariableItem *file_delete_item;
+    //
+    VariableItem *http_method_item;
+    VariableItem *headers_item;
+    VariableItem *payload_item;
+
+    char *path;
+    char *ssid;
+    char *password;
+    char *file_type;
+    char *file_rename;
+    char *http_method;
+    char *headers;
+    char *payload;
+
+    char *temp_buffer_path;
+    uint32_t temp_buffer_size_path;
+
+    char *temp_buffer_ssid;
+    uint32_t temp_buffer_size_ssid;
+
+    char *temp_buffer_password;
+    uint32_t temp_buffer_size_password;
+
+    char *temp_buffer_file_type;
+    uint32_t temp_buffer_size_file_type;
+
+    char *temp_buffer_file_rename;
+    uint32_t temp_buffer_size_file_rename;
+
+    char *temp_buffer_http_method;
+    uint32_t temp_buffer_size_http_method;
+
+    char *temp_buffer_headers;
+    uint32_t temp_buffer_size_headers;
+
+    char *temp_buffer_payload;
+    uint32_t temp_buffer_size_payload;
+} WebCrawlerApp;
+#endif // WEB_CRAWLER_E

+ 277 - 0
web_crawler/web_crawler_free.h

@@ -0,0 +1,277 @@
+// web_crawler_free.h
+
+static void free_buffers(WebCrawlerApp *app)
+{
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Invalid app context");
+        return;
+    }
+    if (app->path)
+    {
+        free(app->path);
+        app->path = NULL;
+    }
+
+    if (app->temp_buffer_path)
+    {
+        free(app->temp_buffer_path);
+        app->temp_buffer_path = NULL;
+    }
+
+    if (app->ssid)
+    {
+        free(app->ssid);
+        app->ssid = NULL;
+    }
+
+    if (app->temp_buffer_ssid)
+    {
+        free(app->temp_buffer_ssid);
+        app->temp_buffer_ssid = NULL;
+    }
+
+    if (app->password)
+    {
+        free(app->password);
+        app->password = NULL;
+    }
+
+    if (app->temp_buffer_password)
+    {
+        free(app->temp_buffer_password);
+        app->temp_buffer_password = NULL;
+    }
+
+    if (app->file_type)
+    {
+        free(app->file_type);
+        app->file_type = NULL;
+    }
+
+    if (app->temp_buffer_file_type)
+    {
+        free(app->temp_buffer_file_type);
+        app->temp_buffer_file_type = NULL;
+    }
+
+    if (app->file_rename)
+    {
+        free(app->file_rename);
+        app->file_rename = NULL;
+    }
+
+    if (app->temp_buffer_file_rename)
+    {
+        free(app->temp_buffer_file_rename);
+        app->temp_buffer_file_rename = NULL;
+    }
+
+    if (app->temp_buffer_http_method)
+    {
+        free(app->temp_buffer_http_method);
+        app->temp_buffer_http_method = NULL;
+    }
+
+    if (app->temp_buffer_headers)
+    {
+        free(app->temp_buffer_headers);
+        app->temp_buffer_headers = NULL;
+    }
+
+    if (app->temp_buffer_payload)
+    {
+        free(app->temp_buffer_payload);
+        app->temp_buffer_payload = NULL;
+    }
+
+    if (app->http_method)
+    {
+        free(app->http_method);
+        app->http_method = NULL;
+    }
+
+    if (app->headers)
+    {
+        free(app->headers);
+        app->headers = NULL;
+    }
+
+    if (app->payload)
+    {
+        free(app->payload);
+        app->payload = NULL;
+    }
+}
+
+static void free_resources(WebCrawlerApp *app)
+{
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Invalid app context");
+        return;
+    }
+
+    free_buffers(app);
+}
+
+static void free_all(WebCrawlerApp *app, char *reason)
+{
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Invalid app context");
+        return;
+    }
+    if (reason)
+    {
+        FURI_LOG_I(TAG, reason);
+    }
+
+    if (app->view_main)
+        view_free(app->view_main);
+    if (app->submenu_main)
+        submenu_free(app->submenu_main);
+    if (app->submenu_config)
+        submenu_free(app->submenu_config);
+    if (app->variable_item_list_wifi)
+        variable_item_list_free(app->variable_item_list_wifi);
+    if (app->variable_item_list_file)
+        variable_item_list_free(app->variable_item_list_file);
+    if (app->variable_item_list_request)
+        variable_item_list_free(app->variable_item_list_request);
+    if (app->view_dispatcher)
+        view_dispatcher_free(app->view_dispatcher);
+    if (app->widget_about)
+        widget_free(app->widget_about);
+    if (app->widget_file_read)
+        widget_free(app->widget_file_read);
+    if (app->widget_file_delete)
+        widget_free(app->widget_file_delete);
+    if (app->text_input_path)
+        uart_text_input_free(app->text_input_path);
+    if (app->text_input_ssid)
+        uart_text_input_free(app->text_input_ssid);
+    if (app->text_input_password)
+        uart_text_input_free(app->text_input_password);
+    if (app->text_input_file_type)
+        uart_text_input_free(app->text_input_file_type);
+    if (app->text_input_file_rename)
+        uart_text_input_free(app->text_input_file_rename);
+    if (app->text_input_headers)
+        uart_text_input_free(app->text_input_headers);
+    if (app->text_input_payload)
+        uart_text_input_free(app->text_input_payload);
+
+    furi_record_close(RECORD_GUI);
+    free_resources(app);
+}
+/**
+ * @brief      Function to free the resources used by WebCrawlerApp.
+ * @param      app  The WebCrawlerApp object to free.
+ */
+static void web_crawler_app_free(WebCrawlerApp *app)
+{
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Invalid app context");
+        return;
+    }
+
+    if (!app->view_dispatcher)
+    {
+        FURI_LOG_E(TAG, "Invalid view dispatcher");
+        return;
+    }
+
+    // Remove and free Main view
+    if (app->view_main)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewMain);
+        view_free(app->view_main);
+    }
+    if (app->view_run)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewRun);
+        view_free(app->view_run);
+    }
+    // Deinitialize UART
+    flipper_http_deinit();
+
+    // Remove and free Submenu
+    if (app->submenu_main)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewSubmenuMain);
+        submenu_free(app->submenu_main);
+    }
+    if (app->submenu_config)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewSubmenuConfig);
+        submenu_free(app->submenu_config);
+    }
+    // Remove and free Variable Item Lists
+    if (app->variable_item_list_wifi)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewVariableItemListWifi);
+        variable_item_list_free(app->variable_item_list_wifi);
+    }
+    if (app->variable_item_list_file)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewVariableItemListFile);
+        variable_item_list_free(app->variable_item_list_file);
+    }
+    if (app->variable_item_list_request)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewVariableItemListRequest);
+        variable_item_list_free(app->variable_item_list_request);
+    }
+
+    // Remove and free Text Input views
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInput);
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputSSID);
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputPassword);
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputFileType);
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputFileRename);
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputHeaders);
+    view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewTextInputPayload);
+    if (app->text_input_path)
+        uart_text_input_free(app->text_input_path);
+    if (app->text_input_ssid)
+        uart_text_input_free(app->text_input_ssid);
+    if (app->text_input_password)
+        uart_text_input_free(app->text_input_password);
+    if (app->text_input_file_type)
+        uart_text_input_free(app->text_input_file_type);
+    if (app->text_input_file_rename)
+        uart_text_input_free(app->text_input_file_rename);
+    if (app->text_input_headers)
+        uart_text_input_free(app->text_input_headers);
+    if (app->text_input_payload)
+        uart_text_input_free(app->text_input_payload);
+
+    // Remove and free Widgets
+    if (app->widget_about)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewAbout);
+        widget_free(app->widget_about);
+    }
+    if (app->widget_file_read)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewFileRead);
+        widget_free(app->widget_file_read);
+    }
+    if (app->widget_file_delete)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewFileDelete);
+        widget_free(app->widget_file_delete);
+    }
+
+    // Free the ViewDispatcher and close GUI
+    if (app->view_dispatcher)
+        view_dispatcher_free(app->view_dispatcher);
+
+    // Free the application structure
+    if (app)
+    {
+        free(app);
+    }
+}

+ 341 - 0
web_crawler/web_crawler_i.h

@@ -0,0 +1,341 @@
+#ifndef WEB_CRAWLER_I_H
+#define WEB_CRAWLER_I_H
+
+/**
+ * @brief      Function to allocate resources for the WebCrawlerApp.
+ * @return     Pointer to the initialized WebCrawlerApp, or NULL on failure.
+ */
+WebCrawlerApp *web_crawler_app_alloc()
+{
+    // Initialize the entire structure to zero to prevent undefined behavior
+    WebCrawlerApp *app = (WebCrawlerApp *)malloc(sizeof(WebCrawlerApp));
+
+    // Open GUI
+    Gui *gui = furi_record_open(RECORD_GUI);
+
+    // Initialize UART with the correct callback
+    if (!flipper_http_init(flipper_http_rx_callback, app))
+    {
+        FURI_LOG_E(TAG, "Failed to initialize UART");
+        return NULL;
+    }
+
+    // Allocate ViewDispatcher
+    if (!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app))
+    {
+        return NULL;
+    }
+
+    // Allocate and initialize temp_buffer and path
+    app->temp_buffer_size_path = 128;
+    app->temp_buffer_size_ssid = 64;
+    app->temp_buffer_size_password = 64;
+    app->temp_buffer_size_file_type = 16;
+    app->temp_buffer_size_file_rename = 128;
+    app->temp_buffer_size_http_method = 16;
+    app->temp_buffer_size_headers = 256;
+    app->temp_buffer_size_payload = 256;
+    if (!easy_flipper_set_buffer(&app->temp_buffer_path, app->temp_buffer_size_path))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->path, app->temp_buffer_size_path))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->temp_buffer_ssid, app->temp_buffer_size_ssid))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->ssid, app->temp_buffer_size_ssid))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->temp_buffer_password, app->temp_buffer_size_password))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->password, app->temp_buffer_size_password))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->temp_buffer_file_type, app->temp_buffer_size_file_type))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->file_type, app->temp_buffer_size_file_type))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->temp_buffer_file_rename, app->temp_buffer_size_file_rename))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->file_rename, app->temp_buffer_size_file_rename))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->temp_buffer_http_method, app->temp_buffer_size_http_method))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->http_method, app->temp_buffer_size_http_method))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->temp_buffer_headers, app->temp_buffer_size_headers))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->headers, app->temp_buffer_size_headers))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->temp_buffer_payload, app->temp_buffer_size_payload))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->payload, app->temp_buffer_size_payload))
+    {
+        return NULL;
+    }
+
+    // Allocate TextInput views
+    if (!easy_flipper_set_uart_text_input(&app->text_input_path, WebCrawlerViewTextInput, "Enter URL", app->temp_buffer_path, app->temp_buffer_size_path, NULL, web_crawler_back_to_request_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_uart_text_input(&app->text_input_ssid, WebCrawlerViewTextInputSSID, "Enter SSID", app->temp_buffer_ssid, app->temp_buffer_size_ssid, NULL, web_crawler_back_to_wifi_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_uart_text_input(&app->text_input_password, WebCrawlerViewTextInputPassword, "Enter Password", app->temp_buffer_password, app->temp_buffer_size_password, NULL, web_crawler_back_to_wifi_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_uart_text_input(&app->text_input_file_type, WebCrawlerViewTextInputFileType, "Enter File Type", app->temp_buffer_file_type, app->temp_buffer_size_file_type, NULL, web_crawler_back_to_file_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_uart_text_input(&app->text_input_file_rename, WebCrawlerViewTextInputFileRename, "Enter File Rename", app->temp_buffer_file_rename, app->temp_buffer_size_file_rename, NULL, web_crawler_back_to_file_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_uart_text_input(&app->text_input_headers, WebCrawlerViewTextInputHeaders, "Enter Headers", app->temp_buffer_headers, app->temp_buffer_size_headers, NULL, web_crawler_back_to_request_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_uart_text_input(&app->text_input_payload, WebCrawlerViewTextInputPayload, "Enter Payload", app->temp_buffer_payload, app->temp_buffer_size_payload, NULL, web_crawler_back_to_request_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+
+    // Allocate VariableItemList views
+    if (!easy_flipper_set_variable_item_list(&app->variable_item_list_wifi, WebCrawlerViewVariableItemListWifi, web_crawler_wifi_enter_callback, web_crawler_back_to_configure_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_variable_item_list(&app->variable_item_list_file, WebCrawlerViewVariableItemListFile, web_crawler_file_enter_callback, web_crawler_back_to_configure_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_variable_item_list(&app->variable_item_list_request, WebCrawlerViewVariableItemListRequest, web_crawler_request_enter_callback, web_crawler_back_to_configure_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+
+    // set variable items
+    app->path_item = variable_item_list_add(app->variable_item_list_request, "Path", 0, NULL, NULL);
+    app->http_method_item = variable_item_list_add(app->variable_item_list_request, "HTTP Method", 5, web_crawler_http_method_change, app);
+    app->headers_item = variable_item_list_add(app->variable_item_list_request, "Headers", 0, NULL, NULL);
+    app->payload_item = variable_item_list_add(app->variable_item_list_request, "Payload", 0, NULL, NULL);
+    //
+    app->ssid_item = variable_item_list_add(app->variable_item_list_wifi, "SSID", 0, NULL, NULL);         // index 0
+    app->password_item = variable_item_list_add(app->variable_item_list_wifi, "Password", 0, NULL, NULL); // index 1
+    //
+    app->file_read_item = variable_item_list_add(app->variable_item_list_file, "Read File", 0, NULL, NULL);     // index 0
+    app->file_type_item = variable_item_list_add(app->variable_item_list_file, "Set File Type", 0, NULL, NULL); // index 1
+    app->file_rename_item = variable_item_list_add(app->variable_item_list_file, "Rename File", 0, NULL, NULL); // index 2
+    app->file_delete_item = variable_item_list_add(app->variable_item_list_file, "Delete File", 0, NULL, NULL); // index 3
+
+    if (!app->ssid_item || !app->password_item || !app->file_type_item || !app->file_rename_item || !app->path_item || !app->file_read_item || !app->file_delete_item || !app->http_method_item || !app->headers_item || !app->payload_item)
+    {
+        free_all(app, "Failed to add items to VariableItemList");
+        return NULL;
+    }
+
+    variable_item_set_current_value_text(app->path_item, "");        // Initialize
+    variable_item_set_current_value_text(app->http_method_item, ""); // Initialize
+    variable_item_set_current_value_text(app->headers_item, "");     // Initialize
+    variable_item_set_current_value_text(app->payload_item, "");     // Initialize
+    variable_item_set_current_value_text(app->ssid_item, "");        // Initialize
+    variable_item_set_current_value_text(app->password_item, "");    // Initialize
+    variable_item_set_current_value_text(app->file_type_item, "");   // Initialize
+    variable_item_set_current_value_text(app->file_rename_item, ""); // Initialize
+    variable_item_set_current_value_text(app->file_read_item, "");   // Initialize
+    variable_item_set_current_value_text(app->file_delete_item, ""); // Initialize
+
+    // Allocate Submenu views
+    if (!easy_flipper_set_submenu(&app->submenu_main, WebCrawlerViewSubmenuMain, "Web Crawler v0.7", web_crawler_exit_app_callback, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_config, WebCrawlerViewSubmenuConfig, "Settings", web_crawler_back_to_main_callback, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+
+    // Add Submenu items
+    submenu_add_item(app->submenu_main, "Run", WebCrawlerSubmenuIndexRun, web_crawler_submenu_callback, app);
+    submenu_add_item(app->submenu_main, "About", WebCrawlerSubmenuIndexAbout, web_crawler_submenu_callback, app);
+    submenu_add_item(app->submenu_main, "Settings", WebCrawlerSubmenuIndexConfig, web_crawler_submenu_callback, app);
+
+    submenu_add_item(app->submenu_config, "WiFi", WebCrawlerSubmenuIndexWifi, web_crawler_submenu_callback, app);
+    submenu_add_item(app->submenu_config, "File", WebCrawlerSubmenuIndexFile, web_crawler_submenu_callback, app);
+    submenu_add_item(app->submenu_config, "Request", WebCrawlerSubmenuIndexRequest, web_crawler_submenu_callback, app);
+
+    // Allocate views
+    if (!easy_flipper_set_view(&app->view_main, WebCrawlerViewMain, NULL, NULL, web_crawler_exit_app_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_view(&app->view_run, WebCrawlerViewRun, web_crawler_view_draw_callback, NULL, web_crawler_back_to_main_callback, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+
+    //-- WIDGET ABOUT VIEW --
+    if (!easy_flipper_set_widget(&app->widget_about, WebCrawlerViewAbout, "Web Crawler App\n---\nThis is a web crawler app for Flipper Zero.\n---\nVisit github.com/jblanked for more details.\n---\nPress BACK to return.", web_crawler_back_to_main_callback, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_widget(&app->widget_file_read, WebCrawlerViewFileRead, "Data will be displayed here.", web_crawler_back_to_file_callback, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_widget(&app->widget_file_delete, WebCrawlerViewFileDelete, "File deleted.", web_crawler_back_to_file_callback, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+
+    // Load Settings and Update Views
+    if (!load_settings(
+            app->path,
+            app->temp_buffer_size_path,
+            app->ssid,
+            app->temp_buffer_size_ssid,
+            app->password,
+            app->temp_buffer_size_password,
+            app->file_rename,
+            app->temp_buffer_size_file_rename,
+            app->file_type,
+            app->temp_buffer_size_file_type,
+            app->http_method,
+            app->temp_buffer_size_http_method,
+            app->headers,
+            app->temp_buffer_size_headers,
+            app->payload,
+            app->temp_buffer_size_payload,
+            app))
+    {
+        FURI_LOG_E(TAG, "Failed to load settings");
+    }
+    else
+    {
+        // Update the configuration items based on loaded settings
+        if (app->path_item)
+        {
+            variable_item_set_current_value_text(app->path_item, app->path);
+        }
+        else
+        {
+            variable_item_set_current_value_text(app->path_item, "https://httpbin.org/get"); // Initialize
+        }
+
+        if (app->ssid_item)
+        {
+            variable_item_set_current_value_text(app->ssid_item, app->ssid);
+        }
+        else
+        {
+            variable_item_set_current_value_text(app->ssid_item, ""); // Initialize
+        }
+
+        if (app->file_type_item)
+        {
+            variable_item_set_current_value_text(app->file_type_item, app->file_type);
+        }
+        else
+        {
+            variable_item_set_current_value_text(app->file_type_item, ".txt"); // Initialize
+        }
+
+        if (app->file_rename_item)
+        {
+            variable_item_set_current_value_text(app->file_rename_item, app->file_rename);
+        }
+        else
+        {
+            variable_item_set_current_value_text(app->file_rename_item, "received_data"); // Initialize
+        }
+
+        if (app->http_method_item)
+        {
+            variable_item_set_current_value_text(app->http_method_item, app->http_method);
+        }
+        else
+        {
+            variable_item_set_current_value_text(app->http_method_item, "GET"); // Initialize
+        }
+
+        if (app->headers_item)
+        {
+            variable_item_set_current_value_text(app->headers_item, app->headers);
+        }
+        else
+        {
+            variable_item_set_current_value_text(app->headers_item, "{\n\t\"Content-Type\": \"application/json\"\n}"); // Initialize
+        }
+
+        if (app->payload_item)
+        {
+            variable_item_set_current_value_text(app->payload_item, app->payload);
+        }
+        else
+        {
+            variable_item_set_current_value_text(app->payload_item, "{\n\t\"key\": \"value\"\n}"); // Initialize
+        }
+
+        // set the file path for fhttp.file_path
+        char file_path[128];
+        snprintf(file_path, sizeof(file_path), "%s%s%s", RECEIVED_DATA_PATH, app->file_rename, app->file_type);
+        snprintf(fhttp.file_path, sizeof(fhttp.file_path), "%s", file_path);
+
+        // update temp buffers
+        strncpy(app->temp_buffer_path, app->path, app->temp_buffer_size_path - 1);
+        app->temp_buffer_path[app->temp_buffer_size_path - 1] = '\0';
+        strncpy(app->temp_buffer_ssid, app->ssid, app->temp_buffer_size_ssid - 1);
+        app->temp_buffer_ssid[app->temp_buffer_size_ssid - 1] = '\0';
+        strncpy(app->temp_buffer_file_type, app->file_type, app->temp_buffer_size_file_type - 1);
+        app->temp_buffer_file_type[app->temp_buffer_size_file_type - 1] = '\0';
+        strncpy(app->temp_buffer_file_rename, app->file_rename, app->temp_buffer_size_file_rename - 1);
+        app->temp_buffer_file_rename[app->temp_buffer_size_file_rename - 1] = '\0';
+        strncpy(app->temp_buffer_http_method, app->http_method, app->temp_buffer_size_http_method - 1);
+        app->temp_buffer_http_method[app->temp_buffer_size_http_method - 1] = '\0';
+        strncpy(app->temp_buffer_headers, app->headers, app->temp_buffer_size_headers - 1);
+        app->temp_buffer_headers[app->temp_buffer_size_headers - 1] = '\0';
+        strncpy(app->temp_buffer_payload, app->payload, app->temp_buffer_size_payload - 1);
+        app->temp_buffer_payload[app->temp_buffer_size_payload - 1] = '\0';
+
+        // Password handling can be omitted for security or handled securely
+    }
+
+    app_instance = app;
+
+    // Start with the Submenu view
+    view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewSubmenuMain);
+
+    return app;
+}
+
+#endif // WEB_CRAWLER_I_H

+ 407 - 0
web_crawler/web_crawler_storage.h

@@ -0,0 +1,407 @@
+#ifndef WEB_CRAWLER_STORAGE_H
+#define WEB_CRAWLER_STORAGE_H
+
+#include <furi.h>
+#include <storage/storage.h>
+
+#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/settings.bin"
+#define RECEIVED_DATA_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/" // add the file name to the end (e.g. "received_data.txt")
+
+// will need to make a duplicate of the file
+// one to save data, and the other for users to manipulate
+
+#define MAX_RECEIVED_DATA_SIZE 1024
+#define SHOW_MAX_FILE_SIZE 2048
+
+// Define the truncation notice
+#define TRUNCATION_NOTICE "\n\n[Data truncated due to size limits]"
+
+// Function to save settings: path, SSID, and password
+static void save_settings(
+    const char *path,
+    const char *ssid,
+    const char *password,
+    const char *file_rename,
+    const char *file_type,
+    const char *http_method,
+    const char *headers,
+    const char *payload)
+{
+    // Create the directory for saving settings
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler_app");
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+
+    // Open the settings file
+    File *file = storage_file_alloc(storage);
+    if (!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(TAG, "Failed to open settings file for writing: %s", SETTINGS_PATH);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return;
+    }
+
+    if (file_type == NULL || strlen(file_type) == 0)
+    {
+        file_type = ".txt";
+    }
+
+    // Save the SSID length and data
+    size_t ssid_length = strlen(ssid) + 1; // Include null terminator
+    if (storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, ssid, ssid_length) != ssid_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write SSID");
+    }
+
+    // Save the password length and data
+    size_t password_length = strlen(password) + 1; // Include null terminator
+    if (storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, password, password_length) != password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write password");
+    }
+
+    // Save the path length and data
+    size_t path_length = strlen(path) + 1; // Include null terminator
+    if (storage_file_write(file, &path_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, path, path_length) != path_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write path");
+    }
+    // Save the file rename length and data
+    size_t file_rename_length = strlen(file_rename) + 1; // Include null terminator
+    if (storage_file_write(file, &file_rename_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, file_rename, file_rename_length) != file_rename_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write file rename");
+    }
+
+    // Save the file type length and data
+    size_t file_type_length = strlen(file_type) + 1; // Include null terminator
+    if (storage_file_write(file, &file_type_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, file_type, file_type_length) != file_type_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write file type");
+    }
+
+    // Save the http method length and data
+    size_t http_method_length = strlen(http_method) + 1; // Include null terminator
+    if (storage_file_write(file, &http_method_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, http_method, http_method_length) != http_method_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write http method");
+    }
+
+    // Save the headers length and data
+    size_t headers_length = strlen(headers) + 1; // Include null terminator
+    if (storage_file_write(file, &headers_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, headers, headers_length) != headers_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write headers");
+    }
+
+    // Save the payload length and data
+    size_t payload_length = strlen(payload) + 1; // Include null terminator
+    if (storage_file_write(file, &payload_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, payload, payload_length) != payload_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write payload");
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+}
+
+// Function to load settings (the variables must be opened in the order they were saved)
+static bool load_settings(
+    char *path,
+    size_t path_size,
+    char *ssid,
+    size_t ssid_size,
+    char *password,
+    size_t password_size,
+    char *file_rename,
+    size_t file_rename_size,
+    char *file_type,
+    size_t file_type_size,
+    char *http_method,
+    size_t http_method_size,
+    char *headers,
+    size_t headers_size,
+    char *payload,
+    size_t payload_size,
+    WebCrawlerApp *app)
+{
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return false;
+    }
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    if (!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", SETTINGS_PATH);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false; // Return false if the file does not exist
+    }
+
+    // Load the SSID
+    size_t ssid_length;
+    if (storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || ssid_length > ssid_size ||
+        storage_file_read(file, ssid, ssid_length) != ssid_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read SSID");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    ssid[ssid_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the password
+    size_t password_length;
+    if (storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || password_length > password_size ||
+        storage_file_read(file, password, password_length) != password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read password");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    password[password_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the path
+    size_t path_length;
+    if (storage_file_read(file, &path_length, sizeof(size_t)) != sizeof(size_t) || path_length > path_size ||
+        storage_file_read(file, path, path_length) != path_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read path");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    path[path_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the file rename
+    size_t file_rename_length;
+    if (storage_file_read(file, &file_rename_length, sizeof(size_t)) != sizeof(size_t) || file_rename_length > file_rename_size ||
+        storage_file_read(file, file_rename, file_rename_length) != file_rename_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read file rename");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    file_rename[file_rename_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the file type
+    size_t file_type_length;
+    if (storage_file_read(file, &file_type_length, sizeof(size_t)) != sizeof(size_t) || file_type_length > file_type_size ||
+        storage_file_read(file, file_type, file_type_length) != file_type_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read file type");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    file_type[file_type_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the http method
+    size_t http_method_length;
+    if (storage_file_read(file, &http_method_length, sizeof(size_t)) != sizeof(size_t) || http_method_length > http_method_size ||
+        storage_file_read(file, http_method, http_method_length) != http_method_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read http method");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Load the headers
+    size_t headers_length;
+    if (storage_file_read(file, &headers_length, sizeof(size_t)) != sizeof(size_t) || headers_length > headers_size ||
+        storage_file_read(file, headers, headers_length) != headers_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read headers");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Load the payload
+    size_t payload_length;
+    if (storage_file_read(file, &payload_length, sizeof(size_t)) != sizeof(size_t) || payload_length > payload_size ||
+        storage_file_read(file, payload, payload_length) != payload_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read payload");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // set the path, ssid, and password
+    strncpy(app->path, path, path_size);
+    strncpy(app->ssid, ssid, ssid_size);
+    strncpy(app->password, password, password_size);
+    strncpy(app->file_rename, file_rename, file_rename_size);
+    strncpy(app->file_type, file_type, file_type_size);
+    strncpy(app->http_method, http_method, http_method_size);
+    strncpy(app->headers, headers, headers_size);
+    strncpy(app->payload, payload, payload_size);
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+    return true;
+}
+
+static bool delete_received_data(WebCrawlerApp *app)
+{
+    if (app == NULL)
+    {
+        FURI_LOG_E(TAG, "WebCrawlerApp is NULL");
+        return false;
+    }
+    // Open the storage record
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    if (!storage)
+    {
+        FURI_LOG_E(TAG, "Failed to open storage record");
+        return false;
+    }
+
+    if (!storage_simply_remove_recursive(storage, RECEIVED_DATA_PATH "received_data.txt"))
+    {
+        FURI_LOG_E(TAG, "Failed to delete main file");
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Allocate memory for new_path
+    char *new_path = malloc(256);
+    if (new_path == NULL)
+    {
+        FURI_LOG_E(TAG, "Memory allocation failed for paths");
+        free(new_path);
+        return false;
+    }
+
+    if (app->file_type == NULL || strlen(app->file_type) == 0)
+    {
+        app->file_type = ".txt";
+    }
+
+    // Format the new_path
+    int ret_new = snprintf(new_path, 256, "%s%s%s", RECEIVED_DATA_PATH, app->file_rename, app->file_type);
+    if (ret_new < 0 || (size_t)ret_new >= 256)
+    {
+        FURI_LOG_E(TAG, "Failed to create new_path");
+        free(new_path);
+        return false;
+    }
+
+    if (!storage_simply_remove_recursive(storage, new_path))
+    {
+        FURI_LOG_E(TAG, "Failed to delete duplicate file");
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+
+static bool rename_received_data(const char *old_name, const char *new_name, const char *file_type, const char *old_file_type)
+{
+    // Open the storage record
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    if (!storage)
+    {
+        FURI_LOG_E(TAG, "Failed to open storage record");
+        return false;
+    }
+    // Allocate memory for old_path and new_path
+    char *new_path = malloc(256);
+    char *old_path = malloc(256);
+    if (new_path == NULL || old_path == NULL)
+    {
+        FURI_LOG_E(TAG, "Memory allocation failed for paths");
+        free(old_path);
+        free(new_path);
+        return false;
+    }
+
+    if (file_type == NULL || strlen(file_type) == 0)
+    {
+        file_type = ".txt";
+    }
+    if (old_file_type == NULL || strlen(old_file_type) == 0)
+    {
+        old_file_type = ".txt";
+    }
+
+    // Format the old_path
+    int ret_old = snprintf(old_path, 256, "%s%s%s", RECEIVED_DATA_PATH, old_name, old_file_type);
+    if (ret_old < 0 || (size_t)ret_old >= 256)
+    {
+        FURI_LOG_E(TAG, "Failed to create old_path");
+        free(old_path);
+        free(new_path);
+        return false;
+    }
+
+    // Format the new_path
+    int ret_new = snprintf(new_path, 256, "%s%s%s", RECEIVED_DATA_PATH, new_name, file_type);
+    if (ret_new < 0 || (size_t)ret_new >= 256)
+    {
+        FURI_LOG_E(TAG, "Failed to create new_path");
+        free(old_path);
+        free(new_path);
+        return false;
+    }
+
+    // Check if the file exists
+    if (!storage_file_exists(storage, old_path))
+    {
+        if (!storage_file_exists(storage, RECEIVED_DATA_PATH "received_data.txt"))
+        {
+            FURI_LOG_E(TAG, "No saved file exists");
+            free(old_path);
+            free(new_path);
+            furi_record_close(RECORD_STORAGE);
+            return false;
+        }
+        else
+        {
+            bool renamed = storage_common_copy(storage, RECEIVED_DATA_PATH "received_data.txt", new_path) == FSE_OK;
+
+            furi_record_close(RECORD_STORAGE);
+            return renamed;
+        }
+    }
+    else
+    {
+        bool renamed = storage_common_rename(storage, old_path, new_path) == FSE_OK;
+        storage_simply_remove_recursive(storage, old_path);
+        furi_record_close(RECORD_STORAGE);
+        return renamed;
+    }
+}
+#endif // WEB_CRAWLER_STORAGE_H