Просмотр исходного кода

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

# Conflicts:
#	web_crawler/application.fam
#	web_crawler/callback/web_crawler_callback.c
#	web_crawler/web_crawler.c
#	web_crawler/web_crawler.h
Willy-JL 1 год назад
Родитель
Сommit
afaf4a0c8d

+ 12 - 0
web_crawler/CHANGELOG.md

@@ -1,3 +1,15 @@
+## 1.0.1
+- Updated the settings so they save as intended.
+- Added additional error handling.
+- Fixed a bug in the text input that prevented users from typing.
+
+## 1.0
+- Updated the HTTP Method toggle to work as intended.
+- Updated FlipperHTTP to the latest version.
+- Improved memory allocation
+- Fixed loading display messages.
+- Added a BROWSE method, which fetches HTML data from the specified path, parses the HTML, then displays the data as a "webpage". 
+
 ## 0.8
 Updates from Derek Jamison:
 - Improved progress display.

+ 4 - 22
web_crawler/README.md

@@ -1,11 +1,8 @@
-## 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.
+Browse the web, fetch API data, and more on your 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
-
+- WiFi Developer Board, Raspberry Pi, or ESP32 device flashed with FlipperHTTP version 1.6 or higher: https://github.com/jblanked/FlipperHTTP
+- 2.4 GHz WiFi Access Point
 
 ## Installation
 - Download from the Official Flipper App Store: https://lab.flipper.net/apps/web_crawler
@@ -52,7 +49,7 @@
    - 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, PUT, and DOWNLOAD.
+   - Choose between GET, POST, DELETE, PUT, DOWNLOAD, and BROWSE.
 
 3. **Headers**
    - Add your required headers to be used in your HTTP requests
@@ -73,19 +70,4 @@
    - 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.
-
-## Known Issues
-1. **Screen Delay**: Occasionally, the Run screen may get stuck on "Receiving Data".
-   - If it takes longer than 10 seconds, restart your Flipper Zero.
-
 *Happy Crawling! 🕷️* 

+ 1 - 280
web_crawler/alloc/web_crawler_alloc.c

@@ -12,13 +12,6 @@ WebCrawlerApp *web_crawler_app_alloc()
     // 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))
     {
@@ -27,54 +20,7 @@ WebCrawlerApp *web_crawler_app_alloc()
     view_dispatcher_set_custom_event_callback(app->view_dispatcher, web_crawler_custom_event_callback);
 
     // 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;
@@ -83,104 +29,9 @@ WebCrawlerApp *web_crawler_app_alloc()
     {
         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.8", 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))
+    if (!easy_flipper_set_submenu(&app->submenu_main, WebCrawlerViewSubmenuMain, VERSION_TAG, web_crawler_exit_app_callback, &app->view_dispatcher))
     {
         return NULL;
     }
@@ -190,147 +41,17 @@ WebCrawlerApp *web_crawler_app_alloc()
     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);
-
     // Main view
     if (!easy_flipper_set_view(&app->view_loader, WebCrawlerViewLoader, web_crawler_loader_draw_callback, NULL, web_crawler_back_to_main_callback, &app->view_dispatcher, app))
     {
         return NULL;
     }
     web_crawler_loader_init(app->view_loader);
-
-    //-- 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;
-    }
     if (!easy_flipper_set_widget(&app->widget_result, WebCrawlerViewWidgetResult, "Error, try again.", web_crawler_back_to_main_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
-    }
-
     // Start with the Submenu view
     view_dispatcher_switch_to_view(app->view_dispatcher, WebCrawlerViewSubmenuMain);
 

+ 25 - 32
web_crawler/app.c

@@ -9,54 +9,47 @@ int32_t web_crawler_app(void *p)
 {
     UNUSED(p);
 
-    app_instance = web_crawler_app_alloc();
-    if (!app_instance)
+    WebCrawlerApp *app = web_crawler_app_alloc();
+    if (!app)
     {
         FURI_LOG_E(TAG, "Failed to allocate WebCrawlerApp");
         return -1;
     }
 
-    if (!flipper_http_ping())
+    // check if board is connected (Derek Jamison)
+    FlipperHTTP *fhttp = flipper_http_alloc();
+    if (!fhttp)
+    {
+        easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero.");
+        return -1;
+    }
+
+    if (!flipper_http_ping(fhttp))
     {
         FURI_LOG_E(TAG, "Failed to ping the device");
+        flipper_http_free(fhttp);
         return -1;
     }
 
-    // Edit from Derek Jamison
-    if (app_instance->text_input_ssid != NULL && app_instance->text_input_password != NULL)
+    // Try to wait for pong response.
+    uint32_t counter = 10;
+    while (fhttp->state == INACTIVE && --counter > 0)
+    {
+        FURI_LOG_D(TAG, "Waiting for PONG");
+        furi_delay_ms(100); // this causes a BusFault
+    }
+
+    flipper_http_free(fhttp);
+    if (counter == 0)
     {
-        // Try to wait for pong response.
-        uint8_t counter = 10;
-        while (fhttp.state == INACTIVE && --counter > 0)
-        {
-            FURI_LOG_D(TAG, "Waiting for PONG");
-            furi_delay_ms(100);
-        }
-
-        if (counter == 0)
-        {
-            DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS);
-            DialogMessage *message = dialog_message_alloc();
-            dialog_message_set_header(
-                message, "[FlipperHTTP Error]", 64, 0, AlignCenter, AlignTop);
-            dialog_message_set_text(
-                message,
-                "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.",
-                0,
-                63,
-                AlignLeft,
-                AlignBottom);
-            dialog_message_show(dialogs, message);
-            dialog_message_free(message);
-            furi_record_close(RECORD_DIALOGS);
-        }
+        easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.");
     }
 
     // Run the application
-    view_dispatcher_run(app_instance->view_dispatcher);
+    view_dispatcher_run(app->view_dispatcher);
 
     // Free resources after the application loop ends
-    web_crawler_app_free(app_instance);
+    web_crawler_app_free(app);
 
     return 0;
 }

+ 2 - 2
web_crawler/application.fam

@@ -7,8 +7,8 @@ App(
     fap_icon="app.png",
     fap_category="GPIO/FlipperHTTP",
     fap_icon_assets="assets",
-    fap_description="Use Wi-Fi to access the internet and scrape data from the web.",
+    fap_description="Browse the web, fetch API data, and more.",
     fap_author="JBlanked",
     fap_weburl="https://github.com/jblanked/WebCrawler-FlipperZero",
-    fap_version="0.8",
+    fap_version="1.0.1",
 )

BIN
web_crawler/assets/02-main.png


Разница между файлами не показана из-за своего большого размера
+ 789 - 95
web_crawler/callback/web_crawler_callback.c


+ 3 - 61
web_crawler/callback/web_crawler_callback.h

@@ -3,11 +3,9 @@
 #include "web_crawler.h"
 #include <flip_storage/web_crawler_storage.h>
 
-extern bool sent_http_request;
-extern bool get_success;
-extern bool already_success;
-
 void web_crawler_http_method_change(VariableItem *item);
+uint32_t web_crawler_back_to_main_callback(void *context);
+void free_all(WebCrawlerApp *app);
 
 /**
  * @brief      Navigation callback to handle exiting from other views to the submenu.
@@ -16,14 +14,6 @@ void web_crawler_http_method_change(VariableItem *item);
  */
 uint32_t web_crawler_back_to_configure_callback(void *context);
 
-/**
- * @brief      Navigation callback to handle returning to the Wifi Settings screen.
- * @param      context   The context - WebCrawlerApp object.
- * @return     WebCrawlerViewSubmenu
- */
-uint32_t web_crawler_back_to_main_callback(void *context);
-uint32_t web_crawler_back_to_file_callback(void *context);
-
 uint32_t web_crawler_back_to_wifi_callback(void *context);
 
 uint32_t web_crawler_back_to_request_callback(void *context);
@@ -105,55 +95,6 @@ void web_crawler_set_file_type_update(void *context);
  */
 void web_crawler_set_file_rename_update(void *context);
 
-/**
- * @brief      Handler for Path configuration item click.
- * @param      context  The context - WebCrawlerApp object.
- * @param      index    The index of the item that was clicked.
- */
-void web_crawler_setting_item_path_clicked(void *context, uint32_t index);
-
-/**
- * @brief      Handler for headers configuration item click.
- * @param      context  The context - WebCrawlerApp object.
- * @param      index    The index of the item that was clicked.
- */
-void web_crawler_setting_item_headers_clicked(void *context, uint32_t index);
-
-/**
- * @brief      Handler for payload configuration item click.
- * @param      context  The context - WebCrawlerApp object.
- * @param      index    The index of the item that was clicked.
- */
-void web_crawler_setting_item_payload_clicked(void *context, uint32_t index);
-
-/**
- * @brief      Handler for SSID configuration item click.
- * @param      context  The context - WebCrawlerApp object.
- * @param      index    The index of the item that was clicked.
- */
-void web_crawler_setting_item_ssid_clicked(void *context, uint32_t index);
-
-/**
- * @brief      Handler for Password configuration item click.
- * @param      context  The context - WebCrawlerApp object.
- * @param      index    The index of the item that was clicked.
- */
-void web_crawler_setting_item_password_clicked(void *context, uint32_t index);
-
-/**
- * @brief      Handler for File Type configuration item click.
- * @param      context  The context - WebCrawlerApp object.
- * @param      index    The index of the item that was clicked.
- */
-void web_crawler_setting_item_file_type_clicked(void *context, uint32_t index);
-
-/**
- * @brief      Handler for File Rename configuration item click.
- * @param      context  The context - WebCrawlerApp object.
- * @param      index    The index of the item that was clicked.
- */
-void web_crawler_setting_item_file_rename_clicked(void *context, uint32_t index);
-
 /**
  * @brief      Handler for File Delete configuration item click.
  * @param      context  The context - WebCrawlerApp object.
@@ -196,6 +137,7 @@ struct DataLoaderModel
     size_t request_count;
     ViewNavigationCallback back_callback;
     FuriTimer *timer;
+    FlipperHTTP *fhttp;
 };
 
 void web_crawler_generic_switch_to_view(WebCrawlerApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id);

+ 56 - 0
web_crawler/easy_flipper/easy_flipper.c

@@ -1,5 +1,25 @@
 #include <easy_flipper/easy_flipper.h>
 
+void easy_flipper_dialog(
+    char *header,
+    char *text)
+{
+    DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS);
+    DialogMessage *message = dialog_message_alloc();
+    dialog_message_set_header(
+        message, header, 64, 0, AlignCenter, AlignTop);
+    dialog_message_set_text(
+        message,
+        text,
+        0,
+        63,
+        AlignLeft,
+        AlignBottom);
+    dialog_message_show(dialogs, message);
+    dialog_message_free(message);
+    furi_record_close(RECORD_DIALOGS);
+}
+
 /**
  * @brief Navigation callback for exiting the application
  * @param context The context - unused
@@ -568,4 +588,40 @@ bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer
     }
     furi_string_set_str(*furi_string, buffer);
     return true;
+}
+
+bool easy_flipper_set_text_box(
+    TextBox **text_box,
+    int32_t view_id,
+    char *text,
+    bool start_at_end,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!text_box)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_text_box");
+        return false;
+    }
+    *text_box = text_box_alloc();
+    if (!*text_box)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate TextBox");
+        return false;
+    }
+    if (text)
+    {
+        text_box_set_text(*text_box, text);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(text_box_get_view(*text_box), previous_callback);
+    }
+    text_box_set_font(*text_box, TextBoxFontText);
+    if (start_at_end)
+    {
+        text_box_set_focus(*text_box, TextBoxFocusEnd);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, text_box_get_view(*text_box));
+    return true;
 }

+ 24 - 0
web_crawler/easy_flipper/easy_flipper.h

@@ -8,6 +8,7 @@
 #include <gui/view.h>
 #include <gui/modules/submenu.h>
 #include <gui/view_dispatcher.h>
+#include <gui/elements.h>
 #include <gui/modules/menu.h>
 #include <gui/modules/submenu.h>
 #include <gui/modules/widget.h>
@@ -21,10 +22,15 @@
 #include <gui/modules/loading.h>
 #include <stdio.h>
 #include <string.h>
+#include <jsmn/jsmn_furi.h>
 #include <jsmn/jsmn.h>
 
 #define EASY_TAG "EasyFlipper"
 
+void easy_flipper_dialog(
+    char *header,
+    char *text);
+
 /**
  * @brief Navigation callback for exiting the application
  * @param context The context - unused
@@ -260,4 +266,22 @@ bool easy_flipper_set_loading(
  */
 bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer);
 
+/**
+ * @brief Initialize a TextBox object
+ * @param text_box The TextBox object to initialize
+ * @param view_id The ID/Index of the view
+ * @param text The text to display in the text box
+ * @param start_at_end Start the text box at the end
+ * @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_box(
+    TextBox **text_box,
+    int32_t view_id,
+    char *text,
+    bool start_at_end,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher);
+
 #endif

+ 104 - 24
web_crawler/flip_storage/web_crawler_storage.c

@@ -239,36 +239,17 @@ bool load_settings(
         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;
 }
 
-bool delete_received_data(WebCrawlerApp *app)
+bool delete_received_data()
 {
-    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;
-    }
+    furi_check(storage, "Failed to open storage record");
 
     if (!storage_simply_remove_recursive(storage, RECEIVED_DATA_PATH "received_data.txt"))
     {
@@ -286,13 +267,19 @@ bool delete_received_data(WebCrawlerApp *app)
         return false;
     }
 
-    if (app->file_type == NULL || strlen(app->file_type) == 0)
+    char file_type[16];
+    if (!load_char("file_type", file_type, sizeof(file_type)))
     {
-        app->file_type = ".txt";
+        snprintf(file_type, sizeof(file_type), ".txt");
+    }
+    char file_rename[128];
+    if (!load_char("file_rename", file_rename, sizeof(file_rename)))
+    {
+        snprintf(file_rename, sizeof(file_rename), "received_data");
     }
 
     // Format the new_path
-    int ret_new = snprintf(new_path, 256, "%s%s%s", RECEIVED_DATA_PATH, app->file_rename, app->file_type);
+    int ret_new = snprintf(new_path, 256, "%s%s%s", RECEIVED_DATA_PATH, file_rename, file_type);
     if (ret_new < 0 || (size_t)ret_new >= 256)
     {
         FURI_LOG_E(TAG, "Failed to create new_path");
@@ -388,3 +375,96 @@ bool rename_received_data(const char *old_name, const char *new_name, const char
         return renamed;
     }
 }
+
+bool save_char(
+    const char *path_name, const char *value)
+{
+    if (!value)
+    {
+        return false;
+    }
+    // Create the directory for saving settings
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler");
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler/data");
+    storage_common_mkdir(storage, directory_path);
+
+    // Open the settings file
+    File *file = storage_file_alloc(storage);
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler/data/%s.txt", path_name);
+
+    // Open the file in write mode
+    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write the data to the file
+    size_t data_size = strlen(value) + 1; // Include null terminator
+    if (storage_file_write(file, value, data_size) != data_size)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+
+bool load_char(
+    const char *path_name,
+    char *value,
+    size_t value_size)
+{
+    if (!value)
+    {
+        return false;
+    }
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/web_crawler/data/%s.txt", path_name);
+
+    // Open the file for reading
+    if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false; // Return false if the file does not exist
+    }
+
+    // Read data into the buffer
+    size_t read_count = storage_file_read(file, value, value_size);
+    if (storage_file_get_error(file) != FSE_OK)
+    {
+        FURI_LOG_E(HTTP_TAG, "Error reading from file.");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Ensure null-termination
+    value[read_count - 1] = '\0';
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return strlen(value) > 0;
+}

+ 10 - 4
web_crawler/flip_storage/web_crawler_storage.h

@@ -1,5 +1,4 @@
-#ifndef WEB_CRAWLER_STORAGE_H
-#define WEB_CRAWLER_STORAGE_H
+#pragma once
 #include <web_crawler.h>
 #include <furi.h>
 #include <storage/storage.h>
@@ -38,6 +37,13 @@ bool load_settings(
     size_t payload_size,
     WebCrawlerApp *app);
 
-bool delete_received_data(WebCrawlerApp *app);
+bool delete_received_data();
 bool rename_received_data(const char *old_name, const char *new_name, const char *file_type, const char *old_file_type);
-#endif // WEB_CRAWLER_STORAGE_H
+
+bool save_char(
+    const char *path_name, const char *value);
+
+bool load_char(
+    const char *path_name,
+    char *value,
+    size_t value_size);

Разница между файлами не показана из-за своего большого размера
+ 395 - 152
web_crawler/flipper_http/flipper_http.c


+ 80 - 39
web_crawler/flipper_http/flipper_http.h

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

+ 288 - 0
web_crawler/html/html_furi.c

@@ -0,0 +1,288 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdbool.h>
+#include <html/html_furi.h>
+
+/*
+ * Checks if the substring of the FuriString starting at index `pos`
+ * matches the given C-string `needle`.
+ */
+static bool furi_string_sub_equals(FuriString *str, int pos, const char *needle)
+{
+    size_t needle_len = strlen(needle);
+    if ((size_t)pos + needle_len > furi_string_size(str))
+    {
+        return false;
+    }
+    for (size_t i = 0; i < needle_len; i++)
+    {
+        if (furi_string_get_char(str, pos + i) != needle[i])
+        {
+            return false;
+        }
+    }
+    return true;
+}
+
+/*
+ * Parse the content for a given HTML tag <tag> in `html`, handling nested tags.
+ * Returns a newly allocated FuriString or NULL on error.
+ *
+ * @param tag    e.g. "<p>"
+ * @param html   The HTML string to parse.
+ * @param index  The position in `html` from where to start searching.
+ */
+FuriString *html_furi_find_tag(const char *tag, FuriString *html, size_t index)
+{
+    int tag_len = strlen(tag);
+    if (tag_len < 3)
+    {
+        FURI_LOG_E("html_furi_parse", "Invalid tag length");
+        return NULL;
+    }
+
+    // Extract the tag name from <p> => "p"
+    int inner_len = tag_len - 2; // exclude '<' and '>'
+    char inner_tag[inner_len + 1];
+    for (int i = 0; i < inner_len; i++)
+    {
+        inner_tag[i] = tag[i + 1];
+    }
+    inner_tag[inner_len] = '\0';
+
+    // Build closing tag => "</p>"
+    char closing_tag[inner_len + 4];
+    snprintf(closing_tag, sizeof(closing_tag), "</%s>", inner_tag);
+
+    int html_len = furi_string_size(html);
+
+    // Find the first occurrence of the opening tag
+    int open_tag_index = -1;
+    for (int i = index; i <= html_len - tag_len; i++)
+    {
+        if (furi_string_sub_equals(html, i, tag))
+        {
+            open_tag_index = i;
+            break;
+        }
+    }
+    if (open_tag_index == -1)
+    {
+        // Tag not found
+        return NULL;
+    }
+
+    // Content starts after the opening tag
+    int content_start = open_tag_index + tag_len;
+
+    // Skip leading whitespace
+    while (content_start < html_len && furi_string_get_char(html, content_start) == ' ')
+    {
+        content_start++;
+    }
+
+    // Find matching closing tag, accounting for nested tags
+    int depth = 1;
+    int i = content_start;
+    int matching_close_index = -1;
+    while (i <= html_len - 1)
+    {
+        if (furi_string_sub_equals(html, i, tag))
+        {
+            depth++;
+            i += tag_len;
+            continue;
+        }
+        if (furi_string_sub_equals(html, i, closing_tag))
+        {
+            depth--;
+            if (depth == 0)
+            {
+                matching_close_index = i;
+                break;
+            }
+            i += strlen(closing_tag);
+            continue;
+        }
+        i++;
+    }
+
+    if (matching_close_index == -1)
+    {
+        // No matching close => return NULL or partial content as you choose
+        return NULL;
+    }
+
+    // Copy the content between <tag>...</tag>
+    size_t content_length = matching_close_index - content_start;
+
+    if (memmgr_get_free_heap() < (content_length + 1 + 1024))
+    {
+        FURI_LOG_E("html_furi_parse", "Not enough heap to allocate result");
+        return NULL;
+    }
+
+    // Allocate and copy
+    FuriString *result = furi_string_alloc();
+    furi_string_reserve(result, content_length + 1);
+    furi_string_set_n(result, html, content_start, content_length);
+    furi_string_trim(result);
+    return result;
+}
+
+static FuriString *_html_furi_find_tag(const char *tag, FuriString *html, size_t index, int *out_next_index)
+{
+    // Clear next index in case of early return
+    *out_next_index = -1;
+
+    int tag_len = strlen(tag);
+    if (tag_len < 3)
+    {
+        FURI_LOG_E("html_furi_parse", "Invalid tag length");
+        return NULL;
+    }
+
+    // Extract "p" from "<p>"
+    int inner_len = tag_len - 2;
+    char inner_tag[inner_len + 1];
+    for (int i = 0; i < inner_len; i++)
+    {
+        inner_tag[i] = tag[i + 1];
+    }
+    inner_tag[inner_len] = '\0';
+
+    // Create closing tag => "</p>"
+    char closing_tag[inner_len + 4];
+    snprintf(closing_tag, sizeof(closing_tag), "</%s>", inner_tag);
+
+    int html_len = furi_string_size(html);
+
+    // 1) Find opening tag from `index`.
+    int open_tag_index = -1;
+    for (int i = index; i <= html_len - tag_len; i++)
+    {
+        if (furi_string_sub_equals(html, i, tag))
+        {
+            open_tag_index = i;
+            break;
+        }
+    }
+    if (open_tag_index == -1)
+    {
+        return NULL; // no more occurrences
+    }
+
+    // The content begins after the opening tag.
+    int content_start = open_tag_index + tag_len;
+
+    // skip leading spaces
+    while (content_start < html_len && furi_string_get_char(html, content_start) == ' ')
+    {
+        content_start++;
+    }
+
+    int depth = 1;
+    int i = content_start;
+    int matching_close_index = -1;
+
+    while (i < html_len)
+    {
+        if (furi_string_sub_equals(html, i, tag))
+        {
+            depth++;
+            i += tag_len;
+        }
+        else if (furi_string_sub_equals(html, i, closing_tag))
+        {
+            depth--;
+            i += strlen(closing_tag);
+            if (depth == 0)
+            {
+                matching_close_index = i - strlen(closing_tag);
+                // i now points just after "</p>"
+                break;
+            }
+        }
+        else
+        {
+            i++;
+        }
+    }
+
+    if (matching_close_index == -1)
+    {
+        // No matching close tag found
+        return NULL;
+    }
+
+    size_t content_length = matching_close_index - content_start;
+
+    // Allocate the result
+    FuriString *result = furi_string_alloc();
+    furi_string_reserve(result, content_length + 1); // +1 for safety
+    furi_string_set_n(result, html, content_start, content_length);
+    furi_string_trim(result);
+    *out_next_index = i;
+
+    return result;
+}
+
+/*
+ * Parse *all* occurrences of <tag> in `html`, handling nested tags.
+ * Returns a FuriString concatenating all parsed contents.
+ */
+FuriString *html_furi_find_tags(const char *tag, FuriString *html)
+{
+    FuriString *result = furi_string_alloc();
+    size_t index = 0;
+
+    while (true)
+    {
+        int next_index;
+        FuriString *parsed = _html_furi_find_tag(tag, html, index, &next_index);
+        if (parsed == NULL)
+        {
+            // No more tags from 'index' onward
+            break;
+        }
+
+        // Append the found content
+        furi_string_cat(result, parsed);
+        furi_string_cat_str(result, "\n");
+        furi_string_free(parsed);
+
+        // Resume searching at `next_index` (just after `</tag>`).
+        index = next_index;
+    }
+
+    return result;
+}
+
+/*
+ * @brief Check if an HTML tag exists in the provided HTML string.
+ * @param tag The HTML tag to search for (including the angle brackets).
+ * @param html The HTML string to search (as a FuriString).
+ * @param index The starting index to search from.
+ * @return True if the tag exists in the HTML string, false otherwise.
+ */
+bool html_furi_tag_exists(const char *tag, FuriString *html, size_t index)
+{
+    int tag_len = strlen(tag);
+    if (tag_len < 3)
+    {
+        FURI_LOG_E("html_furi_parse", "Invalid tag length");
+        return false;
+    }
+
+    int html_len = furi_string_size(html);
+
+    for (int i = index; i <= html_len - tag_len; i++)
+    {
+        if (furi_string_sub_equals(html, i, tag))
+        {
+            return true;
+        }
+    }
+
+    return false;
+}

+ 40 - 0
web_crawler/html/html_furi.h

@@ -0,0 +1,40 @@
+#pragma once
+#include <furi.h>
+#include <furi_hal.h>
+
+/*
+ * @brief Parse a Furigana string from an HTML tag, handling nested child tags.
+ *
+ * This version accepts an HTML tag as a C-string (e.g., "<p>") and searches
+ * for the content inside the corresponding opening and closing tags within
+ * the provided HTML string, taking into account nested occurrences of the tag.
+ *
+ * For example, given the HTML string:
+ *     "<p><h1><p><h1>Test</h1></p></h1></p>"
+ * and searching with tag "<p>" the function will return:
+ *     "<h1><p><h1>Test</h1></p></h1>"
+ *
+ * @param tag The HTML tag to parse (including the angle brackets).
+ * @param html The HTML string to parse (as a FuriString).
+ * @return A newly allocated FuriString containing the parsed content,
+ *         or an empty FuriString if the tag is not found.
+ */
+FuriString *html_furi_find_tag(const char *tag, FuriString *html, size_t index);
+
+/*
+ * @brief Parse all Furigana strings from an HTML tag, handling nested child tags.
+ * @param tag The HTML tag to parse (including the angle brackets).
+ * @param html The HTML string to parse (as a FuriString).
+ * @return A newly allocated FuriString containing the parsed content,
+ *         or an empty FuriString if the tag is not found.
+ */
+FuriString *html_furi_find_tags(const char *tag, FuriString *html);
+
+/*
+ * @brief Check if an HTML tag exists in the provided HTML string.
+ * @param tag The HTML tag to search for (including the angle brackets).
+ * @param html The HTML string to search (as a FuriString).
+ * @param index The starting index to search from.
+ * @return True if the tag exists in the HTML string, false otherwise.
+ */
+bool html_furi_tag_exists(const char *tag, FuriString *html, size_t index);

+ 103 - 47
web_crawler/jsmn/jsmn.c

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

+ 7 - 65
web_crawler/jsmn/jsmn.h

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

+ 736 - 0
web_crawler/jsmn/jsmn_furi.c

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

+ 74 - 0
web_crawler/jsmn/jsmn_furi.h

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

+ 15 - 0
web_crawler/jsmn/jsmn_h.c

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

+ 56 - 0
web_crawler/jsmn/jsmn_h.h

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

+ 17 - 249
web_crawler/web_crawler.c

@@ -1,189 +1,13 @@
 #include <web_crawler.h>
+#include <callback/web_crawler_callback.h>
 
-void web_crawler_loader_free_model(View *view);
-
-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;
-    }
-}
-
-void free_resources(WebCrawlerApp *app)
-{
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "Invalid app context");
-        return;
-    }
-
-    free_buffers(app);
-}
-
-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_loader)
-        view_free(app->view_loader);
-    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)
-        text_input_free(app->text_input_path);
-    if (app->text_input_ssid)
-        text_input_free(app->text_input_ssid);
-    if (app->text_input_password)
-        text_input_free(app->text_input_password);
-    if (app->text_input_file_type)
-        text_input_free(app->text_input_file_type);
-    if (app->text_input_file_rename)
-        text_input_free(app->text_input_file_rename);
-    if (app->text_input_headers)
-        text_input_free(app->text_input_headers);
-    if (app->text_input_payload)
-        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.
  */
 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;
-    }
+    furi_check(app, "web_crawler_app_free: App is NULL");
 
     // Free View(s)
     if (app->view_loader)
@@ -192,8 +16,6 @@ void web_crawler_app_free(WebCrawlerApp *app)
         web_crawler_loader_free_model(app->view_loader);
         view_free(app->view_loader);
     }
-    // Deinitialize UART
-    flipper_http_deinit();
 
     // Remove and free Submenu
     if (app->submenu_main)
@@ -201,83 +23,29 @@ void web_crawler_app_free(WebCrawlerApp *app)
         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)
-        text_input_free(app->text_input_path);
-    if (app->text_input_ssid)
-        text_input_free(app->text_input_ssid);
-    if (app->text_input_password)
-        text_input_free(app->text_input_password);
-    if (app->text_input_file_type)
-        text_input_free(app->text_input_file_type);
-    if (app->text_input_file_rename)
-        text_input_free(app->text_input_file_rename);
-    if (app->text_input_headers)
-        text_input_free(app->text_input_headers);
-    if (app->text_input_payload)
-        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);
-    }
     if (app->widget_result)
     {
         view_dispatcher_remove_view(app->view_dispatcher, WebCrawlerViewWidgetResult);
         widget_free(app->widget_result);
     }
 
-    // Free the ViewDispatcher and close GUI
-    if (app->view_dispatcher)
-        view_dispatcher_free(app->view_dispatcher);
-
-    // Free the application structure
-    if (app)
+    // check and free http method
+    if (app->temp_buffer_http_method)
     {
-        free(app);
+        free(app->temp_buffer_http_method);
+        app->temp_buffer_http_method = NULL;
+    }
+    if (app->http_method)
+    {
+        free(app->http_method);
+        app->http_method = NULL;
     }
-}
 
-WebCrawlerApp *app_instance = NULL;
-char *http_method_names[] = {"GET", "POST", "PUT", "DELETE", "DOWNLOAD"};
+    free_all(app);
+    furi_record_close(RECORD_STORAGE);
+    view_dispatcher_free(app->view_dispatcher);
+    free(app);
+}
+char *http_method_names[] = {"GET", "POST", "PUT", "DELETE", "DOWNLOAD", "BROWSE"};

+ 9 - 30
web_crawler/web_crawler.h

@@ -4,11 +4,10 @@
 
 #include <easy_flipper/easy_flipper.h>
 #include <flipper_http/flipper_http.h>
-#include <jsmn/jsmn.h>
 #include "web_crawler_icons.h"
-#include <storage/storage.h>
 
 #define TAG "Web Crawler"
+#define VERSION_TAG TAG " v1.0.1"
 extern char *http_method_names[];
 
 // Define the submenu items for our WebCrawler application
@@ -42,35 +41,23 @@ typedef enum
     //
     WebCrawlerViewWidgetResult, // The text box that displays the random fact
     WebCrawlerViewLoader,       // The loader screen retrieves data from the internet
+    //
+    WebCrawlerViewWidget,           // Generic widget view
+    WebCrawlerViewVariableItemList, // Generic variable item list view
+    WebCrawlerViewInput,            // Generic text input view
 } WebCrawlerViewIndex;
 
 // Define the application structure
 typedef struct
 {
     ViewDispatcher *view_dispatcher;
-    View *view_main;
-    View *view_run;
     View *view_loader;
+    Widget *widget_result; // The widget that displays the result
     Submenu *submenu_main;
     Submenu *submenu_config;
-    Widget *widget_about;
-    Widget *widget_result; // The widget that displays the result
-
-    TextInput *text_input_path;
-    TextInput *text_input_ssid;
-    TextInput *text_input_password;
-    TextInput *text_input_file_type;
-    TextInput *text_input_file_rename;
-    //
-    TextInput *text_input_headers;
-    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;
+    Widget *widget;
+    VariableItemList *variable_item_list;
+    TextInput *uart_text_input;
 
     VariableItem *path_item;
     VariableItem *ssid_item;
@@ -118,18 +105,10 @@ typedef struct
     uint32_t temp_buffer_size_payload;
 } WebCrawlerApp;
 
-void free_buffers(WebCrawlerApp *app);
-
-void free_resources(WebCrawlerApp *app);
-
-void free_all(WebCrawlerApp *app, char *reason);
-
 /**
  * @brief      Function to free the resources used by WebCrawlerApp.
  * @param      app  The WebCrawlerApp object to free.
  */
 void web_crawler_app_free(WebCrawlerApp *app);
 
-extern WebCrawlerApp *app_instance;
-
 #endif // WEB_CRAWLER_E

Некоторые файлы не были показаны из-за большого количества измененных файлов