Przeglądaj źródła

Merge flip_wifi from https://github.com/jblanked/FlipWiFi

# Conflicts:
#	flip_wifi/callback/flip_wifi_callback.c
#	flip_wifi/flipper_http/flipper_http.h
Willy-JL 9 miesięcy temu
rodzic
commit
6a248d2a3d

+ 5 - 0
flip_wifi/CHANGELOG.md

@@ -1,3 +1,8 @@
+## v1.5
+- Added AP mode (host a network and display a custom HTML)
+- Restructured the code.
+- Updated to the latest FlipperHTTP library.
+
 ## v1.4
 - Updated to the latest FlipperHTTP library.
 - Added a check to verify the WiFi connection was successful.

+ 1 - 2
flip_wifi/README.md

@@ -1,8 +1,7 @@
 FlipWiFi is the companion app for the popular FlipperHTTP flash, originally introduced in the https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP. It allows you to scan and save WiFi networks for use across all FlipperHTTP apps.
 
 ## Requirements
-
-- WiFi Developer Board, Raspberry Pi, or ESP32 Device flashed with FlipperHTTP v1.8.2 or higher: https://github.com/jblanked/FlipperHTTP
+- WiFi Developer Board, BW16, Raspberry Pi, or ESP32 Device flashed with FlipperHTTP v1.8.3 or higher: https://github.com/jblanked/FlipperHTTP
 - 2.4 GHz WiFi Access Point
 
 ## Features

+ 5 - 2
flip_wifi/alloc/flip_wifi_alloc.c

@@ -1,4 +1,4 @@
-#include <callback/flip_wifi_callback.h>
+#include <callback/callback.h>
 
 // Function to allocate resources for the FlipWiFiApp
 FlipWiFiApp *flip_wifi_app_alloc()
@@ -14,15 +14,18 @@ FlipWiFiApp *flip_wifi_app_alloc()
     }
 
     // Submenu
-    if (!easy_flipper_set_submenu(&app->submenu_main, FlipWiFiViewSubmenuMain, "FlipWiFi v1.4", callback_exit_app, &app->view_dispatcher))
+    if (!easy_flipper_set_submenu(&app->submenu_main, FlipWiFiViewSubmenuMain, VERSION_TAG, callback_exit_app, &app->view_dispatcher))
     {
         return NULL;
     }
     submenu_add_item(app->submenu_main, "Scan", FlipWiFiSubmenuIndexWiFiScan, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_main, "AP Mode", FlipWiFiSubmenuIndexWiFiAP, callback_submenu_choices, app);
     submenu_add_item(app->submenu_main, "Saved APs", FlipWiFiSubmenuIndexWiFiSaved, callback_submenu_choices, app);
     submenu_add_item(app->submenu_main, "Commands", FlipWiFiSubmenuIndexCommands, callback_submenu_choices, app);
     submenu_add_item(app->submenu_main, "Info", FlipWiFiSubmenuIndexAbout, callback_submenu_choices, app);
 
+    app->fhttp = NULL;
+
     // Switch to the main view
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain);
 

+ 12 - 1
flip_wifi/app.c

@@ -1,5 +1,7 @@
 #include <flip_wifi.h>
 #include <alloc/flip_wifi_alloc.h>
+#include <update/update.h>
+#include <flip_storage/flip_wifi_storage.h>
 
 // Entry point for the FlipWiFi application
 int32_t flip_wifi_main(void *p)
@@ -36,11 +38,20 @@ int32_t flip_wifi_main(void *p)
         FURI_LOG_D(TAG, "Waiting for PONG");
         furi_delay_ms(100); // this causes a BusFault
     }
-    flipper_http_free(fhttp);
 
     if (counter == 0)
         easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.");
 
+    save_char("app_version", VERSION);
+
+    // for now use the catalog API until I implement caching on the server
+    if (update_is_ready(fhttp, true))
+    {
+        easy_flipper_dialog("Update Status", "Complete.\nRestart your Flipper Zero.");
+    }
+
+    flipper_http_free(fhttp);
+
     // Run the view dispatcher
     view_dispatcher_run(app->view_dispatcher);
 

+ 1 - 1
flip_wifi/application.fam

@@ -9,6 +9,6 @@ App(
     fap_icon_assets="assets",
     fap_author="JBlanked",
     fap_weburl="https://github.com/jblanked/FlipWiFi",
-    fap_version="1.4",
+    fap_version="1.5",
     fap_description="FlipperHTTP companion app.",
 )

BIN
flip_wifi/assets/01-home.png


+ 335 - 0
flip_wifi/callback/alloc.c

@@ -0,0 +1,335 @@
+#include "callback/alloc.h"
+#include <callback/callback.h>
+#include <flip_storage/flip_wifi_storage.h>
+
+bool alloc_playlist(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (!wifi_playlist)
+    {
+        wifi_playlist = (WiFiPlaylist *)malloc(sizeof(WiFiPlaylist));
+        if (!wifi_playlist)
+        {
+            FURI_LOG_E(TAG, "Failed to allocate playlist");
+            return false;
+        }
+        wifi_playlist->count = 0;
+    }
+    // Load the playlist from storage
+    if (!load_playlist(wifi_playlist))
+    {
+        FURI_LOG_E(TAG, "Failed to load playlist");
+
+        // playlist is empty?
+        submenu_reset(app->submenu_wifi);
+        submenu_set_header(app->submenu_wifi, "Saved APs");
+        submenu_add_item(app->submenu_wifi, "[Add Network]", FlipWiFiSubmenuIndexWiFiSavedAddSSID, callback_submenu_choices, app);
+    }
+    else
+    {
+        // Update the submenu
+        callback_redraw_submenu_saved(app);
+    }
+    return true;
+}
+
+bool alloc_submenus(void *context, uint32_t view)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    switch (view)
+    {
+    case FlipWiFiViewSubmenuScan:
+        if (!app->submenu_wifi)
+        {
+            if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "WiFi Nearby", callback_to_submenu_main, &app->view_dispatcher))
+            {
+                return false;
+            }
+            if (!app->submenu_wifi)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate submenu for WiFi Scan");
+                return false;
+            }
+        }
+        return true;
+    case FlipWiFiViewSubmenuSaved:
+        if (!app->submenu_wifi)
+        {
+            if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "Saved APs", callback_to_submenu_main, &app->view_dispatcher))
+            {
+                return false;
+            }
+            if (!app->submenu_wifi)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate submenu for WiFi Saved");
+                return false;
+            }
+            if (!alloc_playlist(app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate playlist");
+                return false;
+            }
+        }
+        return true;
+    case FlipWiFiViewSubmenuCommands:
+        if (!app->submenu_wifi)
+        {
+            if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "Fast Commands", callback_to_submenu_main, &app->view_dispatcher))
+            {
+                return false;
+            }
+            if (!app->submenu_wifi)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate submenu for Commands");
+                return false;
+            }
+            //  PING, LIST, WIFI/LIST, IP/ADDRESS, and WIFI/IP.
+            submenu_add_item(app->submenu_wifi, "[CUSTOM]", FlipWiFiSubmenuIndexFastCommandStart + 0, callback_submenu_choices, app);
+            submenu_add_item(app->submenu_wifi, "PING", FlipWiFiSubmenuIndexFastCommandStart + 1, callback_submenu_choices, app);
+            submenu_add_item(app->submenu_wifi, "LIST", FlipWiFiSubmenuIndexFastCommandStart + 2, callback_submenu_choices, app);
+            submenu_add_item(app->submenu_wifi, "IP/ADDRESS", FlipWiFiSubmenuIndexFastCommandStart + 3, callback_submenu_choices, app);
+            submenu_add_item(app->submenu_wifi, "WIFI/IP", FlipWiFiSubmenuIndexFastCommandStart + 4, callback_submenu_choices, app);
+            submenu_add_item(app->submenu_wifi, "WIFI/AP", FlipWiFiSubmenuIndexFastCommandStart + 5, callback_submenu_choices, app);
+        }
+        return true;
+    case FlipWiFiViewSubmenuAP:
+        if (!app->submenu_wifi)
+        {
+            if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "AP Mode", callback_to_submenu_main, &app->view_dispatcher))
+            {
+                return false;
+            }
+            if (!app->submenu_wifi)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate submenu for AP Mode");
+                return false;
+            }
+            // start, set SSID, set HTML
+            submenu_add_item(app->submenu_wifi, "Start AP", FlipWiFiSubmenuIndexWiFiAPStart, callback_submenu_choices, app);
+            submenu_add_item(app->submenu_wifi, "Set SSID", FlipWiFiSubmenuIndexWiFiAPSetSSID, callback_submenu_choices, app);
+            submenu_add_item(app->submenu_wifi, "Change HTML", FlipWiFiSubmenuIndexWiFiAPSetHTML, callback_submenu_choices, app);
+        }
+        return true;
+    }
+    return false;
+}
+
+bool alloc_text_box(FlipWiFiApp *app)
+{
+    furi_check(app, "FlipWiFiApp is NULL");
+    if (app->textbox)
+    {
+        FURI_LOG_E(TAG, "Text box already allocated");
+        return false;
+    }
+    if (!easy_flipper_set_text_box(
+            &app->textbox,
+            FlipWiFiViewWiFiAP,
+            app->fhttp->last_response_str && furi_string_size(app->fhttp->last_response_str) > 0 ? (char *)furi_string_get_cstr(app->fhttp->last_response_str) : "AP Connected... please wait",
+            false,
+            callback_submenu_ap,
+            &app->view_dispatcher))
+    {
+        FURI_LOG_E(TAG, "Failed to allocate text box");
+        return false;
+    }
+    if (app->timer == NULL)
+    {
+        app->timer = furi_timer_alloc(callback_timer_callback, FuriTimerTypePeriodic, app);
+    }
+    furi_timer_start(app->timer, 250);
+    return app->textbox != NULL;
+}
+
+bool alloc_text_inputs(void *context, uint32_t view)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    app->uart_text_input_buffer_size = MAX_SSID_LENGTH;
+    if (!app->uart_text_input_buffer)
+    {
+        if (!easy_flipper_set_buffer(&app->uart_text_input_buffer, app->uart_text_input_buffer_size))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate text input buffer");
+            return false;
+        }
+        if (!app->uart_text_input_buffer)
+        {
+            FURI_LOG_E(TAG, "Failed to allocate text input buffer");
+            return false;
+        }
+    }
+    if (!app->uart_text_input_temp_buffer)
+    {
+        if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate text input temp buffer");
+            return false;
+        }
+        if (!app->uart_text_input_temp_buffer)
+        {
+            FURI_LOG_E(TAG, "Failed to allocate text input temp buffer");
+            return false;
+        }
+    }
+    switch (view)
+    {
+    case FlipWiFiViewTextInputScan:
+        if (!app->uart_text_input)
+        {
+            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter WiFi Password", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, callback_text_updated_password_scan, callback_to_submenu_scan, &app->view_dispatcher, app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Scan");
+                return false;
+            }
+            if (!app->uart_text_input)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Scan");
+                return false;
+            }
+        }
+        return true;
+    case FlipWiFiViewTextInputSaved:
+        if (!app->uart_text_input)
+        {
+            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter WiFi Password", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, callback_text_updated_password_saved, callback_to_submenu_saved, &app->view_dispatcher, app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved");
+                return false;
+            }
+            if (!app->uart_text_input)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved");
+                return false;
+            }
+        }
+        return true;
+    case FlipWiFiViewTextInputSavedAddSSID:
+        if (!app->uart_text_input)
+        {
+            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter SSID", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, callback_text_updated_add_ssid, callback_to_submenu_saved, &app->view_dispatcher, app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add SSID");
+                return false;
+            }
+            if (!app->uart_text_input)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add SSID");
+                return false;
+            }
+        }
+        return true;
+    case FlipWiFiViewTextInputSavedAddPassword:
+        if (!app->uart_text_input)
+        {
+            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter Password", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, callback_text_updated_add_password, callback_to_submenu_saved, &app->view_dispatcher, app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password");
+                return false;
+            }
+            if (!app->uart_text_input)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password");
+                return false;
+            }
+        }
+        return true;
+    case FlipWiFiSubmenuIndexFastCommandStart:
+        if (!app->uart_text_input)
+        {
+            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter Command", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, callback_custom_command_updated, callback_to_submenu_saved, &app->view_dispatcher, app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command");
+                return false;
+            }
+            if (!app->uart_text_input)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command");
+                return false;
+            }
+        }
+        return true;
+    case FlipWiFiSubmenuIndexWiFiAPSetSSID:
+        if (!app->uart_text_input)
+        {
+            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter AP SSID", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, callback_ap_ssid_updated, callback_to_submenu_saved, &app->view_dispatcher, app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command");
+                return false;
+            }
+            if (!app->uart_text_input)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command");
+                return false;
+            }
+        }
+        return true;
+    }
+    return false;
+}
+
+bool alloc_views(void *context, uint32_t view)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    switch (view)
+    {
+    case FlipWiFiViewWiFiScan:
+        if (!app->view_wifi)
+        {
+            if (!easy_flipper_set_view(&app->view_wifi, FlipWiFiViewGeneric, callback_view_draw_callback_scan, callback_view_input_callback_scan, callback_to_submenu_scan, &app->view_dispatcher, app))
+            {
+                return false;
+            }
+            if (!app->view_wifi)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate view for WiFi Scan");
+                return false;
+            }
+        }
+        return true;
+    case FlipWiFiViewWiFiSaved:
+        if (!app->view_wifi)
+        {
+            if (!easy_flipper_set_view(&app->view_wifi, FlipWiFiViewGeneric, callback_view_draw_callback_saved, callback_view_input_callback_saved, callback_to_submenu_saved, &app->view_dispatcher, app))
+            {
+                return false;
+            }
+            if (!app->view_wifi)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate view for WiFi Scan");
+                return false;
+            }
+        }
+        return true;
+    default:
+        return false;
+    }
+}
+
+bool alloc_widgets(void *context, uint32_t widget)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    switch (widget)
+    {
+    case FlipWiFiViewAbout:
+        if (!app->widget_info)
+        {
+            if (!easy_flipper_set_widget(&app->widget_info, FlipWiFiViewAbout, "FlipWiFi v1.4\n-----\nFlipperHTTP companion app.\nScan and save WiFi networks.\n-----\nwww.github.com/jblanked", callback_to_submenu_main, &app->view_dispatcher))
+            {
+                return false;
+            }
+            if (!app->widget_info)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate widget for About");
+                return false;
+            }
+        }
+        return true;
+    default:
+        return false;
+    }
+}

+ 9 - 0
flip_wifi/callback/alloc.h

@@ -0,0 +1,9 @@
+#pragma once
+#include <flip_wifi.h>
+
+bool alloc_playlist(void *context);
+bool alloc_submenus(void *context, uint32_t view);
+bool alloc_text_box(FlipWiFiApp *app);
+bool alloc_text_inputs(void *context, uint32_t view);
+bool alloc_views(void *context, uint32_t view);
+bool alloc_widgets(void *context, uint32_t widget);

+ 879 - 0
flip_wifi/callback/callback.c

@@ -0,0 +1,879 @@
+#include <callback/callback.h>
+#include <callback/loader.h>
+#include <callback/free.h>
+#include <callback/alloc.h>
+
+uint32_t callback_exit_app(void *context)
+{
+    UNUSED(context);
+    return VIEW_NONE;
+}
+
+uint32_t callback_submenu_ap(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (app->timer)
+    {
+        furi_timer_stop(app->timer);
+        furi_timer_free(app->timer);
+        app->timer = NULL;
+    }
+    back_from_ap = true;
+    return FlipWiFiViewSubmenu;
+}
+
+uint32_t callback_to_submenu_main(void *context)
+{
+    UNUSED(context);
+    ssid_index = 0;
+    return FlipWiFiViewSubmenuMain;
+}
+uint32_t callback_to_submenu_scan(void *context)
+{
+    UNUSED(context);
+    ssid_index = 0;
+    return FlipWiFiViewSubmenu;
+}
+uint32_t callback_to_submenu_saved(void *context)
+{
+    UNUSED(context);
+    ssid_index = 0;
+    return FlipWiFiViewSubmenu;
+}
+
+void callback_custom_command_updated(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (!app->fhttp)
+    {
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL");
+        return;
+    }
+    if (!app->uart_text_input_temp_buffer)
+    {
+        FURI_LOG_E(TAG, "Text input buffer is NULL");
+        return;
+    }
+    if (!app->uart_text_input_temp_buffer[0])
+    {
+        FURI_LOG_E(TAG, "Text input buffer is empty");
+        return;
+    }
+    // Send the custom command
+    flipper_http_send_data(app->fhttp, app->uart_text_input_temp_buffer);
+    uint32_t timeout = 50; // 5 seconds / 100ms iterations
+    while ((app->fhttp->last_response == NULL || strlen(app->fhttp->last_response) == 0) && timeout > 0)
+    {
+        furi_delay_ms(100);
+        timeout--;
+    }
+    // Switch to the view
+    char response[100];
+    snprintf(response, sizeof(response), "%s", app->fhttp->last_response);
+    easy_flipper_dialog("", response);
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+}
+void callback_ap_ssid_updated(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (!app->uart_text_input_temp_buffer)
+    {
+        FURI_LOG_E(TAG, "Text input buffer is NULL");
+        return;
+    }
+    if (!app->uart_text_input_temp_buffer[0])
+    {
+        FURI_LOG_E(TAG, "Text input buffer is empty");
+        return;
+    }
+    save_char("ap_ssid", app->uart_text_input_temp_buffer);
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+}
+
+void callback_redraw_submenu_saved(void *context)
+{
+    // re draw the saved submenu
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (!app->submenu_wifi)
+    {
+        FURI_LOG_E(TAG, "Submenu is NULL");
+        return;
+    }
+    if (!wifi_playlist)
+    {
+        FURI_LOG_E(TAG, "WiFi Playlist is NULL");
+        return;
+    }
+    submenu_reset(app->submenu_wifi);
+    submenu_set_header(app->submenu_wifi, "Saved APs");
+    submenu_add_item(app->submenu_wifi, "[Add Network]", FlipWiFiSubmenuIndexWiFiSavedAddSSID, callback_submenu_choices, app);
+    for (size_t i = 0; i < wifi_playlist->count; i++)
+    {
+        submenu_add_item(app->submenu_wifi, wifi_playlist->ssids[i], FlipWiFiSubmenuIndexWiFiSavedStart + i, callback_submenu_choices, app);
+    }
+}
+// Callback for drawing the main screen
+void callback_view_draw_callback_scan(Canvas *canvas, void *model)
+{
+    UNUSED(model);
+    canvas_clear(canvas);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 0, 10, ssid_list[ssid_index]);
+    canvas_draw_icon(canvas, 0, 53, &I_ButtonBACK_10x8);
+    canvas_draw_str_aligned(canvas, 12, 54, AlignLeft, AlignTop, "Back");
+    canvas_draw_icon(canvas, 96, 53, &I_ButtonRight_4x7);
+    canvas_draw_str_aligned(canvas, 103, 54, AlignLeft, AlignTop, "Add");
+}
+
+void callback_view_draw_callback_saved(Canvas *canvas, void *model)
+{
+    UNUSED(model);
+    canvas_clear(canvas);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 0, 10, current_ssid);
+    canvas_set_font(canvas, FontSecondary);
+    char password[72];
+    snprintf(password, sizeof(password), "Pass: %s", current_password);
+    canvas_draw_str(canvas, 0, 20, password);
+    canvas_draw_icon(canvas, 0, 54, &I_ButtonLeft_4x7);
+    canvas_draw_str_aligned(canvas, 7, 54, AlignLeft, AlignTop, "Delete");
+    canvas_draw_icon(canvas, 37, 53, &I_ButtonBACK_10x8);
+    canvas_draw_str_aligned(canvas, 49, 54, AlignLeft, AlignTop, "Back");
+    canvas_draw_icon(canvas, 73, 54, &I_ButtonOK_7x7);
+    canvas_draw_str_aligned(canvas, 81, 54, AlignLeft, AlignTop, "Set");
+    canvas_draw_icon(canvas, 100, 54, &I_ButtonRight_4x7);
+    canvas_draw_str_aligned(canvas, 107, 54, AlignLeft, AlignTop, "Edit");
+}
+
+// Input callback for the view (async input handling)
+bool callback_view_input_callback_scan(InputEvent *event, void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    if (event->type == InputTypePress && event->key == InputKeyRight)
+    {
+        // switch to text input to set password
+        free_text_inputs(app);
+        if (!alloc_text_inputs(app, FlipWiFiViewTextInputScan))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password");
+            return false;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
+        return true;
+    }
+    return false;
+}
+// Input callback for the view (async input handling)
+bool callback_view_input_callback_saved(InputEvent *event, void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (event->type == InputTypePress && event->key == InputKeyRight)
+    {
+        // set text input buffer as the selected password
+        strncpy(app->uart_text_input_temp_buffer, wifi_playlist->passwords[ssid_index], app->uart_text_input_buffer_size);
+        // switch to text input to set password
+        free_text_inputs(app);
+        if (!alloc_text_inputs(app, FlipWiFiViewTextInputSaved))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved");
+            return false;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
+        return true;
+    }
+    else if (event->type == InputTypePress && event->key == InputKeyOk)
+    {
+        // save the settings
+        save_settings(wifi_playlist->ssids[ssid_index], wifi_playlist->passwords[ssid_index]);
+
+        // initialize uart
+        FlipperHTTP *fhttp = flipper_http_alloc();
+        if (!fhttp)
+        {
+            easy_flipper_dialog("[ERROR]", "Failed to initialize flipper http");
+            return false;
+        }
+
+        // clear response
+        if (fhttp->last_response)
+            snprintf(fhttp->last_response, RX_BUF_SIZE, "%s", "");
+
+        if (!flipper_http_save_wifi(fhttp, wifi_playlist->ssids[ssid_index], wifi_playlist->passwords[ssid_index]))
+        {
+            easy_flipper_dialog("[ERROR]", "Failed to save WiFi settings");
+            return false;
+        }
+
+        while (!fhttp->last_response || strlen(fhttp->last_response) == 0)
+        {
+            furi_delay_ms(100);
+        }
+
+        flipper_http_free(fhttp);
+
+        // check success (if [SUCCESS] is in the response)
+        if (strstr(fhttp->last_response, "[SUCCESS]") == NULL)
+        {
+            char response[512];
+            snprintf(response, sizeof(response), "Failed to save WiFi settings:\n%s", fhttp->last_response);
+            easy_flipper_dialog("[ERROR]", response);
+            return false;
+        }
+
+        easy_flipper_dialog("[SUCCESS]", "All FlipperHTTP apps will now\nuse the selected network.");
+        return true;
+    }
+    else if (event->type == InputTypePress && event->key == InputKeyLeft)
+    {
+        // shift the remaining ssids and passwords
+        for (uint32_t i = ssid_index; i < wifi_playlist->count - 1; i++)
+        {
+            // Use strncpy to prevent buffer overflows and ensure null termination
+            strncpy(wifi_playlist->ssids[i], wifi_playlist->ssids[i + 1], MAX_SSID_LENGTH - 1);
+            wifi_playlist->ssids[i][MAX_SSID_LENGTH - 1] = '\0'; // Ensure null-termination
+
+            strncpy(wifi_playlist->passwords[i], wifi_playlist->passwords[i + 1], MAX_SSID_LENGTH - 1);
+            wifi_playlist->passwords[i][MAX_SSID_LENGTH - 1] = '\0'; // Ensure null-termination
+
+            // Shift ssid_list
+            ssid_list[i] = ssid_list[i + 1];
+        }
+        wifi_playlist->count--;
+
+        // delete the last ssid and password
+        wifi_playlist->ssids[wifi_playlist->count][0] = '\0';
+        wifi_playlist->passwords[wifi_playlist->count][0] = '\0';
+
+        // save the playlist to storage
+        save_playlist(wifi_playlist);
+
+        // re draw the saved submenu
+        callback_redraw_submenu_saved(app);
+        // switch back to the saved view
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+        return true;
+    }
+    return false;
+}
+static bool callback_set_html()
+{
+    DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS);
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, ".js", NULL);
+
+    browser_options.extension = "html";
+    browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX;
+    browser_options.skip_assets = true;
+    browser_options.hide_dot_files = true;
+    browser_options.icon = NULL;
+    browser_options.hide_ext = false;
+
+    FuriString *marauder_html_path = furi_string_alloc_set_str("/ext/apps_data/marauder/html");
+    if (!marauder_html_path)
+    {
+        furi_record_close(RECORD_DIALOGS);
+        return false;
+    }
+
+    if (dialog_file_browser_show(dialogs, marauder_html_path, marauder_html_path, &browser_options))
+    {
+        // Store the selected script file path
+        const char *file_path = furi_string_get_cstr(marauder_html_path);
+        save_char("ap_html_path", file_path);
+    }
+
+    furi_string_free(marauder_html_path);
+    furi_record_close(RECORD_DIALOGS);
+    return true;
+}
+static bool callback_run_ap_mode(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (!app->fhttp)
+    {
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL");
+        return false;
+    }
+    char ssid[64];
+    if (!load_char("ap_ssid", ssid, sizeof(ssid)))
+    {
+        FURI_LOG_E(TAG, "Failed to load AP SSID");
+        return false;
+    }
+    // clear response
+    if (app->fhttp->last_response)
+        snprintf(app->fhttp->last_response, RX_BUF_SIZE, "%s", "");
+    char stat_command[128];
+    snprintf(stat_command, sizeof(stat_command), "[WIFI/AP]{\"ssid\":\"%s\"}", ssid);
+    if (!flipper_http_send_data(app->fhttp, stat_command))
+    {
+        FURI_LOG_E(TAG, "Failed to start AP mode");
+        return false;
+    }
+    Loading *loading = loading_alloc();
+    int32_t loading_view_id = 87654321; // Random ID
+    view_dispatcher_add_view(app->view_dispatcher, loading_view_id, loading_get_view(loading));
+    view_dispatcher_switch_to_view(app->view_dispatcher, loading_view_id);
+    while (app->fhttp->last_response == NULL || strlen(app->fhttp->last_response) == 0)
+    {
+        furi_delay_ms(100);
+    }
+    // check success (if [AP/CONNECTED] is in the response)
+    if (strstr(app->fhttp->last_response, "[AP/CONNECTED]") != NULL)
+    {
+        // send the HTML file
+        char html_path[128];
+        if (!load_char("ap_html_path", html_path, sizeof(html_path)))
+        {
+            FURI_LOG_E(TAG, "Failed to load HTML path");
+            return false;
+        }
+        flipper_http_send_data(app->fhttp, "[WIFI/AP/UPDATE]");
+        furi_delay_ms(1000);
+        FuriString *html_content = flipper_http_load_from_file(html_path);
+        if (html_content == NULL || furi_string_size(html_content) == 0)
+        {
+            FURI_LOG_E(TAG, "Failed to load HTML file");
+            if (html_content)
+                furi_string_free(html_content);
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+            view_dispatcher_remove_view(app->view_dispatcher, loading_view_id);
+            loading_free(loading);
+            loading = NULL;
+            return false;
+        }
+        furi_string_cat_str(html_content, "\n");
+        const char *send_buffer = furi_string_get_cstr(html_content);
+        const size_t send_buffer_size = furi_string_size(html_content);
+
+        app->fhttp->state = SENDING;
+        size_t offset = 0;
+        while (offset < send_buffer_size)
+        {
+            size_t chunk_size = send_buffer_size - offset > 64 ? 64 : send_buffer_size - offset;
+            furi_hal_serial_tx(app->fhttp->serial_handle, (const uint8_t *)(send_buffer + offset), chunk_size);
+            offset += chunk_size;
+            furi_delay_ms(50); // cant go faster than this, no matter the chunk size
+        }
+        // send the [WIFI/AP/UPDATE/END] command
+        flipper_http_send_data(app->fhttp, "[WIFI/AP/UPDATE/END]");
+        app->fhttp->state = IDLE;
+        furi_string_free(html_content);
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+        view_dispatcher_remove_view(app->view_dispatcher, loading_view_id);
+        loading_free(loading);
+        loading = NULL;
+        return true;
+    }
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+    view_dispatcher_remove_view(app->view_dispatcher, loading_view_id);
+    loading_free(loading);
+    loading = NULL;
+    return false;
+}
+void callback_timer_callback(void *context)
+{
+    furi_check(context, "callback_timer_callback: Context is NULL");
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, FlipWiFiCustomEventAP);
+}
+
+static size_t last_response_len = 0;
+static void update_text_box(FlipWiFiApp *app)
+{
+    furi_check(app, "FlipWiFiApp is NULL");
+    furi_check(app->textbox, "Text_box is NULL");
+    if (app->fhttp)
+    {
+        if (!app->fhttp->last_response_str || furi_string_size(app->fhttp->last_response_str) == 0)
+        {
+            text_box_reset(app->textbox);
+            text_box_set_focus(app->textbox, TextBoxFocusEnd);
+            text_box_set_font(app->textbox, TextBoxFontText);
+            text_box_set_text(app->textbox, "AP Connected... please wait");
+        }
+        else if (furi_string_size(app->fhttp->last_response_str) != last_response_len)
+        {
+            text_box_reset(app->textbox);
+            text_box_set_focus(app->textbox, TextBoxFocusEnd);
+            text_box_set_font(app->textbox, TextBoxFontText);
+            last_response_len = furi_string_size(app->fhttp->last_response_str);
+            text_box_set_text(app->textbox, furi_string_get_cstr(app->fhttp->last_response_str));
+        }
+    }
+}
+
+static void callback_loader_process_callback(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app, "FlipWiFiApp is NULL");
+    update_text_box(app);
+}
+
+static bool callback_custom_event_callback(void *context, uint32_t index)
+{
+    furi_check(context, "callback_custom_event_callback: Context is NULL");
+    switch (index)
+    {
+    case FlipWiFiCustomEventAP:
+        callback_loader_process_callback(context);
+        return true;
+    default:
+        FURI_LOG_E(TAG, "callback_custom_event_callback. Unknown index: %ld", index);
+        return false;
+    }
+}
+// scan for wifi ad parse the results
+static bool callback_scan(FlipperHTTP *fhttp)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL");
+        return false;
+    }
+
+    // storage setup
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+
+    snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data/scan.txt");
+    storage_simply_remove_recursive(storage, fhttp->file_path); // ensure the file is empty
+
+    // ensure flip_wifi directory is there
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi");
+    storage_common_mkdir(storage, directory_path);
+
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data");
+    storage_common_mkdir(storage, directory_path);
+
+    furi_record_close(RECORD_STORAGE);
+
+    fhttp->just_started = true;
+    fhttp->save_received_data = true;
+    FURI_LOG_I(TAG, "callback_scan: Sending scan command");
+    return flipper_http_send_command(fhttp, HTTP_CMD_SCAN);
+}
+
+static bool callback_handle_scan(FlipperHTTP *fhttp, void *context)
+{
+    FURI_LOG_I(TAG, "callback_handle_scan1");
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    FURI_LOG_I(TAG, "callback_handle_scan2");
+    if (!fhttp || !context)
+    {
+        FURI_LOG_E(TAG, "FlipperHTTP or context is NULL");
+        return false;
+    }
+
+    // load the received data from the saved file
+    FuriString *scan_data = flipper_http_load_from_file(fhttp->file_path);
+    if (scan_data == NULL)
+    {
+        FURI_LOG_E(TAG, "Failed to load received data from file.");
+        return false;
+    }
+
+    uint8_t ssid_count = 0;
+
+    for (uint8_t i = 0; i < MAX_SCAN_NETWORKS; i++)
+    {
+        char *ssid_item = get_json_array_value("networks", i, furi_string_get_cstr(scan_data));
+        if (ssid_item == NULL)
+        {
+            // end of the list
+            break;
+        }
+        ssid_list[i] = malloc(MAX_SSID_LENGTH);
+        if (ssid_list[i] == NULL)
+        {
+            FURI_LOG_E(TAG, "Failed to allocate memory for SSID");
+            furi_string_free(scan_data);
+            return false;
+        }
+        snprintf(ssid_list[i], MAX_SSID_LENGTH, "%s", ssid_item);
+        free(ssid_item);
+        ssid_count++;
+    }
+
+    // Add each SSID as a submenu item
+    submenu_reset(app->submenu_wifi);
+    submenu_set_header(app->submenu_wifi, "WiFi Nearby");
+    for (uint8_t i = 0; i < ssid_count; i++)
+    {
+        char *ssid_item = ssid_list[i];
+        if (ssid_item == NULL)
+        {
+            // end of the list
+            break;
+        }
+        char ssid[64];
+        snprintf(ssid, sizeof(ssid), "%s", ssid_item);
+        submenu_add_item(app->submenu_wifi, ssid, FlipWiFiSubmenuIndexWiFiScanStart + i, callback_submenu_choices, app);
+    }
+    furi_string_free(scan_data);
+    FURI_LOG_I(TAG, "Scan completed. Found %d networks.", ssid_count);
+    return true;
+}
+
+void callback_submenu_choices(void *context, uint32_t index)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    switch (index)
+    {
+    case FlipWiFiSubmenuIndexWiFiScan:
+        free_all(app);
+        if (!alloc_submenus(app, FlipWiFiViewSubmenuScan))
+        {
+            easy_flipper_dialog("[ERROR]", "Failed to allocate submenus for WiFi Scan");
+            return;
+        }
+        app->fhttp = flipper_http_alloc();
+        if (!app->fhttp)
+        {
+            FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
+            easy_flipper_dialog("Error", "Failed to start UART.\nUART is likely busy or device\nis not connected.");
+            return;
+        }
+        if (callback_scan(app->fhttp))
+        {
+            furi_delay_ms(100); // wait for the command to be sent
+            // wait for the scan to complete
+            Loading *loading = loading_alloc();
+            int32_t loading_view_id = 87654321; // Random ID
+            view_dispatcher_add_view(app->view_dispatcher, loading_view_id, loading_get_view(loading));
+            view_dispatcher_switch_to_view(app->view_dispatcher, loading_view_id);
+            while (app->fhttp->state != IDLE)
+            {
+                furi_delay_ms(100);
+            }
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+            if (!callback_handle_scan(app->fhttp, app))
+            {
+                FURI_LOG_E(TAG, "Failed to handle scan");
+                easy_flipper_dialog("[ERROR]", "Failed to handle scan");
+                return;
+            }
+            view_dispatcher_remove_view(app->view_dispatcher, loading_view_id);
+            loading_free(loading);
+            loading = NULL;
+            flipper_http_free(app->fhttp);
+        }
+        else
+        {
+            flipper_http_free(app->fhttp);
+            easy_flipper_dialog("[ERROR]", "Failed to scan for WiFi networks");
+            return;
+        }
+        break;
+    case FlipWiFiSubmenuIndexWiFiSaved:
+        free_all(app);
+        if (!alloc_submenus(app, FlipWiFiViewSubmenuSaved))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate submenus for WiFi Saved");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+        break;
+    case FlipWiFiSubmenuIndexAbout:
+        free_all(app);
+        if (!alloc_widgets(app, FlipWiFiViewAbout))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate widget for About");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewAbout);
+        break;
+    case FlipWiFiSubmenuIndexWiFiSavedAddSSID:
+        free_text_inputs(app);
+        if (!alloc_text_inputs(app, FlipWiFiViewTextInputSavedAddSSID))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
+        break;
+    case FlipWiFiSubmenuIndexWiFiAP:
+        free_all(app);
+        if (!alloc_submenus(app, FlipWiFiViewSubmenuAP))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate submenus for APs");
+            return;
+        }
+        app->fhttp = flipper_http_alloc();
+        if (!app->fhttp)
+        {
+            FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
+            easy_flipper_dialog("Error", "Failed to start UART.\nUART is likely busy or device\nis not connected.");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+        break;
+    case FlipWiFiSubmenuIndexWiFiAPStart:
+        // start AP
+        view_dispatcher_set_custom_event_callback(app->view_dispatcher, callback_custom_event_callback);
+        // send to AP View to see the responses
+        if (!callback_run_ap_mode(app))
+        {
+            easy_flipper_dialog("[ERROR]", "Failed to start AP mode");
+            return;
+        }
+        free_text_box(app);
+        if (!alloc_text_box(app))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate text box");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewWiFiAP);
+        break;
+    case FlipWiFiSubmenuIndexWiFiAPSetSSID:
+        // set SSID
+        free_text_inputs(app);
+        if (!alloc_text_inputs(app, FlipWiFiSubmenuIndexWiFiAPSetSSID))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
+        break;
+    case FlipWiFiSubmenuIndexWiFiAPSetHTML:
+        // set HTML
+        callback_set_html();
+        break;
+    case FlipWiFiSubmenuIndexCommands:
+        free_all(app);
+        if (!alloc_submenus(app, FlipWiFiViewSubmenuCommands))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate submenus for Commands");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+        break;
+    case FlipWiFiSubmenuIndexFastCommandStart ... FlipWiFiSubmenuIndexFastCommandStart + 4:
+        // initialize uart
+        if (!app->fhttp)
+        {
+            app->fhttp = flipper_http_alloc();
+            if (!app->fhttp)
+            {
+                FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
+                easy_flipper_dialog("Error", "Failed to start UART.\nUART is likely busy or device\nis not connected.");
+                return;
+            }
+        }
+        // Handle fast commands
+        switch (index)
+        {
+        case FlipWiFiSubmenuIndexFastCommandStart + 0:
+            // CUSTOM - send to text input and return
+            free_text_inputs(app);
+            if (!alloc_text_inputs(app, FlipWiFiSubmenuIndexFastCommandStart))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command");
+                return;
+            }
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
+            return;
+        case FlipWiFiSubmenuIndexFastCommandStart + 1:
+            // PING
+            flipper_http_send_command(app->fhttp, HTTP_CMD_PING);
+            break;
+        case FlipWiFiSubmenuIndexFastCommandStart + 2:
+            // LIST
+            flipper_http_send_command(app->fhttp, HTTP_CMD_LIST_COMMANDS);
+            break;
+        case FlipWiFiSubmenuIndexFastCommandStart + 3:
+            // IP/ADDRESS
+            flipper_http_send_command(app->fhttp, HTTP_CMD_IP_ADDRESS);
+            break;
+        case FlipWiFiSubmenuIndexFastCommandStart + 4:
+            // WIFI/IP
+            flipper_http_send_command(app->fhttp, HTTP_CMD_IP_WIFI);
+
+            break;
+        default:
+            break;
+        }
+        while (app->fhttp->last_response == NULL || strlen(app->fhttp->last_response) == 0)
+        {
+            // Wait for the response
+            furi_delay_ms(100);
+        }
+        if (app->fhttp->last_response != NULL)
+        {
+            char response[100];
+            snprintf(response, sizeof(response), "%s", app->fhttp->last_response);
+            easy_flipper_dialog("", response);
+        }
+        flipper_http_free(app->fhttp);
+        break;
+    case 100 ... 199:
+        ssid_index = index - FlipWiFiSubmenuIndexWiFiScanStart;
+        free_views(app);
+        if (!alloc_views(app, FlipWiFiViewWiFiScan))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate views for WiFi Scan");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewGeneric);
+        break;
+    case 200 ... 299:
+        ssid_index = index - FlipWiFiSubmenuIndexWiFiSavedStart;
+        free_views(app);
+        snprintf(current_ssid, sizeof(current_ssid), "%s", wifi_playlist->ssids[ssid_index]);
+        snprintf(current_password, sizeof(current_password), "%s", wifi_playlist->passwords[ssid_index]);
+        if (!alloc_views(app, FlipWiFiViewWiFiSaved))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate views for WiFi Saved");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewGeneric);
+        break;
+    default:
+        break;
+    }
+}
+
+void callback_text_updated_password_scan(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+
+    // Store the entered text with buffer size limit
+    strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size - 1);
+    // Ensure null-termination
+    app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0';
+
+    if (!alloc_playlist(app))
+    {
+        FURI_LOG_E(TAG, "Failed to allocate playlist");
+        return;
+    }
+
+    // Ensure ssid_index is valid
+    if (ssid_index >= MAX_SCAN_NETWORKS)
+    {
+        FURI_LOG_E(TAG, "Invalid ssid_index: %ld", ssid_index);
+        return;
+    }
+
+    // Check if there's space in the playlist
+    if (wifi_playlist->count >= MAX_SAVED_NETWORKS)
+    {
+        FURI_LOG_E(TAG, "Playlist is full. Cannot add more entries.");
+        return;
+    }
+
+    // Add the SSID and password to the playlist
+    snprintf(wifi_playlist->ssids[wifi_playlist->count], MAX_SSID_LENGTH, "%s", ssid_list[ssid_index]);
+    snprintf(wifi_playlist->passwords[wifi_playlist->count], MAX_SSID_LENGTH, "%s", app->uart_text_input_buffer);
+    wifi_playlist->count++;
+
+    // Save the updated playlist to storage
+    save_playlist(wifi_playlist);
+
+    // Redraw the submenu to reflect changes
+    callback_redraw_submenu_saved(app);
+
+    // Switch back to the scan view
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+}
+
+void callback_text_updated_password_saved(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+
+    // store the entered text
+    strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size);
+
+    // Ensure null-termination
+    app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0';
+
+    // update the password_saved in the playlist
+    snprintf(wifi_playlist->passwords[ssid_index], MAX_SSID_LENGTH, app->uart_text_input_buffer);
+
+    // save the playlist to storage
+    save_playlist(wifi_playlist);
+
+    // switch to back to the saved view
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+}
+
+void callback_text_updated_add_ssid(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+
+    // check if empty
+    if (strlen(app->uart_text_input_temp_buffer) == 0)
+    {
+        easy_flipper_dialog("[ERROR]", "SSID cannot be empty");
+        return;
+    }
+
+    // store the entered text
+    strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size);
+
+    // Ensure null-termination
+    app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0';
+    save_char("wifi-ssid", app->uart_text_input_buffer);
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain);
+    uart_text_input_reset(app->uart_text_input);
+    uart_text_input_set_header_text(app->uart_text_input, "Enter Password");
+    app->uart_text_input_buffer_size = MAX_SSID_LENGTH;
+    free(app->uart_text_input_buffer);
+    free(app->uart_text_input_temp_buffer);
+    easy_flipper_set_buffer(&app->uart_text_input_buffer, app->uart_text_input_buffer_size);
+    easy_flipper_set_buffer(&app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size);
+    uart_text_input_set_result_callback(app->uart_text_input, callback_text_updated_add_password, app, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, false);
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
+}
+void callback_text_updated_add_password(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+
+    // check if empty
+    if (strlen(app->uart_text_input_temp_buffer) == 0)
+    {
+        easy_flipper_dialog("[ERROR]", "Password cannot be empty");
+        return;
+    }
+
+    // store the entered text
+    strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size);
+    // Ensure null-termination
+    app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0';
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain);
+
+    save_char("wifi-password", app->uart_text_input_buffer);
+
+    char wifi_ssid[64];
+    if (!load_char("wifi-ssid", wifi_ssid, sizeof(wifi_ssid)))
+    {
+        FURI_LOG_E(TAG, "Failed to load wifi ssid");
+        return;
+    }
+
+    // add the SSID and password_scan to the playlist
+    snprintf(wifi_playlist->ssids[wifi_playlist->count], MAX_SSID_LENGTH, wifi_ssid);
+    snprintf(wifi_playlist->passwords[wifi_playlist->count], MAX_SSID_LENGTH, app->uart_text_input_buffer);
+    wifi_playlist->count++;
+
+    // save the playlist to storage
+    save_playlist(wifi_playlist);
+
+    callback_redraw_submenu_saved(app);
+
+    // switch to back to the saved view
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+}

+ 23 - 0
flip_wifi/callback/callback.h

@@ -0,0 +1,23 @@
+#pragma once
+#include <flip_wifi.h>
+#include <flip_storage/flip_wifi_storage.h>
+#include <flip_wifi_icons.h>
+
+uint32_t callback_exit_app(void *context);
+uint32_t callback_submenu_ap(void *context);
+uint32_t callback_to_submenu_main(void *context);
+uint32_t callback_to_submenu_scan(void *context);
+uint32_t callback_to_submenu_saved(void *context);
+void callback_custom_command_updated(void *context);
+void callback_ap_ssid_updated(void *context);
+void callback_redraw_submenu_saved(void *context);
+void callback_view_draw_callback_scan(Canvas *canvas, void *model);
+void callback_view_draw_callback_saved(Canvas *canvas, void *model);
+bool callback_view_input_callback_scan(InputEvent *event, void *context);
+bool callback_view_input_callback_saved(InputEvent *event, void *context);
+void callback_timer_callback(void *context);
+void callback_submenu_choices(void *context, uint32_t index);
+void callback_text_updated_password_scan(void *context);
+void callback_text_updated_password_saved(void *context);
+void callback_text_updated_add_ssid(void *context);
+void callback_text_updated_add_password(void *context);

+ 0 - 1004
flip_wifi/callback/flip_wifi_callback.c

@@ -1,1004 +0,0 @@
-#include <callback/flip_wifi_callback.h>
-
-static char *ssid_list[64];
-static uint32_t ssid_index = 0;
-static char current_ssid[64];
-static char current_password[64];
-
-static void flip_wifi_redraw_submenu_saved(void *context);
-static void flip_wifi_view_draw_callback_scan(Canvas *canvas, void *model);
-static void flip_wifi_view_draw_callback_saved(Canvas *canvas, void *model);
-static bool flip_wifi_view_input_callback_scan(InputEvent *event, void *context);
-static bool flip_wifi_view_input_callback_saved(InputEvent *event, void *context);
-static uint32_t callback_to_submenu_saved(void *context);
-static uint32_t callback_to_submenu_scan(void *context);
-static uint32_t callback_to_submenu_main(void *context);
-
-void flip_wifi_text_updated_password_scan(void *context);
-void flip_wifi_text_updated_password_saved(void *context);
-void flip_wifi_text_updated_add_ssid(void *context);
-void flip_wifi_text_updated_add_password(void *context);
-
-static bool flip_wifi_alloc_playlist(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return false;
-    }
-    if (!wifi_playlist)
-    {
-        wifi_playlist = (WiFiPlaylist *)malloc(sizeof(WiFiPlaylist));
-        if (!wifi_playlist)
-        {
-            FURI_LOG_E(TAG, "Failed to allocate playlist");
-            return false;
-        }
-        wifi_playlist->count = 0;
-    }
-    // Load the playlist from storage
-    if (!load_playlist(wifi_playlist))
-    {
-        FURI_LOG_E(TAG, "Failed to load playlist");
-
-        // playlist is empty?
-        submenu_reset(app->submenu_wifi);
-        submenu_set_header(app->submenu_wifi, "Saved APs");
-        submenu_add_item(app->submenu_wifi, "[Add Network]", FlipWiFiSubmenuIndexWiFiSavedAddSSID, callback_submenu_choices, app);
-    }
-    else
-    {
-        // Update the submenu
-        flip_wifi_redraw_submenu_saved(app);
-    }
-    return true;
-}
-static void flip_wifi_free_playlist(void)
-{
-    if (wifi_playlist)
-    {
-        free(wifi_playlist);
-        wifi_playlist = NULL;
-    }
-}
-
-static bool flip_wifi_alloc_views(void *context, uint32_t view)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return false;
-    }
-    switch (view)
-    {
-    case FlipWiFiViewWiFiScan:
-        if (!app->view_wifi)
-        {
-            if (!easy_flipper_set_view(&app->view_wifi, FlipWiFiViewGeneric, flip_wifi_view_draw_callback_scan, flip_wifi_view_input_callback_scan, callback_to_submenu_scan, &app->view_dispatcher, app))
-            {
-                return false;
-            }
-            if (!app->view_wifi)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate view for WiFi Scan");
-                return false;
-            }
-        }
-        return true;
-    case FlipWiFiViewWiFiSaved:
-        if (!app->view_wifi)
-        {
-            if (!easy_flipper_set_view(&app->view_wifi, FlipWiFiViewGeneric, flip_wifi_view_draw_callback_saved, flip_wifi_view_input_callback_saved, callback_to_submenu_saved, &app->view_dispatcher, app))
-            {
-                return false;
-            }
-            if (!app->view_wifi)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate view for WiFi Scan");
-                return false;
-            }
-        }
-        return true;
-    default:
-        return false;
-    }
-}
-static void flip_wifi_free_views(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-    if (app->view_wifi)
-    {
-        free(app->view_wifi);
-        app->view_wifi = NULL;
-        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewGeneric);
-    }
-}
-static bool flip_wifi_alloc_widgets(void *context, uint32_t widget)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return false;
-    }
-    switch (widget)
-    {
-    case FlipWiFiViewAbout:
-        if (!app->widget_info)
-        {
-            if (!easy_flipper_set_widget(&app->widget_info, FlipWiFiViewAbout, "FlipWiFi v1.4\n-----\nFlipperHTTP companion app.\nScan and save WiFi networks.\n-----\nwww.github.com/jblanked", callback_to_submenu_main, &app->view_dispatcher))
-            {
-                return false;
-            }
-            if (!app->widget_info)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate widget for About");
-                return false;
-            }
-        }
-        return true;
-    default:
-        return false;
-    }
-}
-static void flip_wifi_free_widgets(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-    if (app->widget_info)
-    {
-        free(app->widget_info);
-        app->widget_info = NULL;
-        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewAbout);
-    }
-}
-static bool flip_wifi_alloc_submenus(void *context, uint32_t view)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return false;
-    }
-    switch (view)
-    {
-    case FlipWiFiViewSubmenuScan:
-        if (!app->submenu_wifi)
-        {
-            if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "WiFi Nearby", callback_to_submenu_main, &app->view_dispatcher))
-            {
-                return false;
-            }
-            if (!app->submenu_wifi)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate submenu for WiFi Scan");
-                return false;
-            }
-        }
-        return true;
-    case FlipWiFiViewSubmenuSaved:
-        if (!app->submenu_wifi)
-        {
-            if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "Saved APs", callback_to_submenu_main, &app->view_dispatcher))
-            {
-                return false;
-            }
-            if (!app->submenu_wifi)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate submenu for WiFi Saved");
-                return false;
-            }
-            if (!flip_wifi_alloc_playlist(app))
-            {
-                FURI_LOG_E(TAG, "Failed to allocate playlist");
-                return false;
-            }
-        }
-        return true;
-    case FlipWiFiViewSubmenuCommands:
-        if (!app->submenu_wifi)
-        {
-            if (!easy_flipper_set_submenu(&app->submenu_wifi, FlipWiFiViewSubmenu, "Fast Commands", callback_to_submenu_main, &app->view_dispatcher))
-            {
-                return false;
-            }
-            if (!app->submenu_wifi)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate submenu for Commands");
-                return false;
-            }
-            //  PING, LIST, WIFI/LIST, IP/ADDRESS, and WIFI/IP.
-            submenu_add_item(app->submenu_wifi, "[CUSTOM]", FlipWiFiSubmenuIndexFastCommandStart + 0, callback_submenu_choices, app);
-            submenu_add_item(app->submenu_wifi, "PING", FlipWiFiSubmenuIndexFastCommandStart + 1, callback_submenu_choices, app);
-            submenu_add_item(app->submenu_wifi, "LIST", FlipWiFiSubmenuIndexFastCommandStart + 2, callback_submenu_choices, app);
-            submenu_add_item(app->submenu_wifi, "IP/ADDRESS", FlipWiFiSubmenuIndexFastCommandStart + 3, callback_submenu_choices, app);
-            submenu_add_item(app->submenu_wifi, "WIFI/IP", FlipWiFiSubmenuIndexFastCommandStart + 4, callback_submenu_choices, app);
-        }
-        return true;
-    }
-    return false;
-}
-static void flip_wifi_free_submenus(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-    if (app->submenu_wifi)
-    {
-        free(app->submenu_wifi);
-        app->submenu_wifi = NULL;
-        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewSubmenu);
-    }
-}
-
-static void flip_wifi_custom_command_updated(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-    if (!app->uart_text_input_temp_buffer)
-    {
-        FURI_LOG_E(TAG, "Text input buffer is NULL");
-        return;
-    }
-    if (!app->uart_text_input_temp_buffer[0])
-    {
-        FURI_LOG_E(TAG, "Text input buffer is empty");
-        return;
-    }
-    FlipperHTTP *fhttp = flipper_http_alloc();
-    if (!fhttp)
-    {
-        FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
-        return;
-    }
-    // Send the custom command
-    flipper_http_send_data(fhttp, app->uart_text_input_temp_buffer);
-    while (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
-    {
-        furi_delay_ms(100);
-    }
-    // Switch to the view
-    char response[100];
-    snprintf(response, sizeof(response), "%s", fhttp->last_response);
-    easy_flipper_dialog("", response);
-    flipper_http_free(fhttp);
-    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
-}
-
-static bool flip_wifi_alloc_text_inputs(void *context, uint32_t view)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return false;
-    }
-    app->uart_text_input_buffer_size = MAX_SSID_LENGTH;
-    if (!app->uart_text_input_buffer)
-    {
-        if (!easy_flipper_set_buffer(&app->uart_text_input_buffer, app->uart_text_input_buffer_size))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate text input buffer");
-            return false;
-        }
-        if (!app->uart_text_input_buffer)
-        {
-            FURI_LOG_E(TAG, "Failed to allocate text input buffer");
-            return false;
-        }
-    }
-    if (!app->uart_text_input_temp_buffer)
-    {
-        if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate text input temp buffer");
-            return false;
-        }
-        if (!app->uart_text_input_temp_buffer)
-        {
-            FURI_LOG_E(TAG, "Failed to allocate text input temp buffer");
-            return false;
-        }
-    }
-    switch (view)
-    {
-    case FlipWiFiViewTextInputScan:
-        if (!app->uart_text_input)
-        {
-            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter WiFi Password", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_text_updated_password_scan, callback_to_submenu_scan, &app->view_dispatcher, app))
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Scan");
-                return false;
-            }
-            if (!app->uart_text_input)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Scan");
-                return false;
-            }
-        }
-        return true;
-    case FlipWiFiViewTextInputSaved:
-        if (!app->uart_text_input)
-        {
-            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter WiFi Password", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_text_updated_password_saved, callback_to_submenu_saved, &app->view_dispatcher, app))
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved");
-                return false;
-            }
-            if (!app->uart_text_input)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved");
-                return false;
-            }
-        }
-        return true;
-    case FlipWiFiViewTextInputSavedAddSSID:
-        if (!app->uart_text_input)
-        {
-            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter SSID", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_text_updated_add_ssid, callback_to_submenu_saved, &app->view_dispatcher, app))
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add SSID");
-                return false;
-            }
-            if (!app->uart_text_input)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add SSID");
-                return false;
-            }
-        }
-        return true;
-    case FlipWiFiViewTextInputSavedAddPassword:
-        if (!app->uart_text_input)
-        {
-            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter Password", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_text_updated_add_password, callback_to_submenu_saved, &app->view_dispatcher, app))
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password");
-                return false;
-            }
-            if (!app->uart_text_input)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password");
-                return false;
-            }
-        }
-        return true;
-    case FlipWiFiSubmenuIndexFastCommandStart:
-        if (!app->uart_text_input)
-        {
-            if (!easy_flipper_set_uart_text_input(&app->uart_text_input, FlipWiFiViewTextInput, "Enter Command", app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, flip_wifi_custom_command_updated, callback_to_submenu_saved, &app->view_dispatcher, app))
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command");
-                return false;
-            }
-            if (!app->uart_text_input)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command");
-                return false;
-            }
-        }
-        return true;
-    }
-    return false;
-}
-static void flip_wifi_free_text_inputs(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-    if (app->uart_text_input)
-    {
-        free(app->uart_text_input);
-        app->uart_text_input = NULL;
-        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewTextInput);
-    }
-    if (app->uart_text_input_buffer)
-    {
-        free(app->uart_text_input_buffer);
-        app->uart_text_input_buffer = NULL;
-    }
-    if (app->uart_text_input_temp_buffer)
-    {
-        free(app->uart_text_input_temp_buffer);
-        app->uart_text_input_temp_buffer = NULL;
-    }
-}
-
-void flip_wifi_free_all(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-    flip_wifi_free_views(app);
-    flip_wifi_free_widgets(app);
-    flip_wifi_free_submenus(app);
-    flip_wifi_free_text_inputs(app);
-    flip_wifi_free_playlist();
-}
-
-static void flip_wifi_redraw_submenu_saved(void *context)
-{
-    // re draw the saved submenu
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-    if (!app->submenu_wifi)
-    {
-        FURI_LOG_E(TAG, "Submenu is NULL");
-        return;
-    }
-    if (!wifi_playlist)
-    {
-        FURI_LOG_E(TAG, "WiFi Playlist is NULL");
-        return;
-    }
-    submenu_reset(app->submenu_wifi);
-    submenu_set_header(app->submenu_wifi, "Saved APs");
-    submenu_add_item(app->submenu_wifi, "[Add Network]", FlipWiFiSubmenuIndexWiFiSavedAddSSID, callback_submenu_choices, app);
-    for (size_t i = 0; i < wifi_playlist->count; i++)
-    {
-        submenu_add_item(app->submenu_wifi, wifi_playlist->ssids[i], FlipWiFiSubmenuIndexWiFiSavedStart + i, callback_submenu_choices, app);
-    }
-}
-
-static uint32_t callback_to_submenu_main(void *context)
-{
-    UNUSED(context);
-    ssid_index = 0;
-    return FlipWiFiViewSubmenuMain;
-}
-static uint32_t callback_to_submenu_scan(void *context)
-{
-    UNUSED(context);
-    ssid_index = 0;
-    return FlipWiFiViewSubmenu;
-}
-static uint32_t callback_to_submenu_saved(void *context)
-{
-    UNUSED(context);
-    ssid_index = 0;
-    return FlipWiFiViewSubmenu;
-}
-uint32_t callback_exit_app(void *context)
-{
-    UNUSED(context);
-    return VIEW_NONE;
-}
-
-// Callback for drawing the main screen
-static void flip_wifi_view_draw_callback_scan(Canvas *canvas, void *model)
-{
-    UNUSED(model);
-    canvas_clear(canvas);
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 0, 10, ssid_list[ssid_index]);
-    canvas_draw_icon(canvas, 0, 53, &I_ButtonBACK_10x8);
-    canvas_draw_str_aligned(canvas, 12, 54, AlignLeft, AlignTop, "Back");
-    canvas_draw_icon(canvas, 96, 53, &I_ButtonRight_4x7);
-    canvas_draw_str_aligned(canvas, 103, 54, AlignLeft, AlignTop, "Add");
-}
-static void flip_wifi_view_draw_callback_saved(Canvas *canvas, void *model)
-{
-    UNUSED(model);
-    canvas_clear(canvas);
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str(canvas, 0, 10, current_ssid);
-    canvas_set_font(canvas, FontSecondary);
-    char password[72];
-    snprintf(password, sizeof(password), "Pass: %s", current_password);
-    canvas_draw_str(canvas, 0, 20, password);
-    canvas_draw_icon(canvas, 0, 54, &I_ButtonLeft_4x7);
-    canvas_draw_str_aligned(canvas, 7, 54, AlignLeft, AlignTop, "Delete");
-    canvas_draw_icon(canvas, 37, 53, &I_ButtonBACK_10x8);
-    canvas_draw_str_aligned(canvas, 49, 54, AlignLeft, AlignTop, "Back");
-    canvas_draw_icon(canvas, 73, 54, &I_ButtonOK_7x7);
-    canvas_draw_str_aligned(canvas, 81, 54, AlignLeft, AlignTop, "Set");
-    canvas_draw_icon(canvas, 100, 54, &I_ButtonRight_4x7);
-    canvas_draw_str_aligned(canvas, 107, 54, AlignLeft, AlignTop, "Edit");
-}
-
-// Input callback for the view (async input handling)
-static bool flip_wifi_view_input_callback_scan(InputEvent *event, void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (event->type == InputTypePress && event->key == InputKeyRight)
-    {
-        // switch to text input to set password
-        flip_wifi_free_text_inputs(app);
-        if (!flip_wifi_alloc_text_inputs(app, FlipWiFiViewTextInputScan))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password");
-            return false;
-        }
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
-        return true;
-    }
-    return false;
-}
-// Input callback for the view (async input handling)
-static bool flip_wifi_view_input_callback_saved(InputEvent *event, void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return false;
-    }
-    if (event->type == InputTypePress && event->key == InputKeyRight)
-    {
-        // set text input buffer as the selected password
-        strncpy(app->uart_text_input_temp_buffer, wifi_playlist->passwords[ssid_index], app->uart_text_input_buffer_size);
-        // switch to text input to set password
-        flip_wifi_free_text_inputs(app);
-        if (!flip_wifi_alloc_text_inputs(app, FlipWiFiViewTextInputSaved))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved");
-            return false;
-        }
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
-        return true;
-    }
-    else if (event->type == InputTypePress && event->key == InputKeyOk)
-    {
-        // save the settings
-        save_settings(wifi_playlist->ssids[ssid_index], wifi_playlist->passwords[ssid_index]);
-
-        // initialize uart
-        FlipperHTTP *fhttp = flipper_http_alloc();
-        if (!fhttp)
-        {
-            easy_flipper_dialog("[ERROR]", "Failed to initialize flipper http");
-            return false;
-        }
-
-        // clear response
-        if (fhttp->last_response)
-            snprintf(fhttp->last_response, RX_BUF_SIZE, "%s", "");
-
-        if (!flipper_http_save_wifi(fhttp, wifi_playlist->ssids[ssid_index], wifi_playlist->passwords[ssid_index]))
-        {
-            easy_flipper_dialog("[ERROR]", "Failed to save WiFi settings");
-            return false;
-        }
-
-        while (!fhttp->last_response || strlen(fhttp->last_response) == 0)
-        {
-            furi_delay_ms(100);
-        }
-
-        flipper_http_free(fhttp);
-
-        // check success (if [SUCCESS] is in the response)
-        if (strstr(fhttp->last_response, "[SUCCESS]") == NULL)
-        {
-            char response[512];
-            snprintf(response, sizeof(response), "Failed to save WiFi settings:\n%s", fhttp->last_response);
-            easy_flipper_dialog("[ERROR]", response);
-            return false;
-        }
-
-        easy_flipper_dialog("[SUCCESS]", "All FlipperHTTP apps will now\nuse the selected network.");
-        return true;
-    }
-    else if (event->type == InputTypePress && event->key == InputKeyLeft)
-    {
-        // shift the remaining ssids and passwords
-        for (uint32_t i = ssid_index; i < wifi_playlist->count - 1; i++)
-        {
-            // Use strncpy to prevent buffer overflows and ensure null termination
-            strncpy(wifi_playlist->ssids[i], wifi_playlist->ssids[i + 1], MAX_SSID_LENGTH - 1);
-            wifi_playlist->ssids[i][MAX_SSID_LENGTH - 1] = '\0'; // Ensure null-termination
-
-            strncpy(wifi_playlist->passwords[i], wifi_playlist->passwords[i + 1], MAX_SSID_LENGTH - 1);
-            wifi_playlist->passwords[i][MAX_SSID_LENGTH - 1] = '\0'; // Ensure null-termination
-
-            // Shift ssid_list
-            ssid_list[i] = ssid_list[i + 1];
-        }
-        wifi_playlist->count--;
-
-        // delete the last ssid and password
-        wifi_playlist->ssids[wifi_playlist->count][0] = '\0';
-        wifi_playlist->passwords[wifi_playlist->count][0] = '\0';
-
-        // save the playlist to storage
-        save_playlist(wifi_playlist);
-
-        // re draw the saved submenu
-        flip_wifi_redraw_submenu_saved(app);
-        // switch back to the saved view
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
-        return true;
-    }
-    return false;
-}
-void callback_submenu_choices(void *context, uint32_t index)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-    // initialize uart
-    FlipperHTTP *fhttp = flipper_http_alloc();
-    if (!fhttp)
-    {
-        easy_flipper_dialog("[ERROR]", "Failed to initialize flipper http");
-        return;
-    }
-    switch (index)
-    {
-    case FlipWiFiSubmenuIndexWiFiScan:
-        flip_wifi_free_all(app);
-        if (!flip_wifi_alloc_submenus(app, FlipWiFiViewSubmenuScan))
-        {
-            easy_flipper_dialog("[ERROR]", "Failed to allocate submenus for WiFi Scan");
-            return;
-        }
-
-        // scan for wifi ad parse the results
-        bool _flip_wifi_scan()
-        {
-            // storage setup
-            Storage *storage = furi_record_open(RECORD_STORAGE);
-
-            snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data/scan.txt");
-            storage_simply_remove_recursive(storage, fhttp->file_path); // ensure the file is empty
-
-            // ensure flip_wifi directory is there
-            char directory_path[128];
-            snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi");
-            storage_common_mkdir(storage, directory_path);
-
-            snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_wifi/data");
-            storage_common_mkdir(storage, directory_path);
-
-            furi_record_close(RECORD_STORAGE);
-
-            fhttp->just_started = true;
-            fhttp->save_received_data = true;
-            return flipper_http_send_command(fhttp, HTTP_CMD_SCAN);
-        }
-
-        bool _flip_wifi_handle_scan()
-        {
-            // load the received data from the saved file
-            FuriString *scan_data = flipper_http_load_from_file(fhttp->file_path);
-            if (scan_data == NULL)
-            {
-                FURI_LOG_E(TAG, "Failed to load received data from file.");
-                easy_flipper_dialog("[ERROR]", "Failed to load data from /apps_data/flip_wifi/data/scan.txt");
-                return false;
-            }
-
-            uint8_t ssid_count = 0;
-
-            for (uint8_t i = 0; i < MAX_SCAN_NETWORKS; i++)
-            {
-                char *ssid_item = get_json_array_value("networks", i, furi_string_get_cstr(scan_data));
-                if (ssid_item == NULL)
-                {
-                    // end of the list
-                    break;
-                }
-                ssid_list[i] = malloc(MAX_SSID_LENGTH);
-                if (ssid_list[i] == NULL)
-                {
-                    FURI_LOG_E(TAG, "Failed to allocate memory for SSID");
-                    furi_string_free(scan_data);
-                    return false;
-                }
-                snprintf(ssid_list[i], MAX_SSID_LENGTH, "%s", ssid_item);
-                free(ssid_item);
-                ssid_count++;
-            }
-
-            // Add each SSID as a submenu item
-            submenu_reset(app->submenu_wifi);
-            submenu_set_header(app->submenu_wifi, "WiFi Nearby");
-            for (uint8_t i = 0; i < ssid_count; i++)
-            {
-                char *ssid_item = ssid_list[i];
-                if (ssid_item == NULL)
-                {
-                    // end of the list
-                    break;
-                }
-                char ssid[64];
-                snprintf(ssid, sizeof(ssid), "%s", ssid_item);
-                submenu_add_item(app->submenu_wifi, ssid, FlipWiFiSubmenuIndexWiFiScanStart + i, callback_submenu_choices, app);
-            }
-            furi_string_free(scan_data);
-            return true;
-        }
-
-        flipper_http_loading_task(fhttp, _flip_wifi_scan, _flip_wifi_handle_scan, FlipWiFiViewSubmenu, FlipWiFiViewSubmenuMain, &app->view_dispatcher);
-        break;
-    case FlipWiFiSubmenuIndexWiFiSaved:
-        flip_wifi_free_all(app);
-        if (!flip_wifi_alloc_submenus(app, FlipWiFiViewSubmenuSaved))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate submenus for WiFi Saved");
-            return;
-        }
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
-        break;
-    case FlipWiFiSubmenuIndexAbout:
-        flip_wifi_free_all(app);
-        if (!flip_wifi_alloc_widgets(app, FlipWiFiViewAbout))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate widget for About");
-            return;
-        }
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewAbout);
-        break;
-    case FlipWiFiSubmenuIndexWiFiSavedAddSSID:
-        flip_wifi_free_text_inputs(app);
-        if (!flip_wifi_alloc_text_inputs(app, FlipWiFiViewTextInputSavedAddSSID))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate text input for WiFi Saved Add Password");
-            return;
-        }
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
-        break;
-    case FlipWiFiSubmenuIndexCommands:
-        flip_wifi_free_all(app);
-        if (!flip_wifi_alloc_submenus(app, FlipWiFiViewSubmenuCommands))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate submenus for Commands");
-            return;
-        }
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
-        break;
-    case FlipWiFiSubmenuIndexFastCommandStart ... FlipWiFiSubmenuIndexFastCommandStart + 4:
-        // Handle fast commands
-        switch (index)
-        {
-        case FlipWiFiSubmenuIndexFastCommandStart + 0:
-            // CUSTOM - send to text input and return
-            flip_wifi_free_text_inputs(app);
-            if (!flip_wifi_alloc_text_inputs(app, FlipWiFiSubmenuIndexFastCommandStart))
-            {
-                FURI_LOG_E(TAG, "Failed to allocate text input for Fast Command");
-                return;
-            }
-            view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
-            return;
-        case FlipWiFiSubmenuIndexFastCommandStart + 1:
-            // PING
-            flipper_http_send_command(fhttp, HTTP_CMD_PING);
-            break;
-        case FlipWiFiSubmenuIndexFastCommandStart + 2:
-            // LIST
-            flipper_http_send_command(fhttp, HTTP_CMD_LIST_COMMANDS);
-            break;
-        case FlipWiFiSubmenuIndexFastCommandStart + 3:
-            // IP/ADDRESS
-            flipper_http_send_command(fhttp, HTTP_CMD_IP_ADDRESS);
-            break;
-        case FlipWiFiSubmenuIndexFastCommandStart + 4:
-            // WIFI/IP
-            flipper_http_send_command(fhttp, HTTP_CMD_IP_WIFI);
-            break;
-        default:
-            break;
-        }
-        while (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
-        {
-            // Wait for the response
-            furi_delay_ms(100);
-        }
-        if (fhttp->last_response != NULL)
-        {
-            char response[100];
-            snprintf(response, sizeof(response), "%s", fhttp->last_response);
-            easy_flipper_dialog("", response);
-        }
-        break;
-    case 100 ... 199:
-        ssid_index = index - FlipWiFiSubmenuIndexWiFiScanStart;
-        flip_wifi_free_views(app);
-        if (!flip_wifi_alloc_views(app, FlipWiFiViewWiFiScan))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate views for WiFi Scan");
-            return;
-        }
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewGeneric);
-        break;
-    case 200 ... 299:
-        ssid_index = index - FlipWiFiSubmenuIndexWiFiSavedStart;
-        flip_wifi_free_views(app);
-        snprintf(current_ssid, sizeof(current_ssid), "%s", wifi_playlist->ssids[ssid_index]);
-        snprintf(current_password, sizeof(current_password), "%s", wifi_playlist->passwords[ssid_index]);
-        if (!flip_wifi_alloc_views(app, FlipWiFiViewWiFiSaved))
-        {
-            FURI_LOG_E(TAG, "Failed to allocate views for WiFi Saved");
-            return;
-        }
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewGeneric);
-        break;
-    default:
-        break;
-    }
-    flipper_http_free(fhttp);
-}
-
-void flip_wifi_text_updated_password_scan(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-
-    // Store the entered text with buffer size limit
-    strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size - 1);
-    // Ensure null-termination
-    app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0';
-
-    if (!flip_wifi_alloc_playlist(app))
-    {
-        FURI_LOG_E(TAG, "Failed to allocate playlist");
-        return;
-    }
-
-    // Ensure ssid_index is valid
-    if (ssid_index >= MAX_SCAN_NETWORKS)
-    {
-        FURI_LOG_E(TAG, "Invalid ssid_index: %ld", ssid_index);
-        return;
-    }
-
-    // Check if there's space in the playlist
-    if (wifi_playlist->count >= MAX_SAVED_NETWORKS)
-    {
-        FURI_LOG_E(TAG, "Playlist is full. Cannot add more entries.");
-        return;
-    }
-
-    // Add the SSID and password to the playlist
-    snprintf(wifi_playlist->ssids[wifi_playlist->count], MAX_SSID_LENGTH, "%s", ssid_list[ssid_index]);
-    snprintf(wifi_playlist->passwords[wifi_playlist->count], MAX_SSID_LENGTH, "%s", app->uart_text_input_buffer);
-    wifi_playlist->count++;
-
-    // Save the updated playlist to storage
-    save_playlist(wifi_playlist);
-
-    // Redraw the submenu to reflect changes
-    flip_wifi_redraw_submenu_saved(app);
-
-    // Switch back to the scan view
-    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
-}
-
-void flip_wifi_text_updated_password_saved(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-
-    // store the entered text
-    strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size);
-
-    // Ensure null-termination
-    app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0';
-
-    // update the password_saved in the playlist
-    snprintf(wifi_playlist->passwords[ssid_index], MAX_SSID_LENGTH, app->uart_text_input_buffer);
-
-    // save the playlist to storage
-    save_playlist(wifi_playlist);
-
-    // switch to back to the saved view
-    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
-}
-
-void flip_wifi_text_updated_add_ssid(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-
-    // check if empty
-    if (strlen(app->uart_text_input_temp_buffer) == 0)
-    {
-        easy_flipper_dialog("[ERROR]", "SSID cannot be empty");
-        return;
-    }
-
-    // store the entered text
-    strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size);
-
-    // Ensure null-termination
-    app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0';
-    save_char("wifi-ssid", app->uart_text_input_buffer);
-    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain);
-    text_input_reset(app->uart_text_input);
-    text_input_set_header_text(app->uart_text_input, "Enter Password");
-    app->uart_text_input_buffer_size = MAX_SSID_LENGTH;
-    free(app->uart_text_input_buffer);
-    free(app->uart_text_input_temp_buffer);
-    easy_flipper_set_buffer(&app->uart_text_input_buffer, app->uart_text_input_buffer_size);
-    easy_flipper_set_buffer(&app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size);
-    text_input_set_result_callback(app->uart_text_input, flip_wifi_text_updated_add_password, app, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size, false);
-    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewTextInput);
-}
-void flip_wifi_text_updated_add_password(void *context)
-{
-    FlipWiFiApp *app = (FlipWiFiApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWiFiApp is NULL");
-        return;
-    }
-
-    // check if empty
-    if (strlen(app->uart_text_input_temp_buffer) == 0)
-    {
-        easy_flipper_dialog("[ERROR]", "Password cannot be empty");
-        return;
-    }
-
-    // store the entered text
-    strncpy(app->uart_text_input_buffer, app->uart_text_input_temp_buffer, app->uart_text_input_buffer_size);
-    // Ensure null-termination
-    app->uart_text_input_buffer[app->uart_text_input_buffer_size - 1] = '\0';
-
-    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenuMain);
-
-    save_char("wifi-password", app->uart_text_input_buffer);
-
-    char wifi_ssid[64];
-    if (!load_char("wifi-ssid", wifi_ssid, sizeof(wifi_ssid)))
-    {
-        FURI_LOG_E(TAG, "Failed to load wifi ssid");
-        return;
-    }
-
-    // add the SSID and password_scan to the playlist
-    snprintf(wifi_playlist->ssids[wifi_playlist->count], MAX_SSID_LENGTH, wifi_ssid);
-    snprintf(wifi_playlist->passwords[wifi_playlist->count], MAX_SSID_LENGTH, app->uart_text_input_buffer);
-    wifi_playlist->count++;
-
-    // save the playlist to storage
-    save_playlist(wifi_playlist);
-
-    flip_wifi_redraw_submenu_saved(app);
-
-    // switch to back to the saved view
-    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewSubmenu);
-}

+ 0 - 8
flip_wifi/callback/flip_wifi_callback.h

@@ -1,8 +0,0 @@
-#pragma once
-#include <flip_wifi.h>
-#include <flip_storage/flip_wifi_storage.h>
-#include <flip_wifi_icons.h>
-
-void flip_wifi_free_all(void *context);
-uint32_t callback_exit_app(void *context);
-void callback_submenu_choices(void *context, uint32_t index);

+ 132 - 0
flip_wifi/callback/free.c

@@ -0,0 +1,132 @@
+#include "callback/free.h"
+#include <callback/loader.h>
+
+void free_all(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    free_views(app);
+    free_widgets(app);
+    free_submenus(app);
+    free_text_inputs(app);
+    free_playlist();
+    free_text_box(app);
+
+    if (back_from_ap)
+    {
+        // cannot do this in callback_submenu_ap (we got a NULL pointer error)
+        // so let's do it here
+        if (app->fhttp != NULL)
+        {
+            app->fhttp->state = IDLE;
+            flipper_http_send_data(app->fhttp, "[WIFI/AP/STOP]");
+            furi_delay_ms(100);
+            flipper_http_free(app->fhttp);
+            app->fhttp = NULL;
+        }
+        back_from_ap = false;
+    }
+
+    if (app->fhttp)
+    {
+        flipper_http_free(app->fhttp);
+        app->fhttp = NULL;
+    }
+
+    if (app->timer)
+    {
+        if (furi_timer_is_running(app->timer) > 0)
+            furi_timer_stop(app->timer);
+        furi_timer_free(app->timer);
+        app->timer = NULL;
+    }
+
+    loader_view_free(app);
+}
+
+void free_playlist(void)
+{
+    if (wifi_playlist)
+    {
+        free(wifi_playlist);
+        wifi_playlist = NULL;
+    }
+}
+
+void free_submenus(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (app->submenu_wifi)
+    {
+        free(app->submenu_wifi);
+        app->submenu_wifi = NULL;
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewSubmenu);
+    }
+}
+
+void free_text_box(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app, "FlipWiFiApp is NULL");
+
+    // Free the text box
+    if (app->textbox)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewWiFiAP);
+        text_box_free(app->textbox);
+        app->textbox = NULL;
+    }
+    // Free the timer
+    if (app->timer)
+    {
+        furi_timer_free(app->timer);
+        app->timer = NULL;
+    }
+}
+
+void free_text_inputs(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (app->uart_text_input)
+    {
+        free(app->uart_text_input);
+        app->uart_text_input = NULL;
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewTextInput);
+    }
+    if (app->uart_text_input_buffer)
+    {
+        free(app->uart_text_input_buffer);
+        app->uart_text_input_buffer = NULL;
+    }
+    if (app->uart_text_input_temp_buffer)
+    {
+        free(app->uart_text_input_temp_buffer);
+        app->uart_text_input_temp_buffer = NULL;
+    }
+}
+
+void free_widgets(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (app->widget_info)
+    {
+        free(app->widget_info);
+        app->widget_info = NULL;
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewAbout);
+    }
+};
+
+void free_views(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app);
+    if (app->view_wifi)
+    {
+        free(app->view_wifi);
+        app->view_wifi = NULL;
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewGeneric);
+    }
+}

+ 10 - 0
flip_wifi/callback/free.h

@@ -0,0 +1,10 @@
+#pragma once
+#include <flip_wifi.h>
+
+void free_all(void *context);
+void free_playlist(void);
+void free_submenus(void *context);
+void free_text_box(void *context);
+void free_text_inputs(void *context);
+void free_widgets(void *context);
+void free_views(void *context);

+ 591 - 0
flip_wifi/callback/loader.c

@@ -0,0 +1,591 @@
+#include <callback/loader.h>
+#include <callback/utils.h>
+#include <callback/callback.h>
+#include <alloc/flip_wifi_alloc.h>
+
+bool loader_view_alloc(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app, "FlipWiFiApp is NULL");
+    if (app->view_loader)
+    {
+        FURI_LOG_E(TAG, "View loader already allocated");
+        return false;
+    }
+    if (app->widget_result)
+    {
+        FURI_LOG_E(TAG, "Widget result already allocated");
+        return false;
+    }
+
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, loader_custom_event_callback);
+
+    if (!easy_flipper_set_view(&app->view_loader, FlipWiFiViewLoader, loader_draw_callback, NULL, callback_to_submenu_main, &app->view_dispatcher, app))
+    {
+        return false;
+    }
+
+    loader_init(app->view_loader);
+
+    return easy_flipper_set_widget(&app->widget_result, FlipWiFiViewWidgetResult, "", callback_to_submenu_main, &app->view_dispatcher);
+}
+
+void loader_view_free(void *context)
+{
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    furi_check(app, "FlipWiFiApp is NULL");
+    // Free Widget(s)
+    if (app->widget_result)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewWidgetResult);
+        widget_free(app->widget_result);
+        app->widget_result = NULL;
+    }
+
+    // Free View(s)
+    if (app->view_loader)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWiFiViewLoader);
+        loader_free_model(app->view_loader);
+        view_free(app->view_loader);
+        app->view_loader = NULL;
+    }
+}
+
+static void loader_error_draw(Canvas *canvas, DataLoaderModel *model)
+{
+    if (canvas == NULL)
+    {
+        FURI_LOG_E(TAG, "error_draw - canvas is NULL");
+        DEV_CRASH();
+        return;
+    }
+    if (model->fhttp->last_response != NULL)
+    {
+        if (strstr(model->fhttp->last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
+            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+        }
+        else if (strstr(model->fhttp->last_response, "[ERROR] Failed to connect to Wifi.") != NULL)
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
+            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+        }
+        else if (strstr(model->fhttp->last_response, "[ERROR] GET request failed or returned empty data.") != NULL)
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "[ERROR] WiFi error.");
+            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+        }
+        else if (strstr(model->fhttp->last_response, "[PONG]") != NULL)
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP...");
+        }
+        else
+        {
+            canvas_clear(canvas);
+            FURI_LOG_E(TAG, "Received an error: %s", model->fhttp->last_response);
+            canvas_draw_str(canvas, 0, 10, "[ERROR] Unusual error...");
+            canvas_draw_str(canvas, 0, 60, "Press BACK and retry.");
+        }
+    }
+    else
+    {
+        canvas_clear(canvas);
+        canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error.");
+        canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+    }
+}
+
+static void loader_process_callback(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_process_callback - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    View *view = app->view_loader;
+
+    DataState current_data_state;
+    DataLoaderModel *loader_model = NULL;
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            current_data_state = model->data_state;
+            loader_model = model;
+        },
+        false);
+    if (!loader_model || !loader_model->fhttp)
+    {
+        FURI_LOG_E(TAG, "Model or fhttp is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    if (current_data_state == DataStateInitial)
+    {
+        with_view_model(
+            view,
+            DataLoaderModel * model,
+            {
+                model->data_state = DataStateRequested;
+                DataLoaderFetch fetch = model->fetcher;
+                if (fetch == NULL)
+                {
+                    FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned.");
+                    model->data_state = DataStateError;
+                    return;
+                }
+
+                // Clear any previous responses
+                strncpy(model->fhttp->last_response, "", 1);
+                bool request_status = fetch(model);
+                if (!request_status)
+                {
+                    model->data_state = DataStateError;
+                }
+            },
+            true);
+    }
+    else if (current_data_state == DataStateRequested || current_data_state == DataStateError)
+    {
+        if (loader_model->fhttp->state == IDLE && loader_model->fhttp->last_response != NULL)
+        {
+            if (strstr(loader_model->fhttp->last_response, "[PONG]") != NULL)
+            {
+                FURI_LOG_DEV(TAG, "PONG received.");
+            }
+            else if (strncmp(loader_model->fhttp->last_response, "[SUCCESS]", 9))
+            {
+                FURI_LOG_DEV(TAG, "SUCCESS received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL");
+            }
+            else if (strncmp(loader_model->fhttp->last_response, "[ERROR]", 9))
+            {
+                FURI_LOG_DEV(TAG, "ERROR received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL");
+            }
+            else if (strlen(loader_model->fhttp->last_response))
+            {
+                // Still waiting on response
+            }
+            else
+            {
+                with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateReceived; }, true);
+            }
+        }
+        else if (loader_model->fhttp->state == SENDING || loader_model->fhttp->state == RECEIVING)
+        {
+            // continue waiting
+        }
+        else if (loader_model->fhttp->state == INACTIVE)
+        {
+            // inactive. try again
+        }
+        else if (loader_model->fhttp->state == ISSUE)
+        {
+            with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateError; }, true);
+        }
+        else
+        {
+            FURI_LOG_DEV(TAG, "Unexpected state: %d lastresp: %s", loader_model->fhttp->state, loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL");
+            DEV_CRASH();
+        }
+    }
+    else if (current_data_state == DataStateReceived)
+    {
+        with_view_model(
+            view,
+            DataLoaderModel * model,
+            {
+                char *data_text;
+                if (model->parser == NULL)
+                {
+                    data_text = NULL;
+                    FURI_LOG_DEV(TAG, "Parser is NULL");
+                    DEV_CRASH();
+                }
+                else
+                {
+                    data_text = model->parser(model);
+                }
+                FURI_LOG_DEV(TAG, "Parsed data: %s\r\ntext: %s", model->fhttp->last_response ? model->fhttp->last_response : "NULL", data_text ? data_text : "NULL");
+                model->data_text = data_text;
+                if (data_text == NULL)
+                {
+                    model->data_state = DataStateParseError;
+                }
+                else
+                {
+                    model->data_state = DataStateParsed;
+                }
+            },
+            true);
+    }
+    else if (current_data_state == DataStateParsed)
+    {
+        with_view_model(
+            view,
+            DataLoaderModel * model,
+            {
+                if (++model->request_index < model->request_count)
+                {
+                    model->data_state = DataStateInitial;
+                }
+                else
+                {
+                    loader_widget_set_text(model->data_text != NULL ? model->data_text : "", &app->widget_result);
+                    if (model->data_text != NULL)
+                    {
+                        free(model->data_text);
+                        model->data_text = NULL;
+                    }
+                    view_set_previous_callback(widget_get_view(app->widget_result), model->back_callback);
+                    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWiFiViewWidgetResult);
+                }
+            },
+            true);
+    }
+}
+
+bool loader_custom_event_callback(void *context, uint32_t index)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "custom_event_callback - context is NULL");
+        DEV_CRASH();
+        return false;
+    }
+
+    switch (index)
+    {
+    case FlipWiFiCustomEventProcess:
+        loader_process_callback(context);
+        return true;
+    default:
+        FURI_LOG_DEV(TAG, "custom_event_callback. Unknown index: %ld", index);
+        return false;
+    }
+}
+
+void loader_draw_callback(Canvas *canvas, void *model)
+{
+    if (!canvas || !model)
+    {
+        FURI_LOG_E(TAG, "loader_draw_callback - canvas or model is NULL");
+        return;
+    }
+
+    DataLoaderModel *data_loader_model = (DataLoaderModel *)model;
+    HTTPState http_state = data_loader_model->fhttp->state;
+    DataState data_state = data_loader_model->data_state;
+    char *title = data_loader_model->title;
+
+    canvas_set_font(canvas, FontSecondary);
+
+    if (http_state == INACTIVE)
+    {
+        canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected.");
+        canvas_draw_str(canvas, 0, 17, "Please connect to the board.");
+        canvas_draw_str(canvas, 0, 32, "If your board is connected,");
+        canvas_draw_str(canvas, 0, 42, "make sure you have flashed");
+        canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the");
+        canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash.");
+        return;
+    }
+
+    if (data_state == DataStateError || data_state == DataStateParseError)
+    {
+        loader_error_draw(canvas, data_loader_model);
+        return;
+    }
+
+    canvas_draw_str(canvas, 0, 7, title);
+    canvas_draw_str(canvas, 0, 17, "Loading...");
+
+    if (data_state == DataStateInitial)
+    {
+        return;
+    }
+
+    if (http_state == SENDING)
+    {
+        canvas_draw_str(canvas, 0, 27, "Fetching...");
+        return;
+    }
+
+    if (http_state == RECEIVING || data_state == DataStateRequested)
+    {
+        canvas_draw_str(canvas, 0, 27, "Receiving...");
+        return;
+    }
+
+    if (http_state == IDLE && data_state == DataStateReceived)
+    {
+        canvas_draw_str(canvas, 0, 27, "Processing...");
+        return;
+    }
+
+    if (http_state == IDLE && data_state == DataStateParsed)
+    {
+        canvas_draw_str(canvas, 0, 27, "Processed...");
+        return;
+    }
+}
+
+static void loader_timer_callback(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_timer_callback - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, FlipWiFiCustomEventProcess);
+}
+
+static void loader_on_enter(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_on_enter - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    View *view = app->view_loader;
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            view_set_previous_callback(view, model->back_callback);
+            if (model->timer == NULL)
+            {
+                model->timer = furi_timer_alloc(loader_timer_callback, FuriTimerTypePeriodic, app);
+            }
+            furi_timer_start(model->timer, 250);
+        },
+        true);
+}
+
+static void loader_on_exit(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_on_exit - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipWiFiApp *app = (FlipWiFiApp *)context;
+    View *view = app->view_loader;
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            if (model->timer)
+            {
+                furi_timer_stop(model->timer);
+            }
+        },
+        false);
+}
+
+void loader_init(View *view)
+{
+    if (view == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_init - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+    view_allocate_model(view, ViewModelTypeLocking, sizeof(DataLoaderModel));
+    view_set_enter_callback(view, loader_on_enter);
+    view_set_exit_callback(view, loader_on_exit);
+}
+
+void loader_free_model(View *view)
+{
+    if (view == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_free_model - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            if (model->timer)
+            {
+                furi_timer_free(model->timer);
+                model->timer = NULL;
+            }
+            if (model->parser_context)
+            {
+                // do not free the context here, it is the app context
+                // free(model->parser_context);
+                // model->parser_context = NULL;
+            }
+            if (model->fhttp)
+            {
+                flipper_http_free(model->fhttp);
+                model->fhttp = NULL;
+            }
+        },
+        false);
+}
+
+void loader_switch_to_view(FlipWiFiApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id)
+{
+    if (app == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_switch_to_view - app is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    View *view = app->view_loader;
+    if (view == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_switch_to_view - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            model->title = title;
+            model->fetcher = fetcher;
+            model->parser = parser;
+            model->request_index = 0;
+            model->request_count = request_count;
+            model->back_callback = back;
+            model->data_state = DataStateInitial;
+            model->data_text = NULL;
+            //
+            model->parser_context = app;
+            if (!model->fhttp)
+            {
+                model->fhttp = flipper_http_alloc();
+            }
+        },
+        true);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, view_id);
+}
+
+void loader_widget_set_text(char *message, Widget **widget)
+{
+    if (widget == NULL)
+    {
+        FURI_LOG_E(TAG, "set_widget_text - widget is NULL");
+        DEV_CRASH();
+        return;
+    }
+    if (message == NULL)
+    {
+        FURI_LOG_E(TAG, "set_widget_text - message is NULL");
+        DEV_CRASH();
+        return;
+    }
+    widget_reset(*widget);
+
+    uint32_t message_length = strlen(message); // Length of the message
+    uint32_t i = 0;                            // Index tracker
+    uint32_t formatted_index = 0;              // Tracker for where we are in the formatted message
+    char *formatted_message;                   // Buffer to hold the final formatted message
+
+    // Allocate buffer with double the message length plus one for safety
+    if (!easy_flipper_set_buffer(&formatted_message, message_length * 2 + 1))
+    {
+        return;
+    }
+
+    while (i < message_length)
+    {
+        uint32_t max_line_length = 31;                  // Maximum characters per line
+        uint32_t remaining_length = message_length - i; // Remaining characters
+        uint32_t line_length = (remaining_length < max_line_length) ? remaining_length : max_line_length;
+
+        // Check for newline character within the current segment
+        uint32_t newline_pos = i;
+        bool found_newline = false;
+        for (; newline_pos < i + line_length && newline_pos < message_length; newline_pos++)
+        {
+            if (message[newline_pos] == '\n')
+            {
+                found_newline = true;
+                break;
+            }
+        }
+
+        if (found_newline)
+        {
+            // If newline found, set line_length up to the newline
+            line_length = newline_pos - i;
+        }
+
+        // Temporary buffer to hold the current line
+        char line[32];
+        strncpy(line, message + i, line_length);
+        line[line_length] = '\0';
+
+        // If newline was found, skip it for the next iteration
+        if (found_newline)
+        {
+            i += line_length + 1; // +1 to skip the '\n' character
+        }
+        else
+        {
+            // Check if the line ends in the middle of a word and adjust accordingly
+            if (line_length == max_line_length && message[i + line_length] != '\0' && message[i + line_length] != ' ')
+            {
+                // Find the last space within the current line to avoid breaking a word
+                char *last_space = strrchr(line, ' ');
+                if (last_space != NULL)
+                {
+                    // Adjust the line_length to avoid cutting the word
+                    line_length = last_space - line;
+                    line[line_length] = '\0'; // Null-terminate at the space
+                }
+            }
+
+            // Move the index forward by the determined line_length
+            i += line_length;
+
+            // Skip any spaces at the beginning of the next line
+            while (i < message_length && message[i] == ' ')
+            {
+                i++;
+            }
+        }
+
+        // Manually copy the fixed line into the formatted_message buffer
+        for (uint32_t j = 0; j < line_length; j++)
+        {
+            formatted_message[formatted_index++] = line[j];
+        }
+
+        // Add a newline character for line spacing
+        formatted_message[formatted_index++] = '\n';
+    }
+
+    // Null-terminate the formatted_message
+    formatted_message[formatted_index] = '\0';
+
+    // Add the formatted message to the widget
+    widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message);
+}

+ 39 - 0
flip_wifi/callback/loader.h

@@ -0,0 +1,39 @@
+#pragma once
+#include <flip_wifi.h>
+
+typedef enum DataState DataState;
+enum DataState
+{
+    DataStateInitial,
+    DataStateRequested,
+    DataStateReceived,
+    DataStateParsed,
+    DataStateParseError,
+    DataStateError,
+};
+
+typedef struct DataLoaderModel DataLoaderModel;
+typedef bool (*DataLoaderFetch)(DataLoaderModel *model);
+typedef char *(*DataLoaderParser)(DataLoaderModel *model);
+struct DataLoaderModel
+{
+    char *title;
+    char *data_text;
+    DataState data_state;
+    DataLoaderFetch fetcher;
+    DataLoaderParser parser;
+    void *parser_context;
+    size_t request_index;
+    size_t request_count;
+    ViewNavigationCallback back_callback;
+    FuriTimer *timer;
+    FlipperHTTP *fhttp;
+};
+bool loader_view_alloc(void *context);
+void loader_view_free(void *context);
+void loader_switch_to_view(FlipWiFiApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id);
+void loader_draw_callback(Canvas *canvas, void *model);
+void loader_init(View *view);
+void loader_widget_set_text(char *message, Widget **widget);
+void loader_free_model(View *view);
+bool loader_custom_event_callback(void *context, uint32_t index);

+ 24 - 0
flip_wifi/callback/utils.h

@@ -0,0 +1,24 @@
+#pragma once
+
+// Below added by Derek Jamison
+// FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port.
+#ifdef DEVELOPMENT
+#define FURI_LOG_DEV(tag, format, ...) furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__)
+#define DEV_CRASH() furi_crash()
+#else
+#define FURI_LOG_DEV(tag, format, ...)
+#define DEV_CRASH()
+#endif
+
+typedef enum MessageState MessageState;
+enum MessageState
+{
+    MessageStateAbout,        // The about screen
+    MessageStateLoading,      // The loading screen (for game)
+    MessageStateWaitingLobby, // The waiting lobby screen
+};
+typedef struct MessageModel MessageModel;
+struct MessageModel
+{
+    MessageState message_state;
+};

+ 35 - 0
flip_wifi/easy_flipper/easy_flipper.c

@@ -588,4 +588,39 @@ 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;
 }

+ 17 - 0
flip_wifi/easy_flipper/easy_flipper.h

@@ -266,4 +266,21 @@ 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

+ 7 - 2
flip_wifi/flip_wifi.c

@@ -1,5 +1,5 @@
 #include "flip_wifi.h"
-#include <callback/flip_wifi_callback.h>
+#include <callback/free.h>
 WiFiPlaylist *wifi_playlist = NULL;
 // Function to free the resources used by FlipWiFiApp
 void flip_wifi_app_free(FlipWiFiApp *app)
@@ -17,7 +17,7 @@ void flip_wifi_app_free(FlipWiFiApp *app)
         submenu_free(app->submenu_main);
     }
 
-    flip_wifi_free_all(app);
+    free_all(app);
 
     // free the view dispatcher
     if (app->view_dispatcher)
@@ -30,3 +30,8 @@ void flip_wifi_app_free(FlipWiFiApp *app)
     if (app)
         free(app);
 }
+char *ssid_list[64];
+uint32_t ssid_index = 0;
+char current_ssid[64];
+char current_password[64];
+bool back_from_ap = false;

+ 34 - 2
flip_wifi/flip_wifi.h

@@ -6,6 +6,8 @@
 #include <storage/storage.h>
 
 #define TAG "FlipWiFi"
+#define VERSION "1.5"
+#define VERSION_TAG TAG " " VERSION
 #define MAX_SCAN_NETWORKS 100
 #define MAX_SAVED_NETWORKS 25
 #define MAX_SSID_LENGTH 64
@@ -16,24 +18,32 @@ typedef enum
     FlipWiFiSubmenuIndexAbout,
     //
     FlipWiFiSubmenuIndexWiFiScan,
+    FlipWiFiSubmenuIndexWiFiAP,
     FlipWiFiSubmenuIndexWiFiSaved,
     FlipWiFiSubmenuIndexCommands,
     //
+    FlipWiFiSubmenuIndexWiFiAPStart,
+    FlipWiFiSubmenuIndexWiFiAPSetSSID,
+    FlipWiFiSubmenuIndexWiFiAPSetHTML,
+    //
     FlipWiFiSubmenuIndexWiFiSavedAddSSID,
     //
     FlipWiFiSubmenuIndexFastCommandStart = 50,
     FlipWiFiSubmenuIndexWiFiScanStart = 100,
-    FlipWiFiSubmenuIndexWiFiSavedStart = 200,
+    FlipWiFiSubmenuIndexWiFiSavedStart = 200
+
 } FlipWiFiSubmenuIndex;
 
 // Define a single view for our FlipWiFi application
 typedef enum
 {
     FlipWiFiViewWiFiScan,  // The view for the wifi scan screen
+    FlipWiFiViewWiFiAP,    // The view for the wifi AP screen
     FlipWiFiViewWiFiSaved, // The view for the wifi scan screen
     //
     FlipWiFiViewSubmenuMain,     // The submenu for the main screen
     FlipWiFiViewSubmenuScan,     // The submenu for the wifi scan screen
+    FlipWiFiViewSubmenuAP,       // The submenu for the wifi AP screen
     FlipWiFiViewSubmenuSaved,    // The submenu for the wifi saved screen
     FlipWiFiViewSubmenuCommands, // The submenu for the fast commands screen
     FlipWiFiViewAbout,           // The about screen
@@ -46,8 +56,17 @@ typedef enum
     FlipWiFiViewGeneric,   // generic view
     FlipWiFiViewSubmenu,   // generic submenu
     FlipWiFiViewTextInput, // generic text input
+    //
+    FlipWiFiViewWidgetResult, // The text box that displays the random fact
+    FlipWiFiViewLoader,       // The loader screen retrieves data from the internet
 } FlipWiFiView;
 
+typedef enum
+{
+    FlipWiFiCustomEventAP,
+    FlipWiFiCustomEventProcess
+} FlipWiFiCustomEvent;
+
 // Define the WiFiPlaylist structure
 typedef struct
 {
@@ -59,9 +78,13 @@ typedef struct
 // Each screen will have its own view
 typedef struct
 {
+    View *view_loader;
+    Widget *widget_result;
+    View *view_message;
+    //
     ViewDispatcher *view_dispatcher;           // Switches between our views
     Widget *widget_info;                       // The widget for the about screen
-    View *view_wifi;                           // generic view for the wifi scan and saved screens
+    View *view_wifi;                           // generic view for the wifi scan,ap and saved screens
     Submenu *submenu_main;                     // The submenu for the main screen
     Submenu *submenu_wifi;                     // generic submenu for the wifi scan and saved screens
     VariableItemList *variable_item_list_wifi; // The variable item list (settngs)
@@ -70,10 +93,19 @@ typedef struct
     char *uart_text_input_buffer;              // Buffer for the text input
     char *uart_text_input_temp_buffer;         // Temporary buffer for the text input
     uint32_t uart_text_input_buffer_size;      // Size of the text input buffer
+    TextBox *textbox;                          // The text box
+    FlipperHTTP *fhttp;                        // FlipperHTTP (UART, simplified loading, etc.)
+    FuriTimer *timer;                          // timer to redraw the UART data as it comes in
 } FlipWiFiApp;
 
 // Function to free the resources used by FlipWiFiApp
 void flip_wifi_app_free(FlipWiFiApp *app);
 extern WiFiPlaylist *wifi_playlist; // The playlist of wifi networks
 
+extern char *ssid_list[64];
+extern uint32_t ssid_index;
+extern char current_ssid[64];
+extern char current_password[64];
+extern bool back_from_ap;
+
 #endif // FLIP_WIFI_E_H

+ 98 - 81
flip_wifi/flipper_http/flipper_http.c

@@ -153,22 +153,26 @@ static void get_timeout_timer_callback(void *context)
 }
 
 static void flipper_http_rx_callback(const char *line, void *context); // forward declaration
+// Instead of two globals, we use a single static pointer to the active instance.
+static FlipperHTTP *active_fhttp = NULL;
 
-// UART initialization function
-/**
- * @brief      Initialize UART.
- * @return     FlipperHTTP context if the UART was initialized successfully, NULL otherwise.
- * @note       The received data will be handled asynchronously via the callback.
- */
 FlipperHTTP *flipper_http_alloc()
 {
-    FlipperHTTP *fhttp = (FlipperHTTP *)malloc(sizeof(FlipperHTTP));
+    // If an active instance already exists, free it first.
+    if (active_fhttp != NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "Existing UART instance detected, freeing previous instance.");
+        flipper_http_free(active_fhttp);
+        active_fhttp = NULL;
+    }
+
+    FlipperHTTP *fhttp = malloc(sizeof(FlipperHTTP));
     if (!fhttp)
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate FlipperHTTP.");
         return NULL;
     }
-    memset(fhttp, 0, sizeof(FlipperHTTP)); // Initialize allocated memory to zero
+    memset(fhttp, 0, sizeof(FlipperHTTP));
 
     fhttp->flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1);
     if (!fhttp->flipper_http_stream)
@@ -189,7 +193,7 @@ FlipperHTTP *flipper_http_alloc()
 
     furi_thread_set_name(fhttp->rx_thread, "FlipperHTTP_RxThread");
     furi_thread_set_stack_size(fhttp->rx_thread, 1024);
-    furi_thread_set_context(fhttp->rx_thread, fhttp); // Corrected context
+    furi_thread_set_context(fhttp->rx_thread, fhttp);
     furi_thread_set_callback(fhttp->rx_thread, flipper_http_worker);
 
     fhttp->handle_rx_line_cb = flipper_http_rx_callback;
@@ -198,24 +202,11 @@ FlipperHTTP *flipper_http_alloc()
     furi_thread_start(fhttp->rx_thread);
     fhttp->rx_thread_id = furi_thread_get_id(fhttp->rx_thread);
 
-    // Handle when the UART control is busy to avoid furi_check failed
-    if (furi_hal_serial_control_is_busy(UART_CH))
-    {
-        FURI_LOG_E(HTTP_TAG, "UART control is busy.");
-        // Cleanup resources
-        furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop);
-        furi_thread_join(fhttp->rx_thread);
-        furi_thread_free(fhttp->rx_thread);
-        furi_stream_buffer_free(fhttp->flipper_http_stream);
-        free(fhttp);
-        return NULL;
-    }
-
+    // Acquire UART control
     fhttp->serial_handle = furi_hal_serial_control_acquire(UART_CH);
     if (!fhttp->serial_handle)
     {
         FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL");
-        // Cleanup resources
         furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop);
         furi_thread_join(fhttp->rx_thread);
         furi_thread_free(fhttp->rx_thread);
@@ -224,29 +215,17 @@ FlipperHTTP *flipper_http_alloc()
         return NULL;
     }
 
-    // Initialize UART with acquired handle
+    // Initialize and enable UART
     furi_hal_serial_init(fhttp->serial_handle, BAUDRATE);
-
-    // Enable RX direction
     furi_hal_serial_enable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
-
-    // Start asynchronous RX with the corrected callback and context
-    furi_hal_serial_async_rx_start(fhttp->serial_handle, _flipper_http_rx_callback, fhttp, false); // Corrected context
-
-    // Wait for the TX to complete to ensure UART is ready
+    furi_hal_serial_async_rx_start(fhttp->serial_handle, _flipper_http_rx_callback, fhttp, false);
     furi_hal_serial_tx_wait_complete(fhttp->serial_handle);
 
-    // Allocate the timer for handling timeouts
-    fhttp->get_timeout_timer = furi_timer_alloc(
-        get_timeout_timer_callback, // Callback function
-        FuriTimerTypeOnce,          // One-shot timer
-        fhttp                       // Corrected context
-    );
-
+    // Allocate the timeout timer
+    fhttp->get_timeout_timer = furi_timer_alloc(get_timeout_timer_callback, FuriTimerTypeOnce, fhttp);
     if (!fhttp->get_timeout_timer)
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate HTTP request timeout timer.");
-        // Cleanup resources
         furi_hal_serial_async_rx_stop(fhttp->serial_handle);
         furi_hal_serial_disable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
         furi_hal_serial_control_release(fhttp->serial_handle);
@@ -258,15 +237,12 @@ FlipperHTTP *flipper_http_alloc()
         free(fhttp);
         return NULL;
     }
-
-    // Set the timer thread priority if needed
     furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
 
-    fhttp->last_response = (char *)malloc(RX_BUF_SIZE);
+    fhttp->last_response = malloc(RX_BUF_SIZE);
     if (!fhttp->last_response)
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate memory for last_response.");
-        // Cleanup resources
         furi_timer_free(fhttp->get_timeout_timer);
         furi_hal_serial_async_rx_stop(fhttp->serial_handle);
         furi_hal_serial_disable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
@@ -279,21 +255,18 @@ FlipperHTTP *flipper_http_alloc()
         free(fhttp);
         return NULL;
     }
-    memset(fhttp->last_response, 0, RX_BUF_SIZE); // Initialize last_response
-
+    memset(fhttp->last_response, 0, RX_BUF_SIZE);
     fhttp->state = IDLE;
 
-    // FURI_LOG_I(HTTP_TAG, "UART initialized successfully.");
+    fhttp->last_response_str = furi_string_alloc();
+    // furi_string_reserve(fhttp->last_response_str, MAX_FILE_SHOW);
+
+    // Track the active instance globally.
+    active_fhttp = fhttp;
+
     return fhttp;
 }
 
-// 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_free(FlipperHTTP *fhttp)
 {
     if (!fhttp)
@@ -306,43 +279,49 @@ void flipper_http_free(FlipperHTTP *fhttp)
         FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?");
         return;
     }
-    // Stop asynchronous RX
+    // Stop asynchronous RX and clean up UART
     furi_hal_serial_async_rx_stop(fhttp->serial_handle);
-
-    // Release and deinitialize the serial handle
     furi_hal_serial_disable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
-    furi_hal_serial_control_release(fhttp->serial_handle);
     furi_hal_serial_deinit(fhttp->serial_handle);
+    furi_hal_serial_control_release(fhttp->serial_handle);
 
-    // Signal the worker thread to stop
+    // Signal and free the worker thread
     furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop);
-    // Wait for the thread to finish
     furi_thread_join(fhttp->rx_thread);
-    // Free the thread resources
     furi_thread_free(fhttp->rx_thread);
 
     // Free the stream buffer
     furi_stream_buffer_free(fhttp->flipper_http_stream);
 
-    // Free the timer
+    // Free the timer, if allocated
     if (fhttp->get_timeout_timer)
     {
         furi_timer_free(fhttp->get_timeout_timer);
         fhttp->get_timeout_timer = NULL;
     }
 
-    // Free the last response
+    // Free the last_response buffer
     if (fhttp->last_response)
     {
         free(fhttp->last_response);
         fhttp->last_response = NULL;
     }
 
-    // Free the FlipperHTTP context
-    free(fhttp);
-    fhttp = NULL;
+    // Free the last response string
+    if (fhttp->last_response_str)
+    {
+        furi_string_free(fhttp->last_response_str);
+        fhttp->last_response_str = NULL;
+    }
 
-    // FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully.");
+    // If this instance is the active instance, clear the static pointer.
+    if (active_fhttp == fhttp)
+    {
+        free(active_fhttp);
+        active_fhttp = NULL;
+    }
+
+    free(fhttp);
 }
 
 /**
@@ -446,32 +425,43 @@ FuriString *flipper_http_load_from_file(char *file_path)
         return NULL;
     }
 
-    // Allocate a FuriString to hold the received data
-    FuriString *str_result = furi_string_alloc();
-    if (!str_result)
+    size_t file_size = storage_file_size(file);
+
+    // final memory check
+    if (memmgr_heap_get_max_free_block() < file_size)
     {
-        FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString");
+        FURI_LOG_E(HTTP_TAG, "Not enough heap to read file.");
         storage_file_close(file);
         storage_file_free(file);
         furi_record_close(RECORD_STORAGE);
         return NULL;
     }
 
-    // Reset the FuriString to ensure it's empty before reading
-    furi_string_reset(str_result);
-
-    // Define a buffer to hold the read data
-    uint8_t *buffer = (uint8_t *)malloc(MAX_FILE_SHOW);
+    // Allocate a buffer to hold the read data
+    uint8_t *buffer = (uint8_t *)malloc(file_size);
     if (!buffer)
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer");
-        furi_string_free(str_result);
         storage_file_close(file);
         storage_file_free(file);
         furi_record_close(RECORD_STORAGE);
         return NULL;
     }
 
+    // Allocate a FuriString to hold the received data
+    FuriString *str_result = furi_string_alloc();
+    if (!str_result)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Reset the FuriString to ensure it's empty before reading
+    furi_string_reset(str_result);
+
     // Read data into the buffer
     size_t read_count = storage_file_read(file, buffer, MAX_FILE_SHOW);
     if (storage_file_get_error(file) != FSE_OK)
@@ -506,6 +496,12 @@ FuriString *flipper_http_load_from_file(char *file_path)
  */
 FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit)
 {
+    if (memmgr_heap_get_max_free_block() < limit)
+    {
+        FURI_LOG_E(HTTP_TAG, "Not enough heap to read file.");
+        return NULL;
+    }
+
     // Open the storage record
     Storage *storage = furi_record_open(RECORD_STORAGE);
     if (!storage)
@@ -532,7 +528,19 @@ FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit
         return NULL;
     }
 
-    if (memmgr_get_free_heap() < limit)
+    size_t file_size = storage_file_size(file);
+
+    if (file_size > limit)
+    {
+        FURI_LOG_E(HTTP_TAG, "File size exceeds limit: %d > %d", file_size, limit);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // final memory check
+    if (memmgr_heap_get_max_free_block() < file_size)
     {
         FURI_LOG_E(HTTP_TAG, "Not enough heap to read file.");
         storage_file_close(file);
@@ -542,7 +550,7 @@ FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit
     }
 
     // Allocate a buffer to hold the read data
-    uint8_t *buffer = (uint8_t *)malloc(limit);
+    uint8_t *buffer = (uint8_t *)malloc(file_size);
     if (!buffer)
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer");
@@ -563,10 +571,10 @@ FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit
         furi_record_close(RECORD_STORAGE);
         return NULL;
     }
-    furi_string_reserve(str_result, limit);
+    furi_string_reserve(str_result, file_size);
 
     // Read data into the buffer
-    size_t read_count = storage_file_read(file, buffer, limit);
+    size_t read_count = storage_file_read(file, buffer, file_size);
     if (storage_file_get_error(file) != FSE_OK)
     {
         FURI_LOG_E(HTTP_TAG, "Error reading from file.");
@@ -935,6 +943,7 @@ bool flipper_http_send_command(FlipperHTTP *fhttp, HTTPCommand command)
     case HTTP_CMD_IP_WIFI:
         return flipper_http_send_data(fhttp, "[WIFI/IP]");
     case HTTP_CMD_SCAN:
+        fhttp->method = GET;
         return flipper_http_send_data(fhttp, "[WIFI/SCAN]");
     case HTTP_CMD_LIST_COMMANDS:
         return flipper_http_send_data(fhttp, "[LIST]");
@@ -945,6 +954,8 @@ bool flipper_http_send_command(FlipperHTTP *fhttp, HTTPCommand command)
     case HTTP_CMD_PING:
         fhttp->state = INACTIVE; // set state as INACTIVE to be made IDLE if PONG is received
         return flipper_http_send_data(fhttp, "[PING]");
+    case HTTP_CMD_REBOOT:
+        return flipper_http_send_data(fhttp, "[REBOOT]");
     default:
         FURI_LOG_E(HTTP_TAG, "Invalid command.");
         return false;
@@ -1147,6 +1158,12 @@ static void flipper_http_rx_callback(const char *line, void *context)
             strstr(trimmed_line, "[DELETE/END]") == NULL)
         {
             strncpy(fhttp->last_response, trimmed_line, RX_BUF_SIZE);
+            if (strlen(furi_string_get_cstr(fhttp->last_response_str)) > MAX_FILE_SHOW)
+            {
+                furi_string_reset(fhttp->last_response_str);
+            }
+            furi_string_cat(fhttp->last_response_str, "\n");
+            furi_string_cat(fhttp->last_response_str, line);
         }
     }
     free(trimmed_line); // Free the allocated memory for trimmed_line
@@ -1157,7 +1174,7 @@ static void flipper_http_rx_callback(const char *line, void *context)
     }
 
     // Uncomment below line to log the data received over UART
-    // FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line);
+    FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line);
 
     // Check if we've started receiving data from a GET request
     if (fhttp->started_receiving && (fhttp->method == GET || fhttp->method == BYTES))

+ 23 - 20
flip_wifi/flipper_http/flipper_http.h

@@ -62,29 +62,32 @@ typedef enum {
     HTTP_CMD_LIST_COMMANDS,
     HTTP_CMD_LED_ON,
     HTTP_CMD_LED_OFF,
-    HTTP_CMD_PING
+    HTTP_CMD_PING,
+    HTTP_CMD_REBOOT
 } HTTPCommand; // list of non-input commands
 
 // FlipperHTTP Structure
-typedef struct {
-    FuriStreamBuffer* flipper_http_stream; // Stream buffer for UART communication
-    FuriHalSerialHandle* serial_handle; // Serial handle for UART communication
-    FuriThread* rx_thread; // Worker thread for UART
-    FuriThreadId rx_thread_id; // Worker thread ID
-    FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines
-    void* callback_context; // Context for the callback
-    HTTPState state; // State of the UART
-    HTTPMethod method; // HTTP method
-    char* last_response; // variable to store the last received data from the UART
-    char file_path[256]; // Path to save the received data
-    FuriTimer* get_timeout_timer; // Timer for HTTP request timeout
-    bool started_receiving; // Indicates if a request has started
-    bool just_started; // Indicates if data reception has just started
-    bool is_bytes_request; // Flag to indicate if the request is for bytes
-    bool save_bytes; // Flag to save the received data to a file
-    bool save_received_data; // Flag to save the received data to a file
-    bool just_started_bytes; // Indicates if bytes data reception has just started
-    size_t bytes_received; // Number of bytes received
+typedef struct
+{
+    FuriStreamBuffer *flipper_http_stream;    // Stream buffer for UART communication
+    FuriHalSerialHandle *serial_handle;       // Serial handle for UART communication
+    FuriThread *rx_thread;                    // Worker thread for UART
+    FuriThreadId rx_thread_id;                // Worker thread ID
+    FlipperHTTP_Callback handle_rx_line_cb;   // Callback for received lines
+    void *callback_context;                   // Context for the callback
+    HTTPState state;                          // State of the UART
+    HTTPMethod method;                        // HTTP method
+    char *last_response;                      // variable to store the last received data from the UART
+    FuriString *last_response_str;            // String to store the last received data
+    char file_path[256];                      // Path to save the received data
+    FuriTimer *get_timeout_timer;             // Timer for HTTP request timeout
+    bool started_receiving;                   // Indicates if a request has started
+    bool just_started;                        // Indicates if data reception has just started
+    bool is_bytes_request;                    // Flag to indicate if the request is for bytes
+    bool save_bytes;                          // Flag to save the received data to a file
+    bool save_received_data;                  // Flag to save the received data to a file
+    bool just_started_bytes;                  // Indicates if bytes data reception has just started
+    size_t bytes_received;                    // Number of bytes received
     char rx_line_buffer[RX_LINE_BUFFER_SIZE]; // Buffer for received lines
     uint8_t file_buffer[FILE_BUFFER_SIZE]; // Buffer for file data
     size_t file_buffer_len; // Length of the file buffer

+ 428 - 0
flip_wifi/update/update.c

@@ -0,0 +1,428 @@
+#include <update/update.h>
+#include <flip_storage/flip_wifi_storage.h>
+#include <storage/storage.h>
+
+static bool update_is_str(const char *src, const char *dst) { return strcmp(src, dst) == 0; }
+static bool update_json_to_datetime(DateTime *rtc_time, FuriString *str)
+{
+    if (!rtc_time || !str)
+    {
+        FURI_LOG_E(TAG, "rtc_time or str is NULL");
+        return false;
+    }
+    FuriString *hour = get_json_value_furi("hour", str);
+    if (hour)
+    {
+        rtc_time->hour = atoi(furi_string_get_cstr(hour));
+        furi_string_free(hour);
+    }
+    FuriString *minute = get_json_value_furi("minute", str);
+    if (minute)
+    {
+        rtc_time->minute = atoi(furi_string_get_cstr(minute));
+        furi_string_free(minute);
+    }
+    FuriString *second = get_json_value_furi("second", str);
+    if (second)
+    {
+        rtc_time->second = atoi(furi_string_get_cstr(second));
+        furi_string_free(second);
+    }
+    FuriString *day = get_json_value_furi("day", str);
+    if (day)
+    {
+        rtc_time->day = atoi(furi_string_get_cstr(day));
+        furi_string_free(day);
+    }
+    FuriString *month = get_json_value_furi("month", str);
+    if (month)
+    {
+        rtc_time->month = atoi(furi_string_get_cstr(month));
+        furi_string_free(month);
+    }
+    FuriString *year = get_json_value_furi("year", str);
+    if (year)
+    {
+        rtc_time->year = atoi(furi_string_get_cstr(year));
+        furi_string_free(year);
+    }
+    FuriString *weekday = get_json_value_furi("weekday", str);
+    if (weekday)
+    {
+        rtc_time->weekday = atoi(furi_string_get_cstr(weekday));
+        furi_string_free(weekday);
+    }
+    return datetime_validate_datetime(rtc_time);
+}
+
+static FuriString *update_datetime_to_json(DateTime *rtc_time)
+{
+    if (!rtc_time)
+    {
+        FURI_LOG_E(TAG, "rtc_time is NULL");
+        return NULL;
+    }
+    char json[256];
+    snprintf(
+        json,
+        sizeof(json),
+        "{\"hour\":%d,\"minute\":%d,\"second\":%d,\"day\":%d,\"month\":%d,\"year\":%d,\"weekday\":%d}",
+        rtc_time->hour,
+        rtc_time->minute,
+        rtc_time->second,
+        rtc_time->day,
+        rtc_time->month,
+        rtc_time->year,
+        rtc_time->weekday);
+    return furi_string_alloc_set_str(json);
+}
+
+static bool update_save_rtc_time(DateTime *rtc_time)
+{
+    if (!rtc_time)
+    {
+        FURI_LOG_E(TAG, "rtc_time is NULL");
+        return false;
+    }
+    FuriString *json = update_datetime_to_json(rtc_time);
+    if (!json)
+    {
+        FURI_LOG_E(TAG, "Failed to convert DateTime to JSON");
+        return false;
+    }
+    save_char("last_checked", furi_string_get_cstr(json));
+    furi_string_free(json);
+    return true;
+}
+
+//
+// Returns true if time_current is one hour (or more) later than the stored last_updated time
+//
+static bool update_is_update_time(DateTime *time_current)
+{
+    if (!time_current)
+    {
+        FURI_LOG_E(TAG, "time_current is NULL");
+        return false;
+    }
+    char last_updated_old[128];
+    if (!load_char("last_updated", last_updated_old, sizeof(last_updated_old)))
+    {
+        FURI_LOG_E(TAG, "Failed to load last_updated");
+        FuriString *json = update_datetime_to_json(time_current);
+        if (json)
+        {
+            save_char("last_updated", furi_string_get_cstr(json));
+            furi_string_free(json);
+        }
+        return false;
+    }
+
+    DateTime last_updated_time;
+
+    FuriString *last_updated_furi = char_to_furi_string(last_updated_old);
+    if (!last_updated_furi)
+    {
+        FURI_LOG_E(TAG, "Failed to convert char to FuriString");
+        return false;
+    }
+    if (!update_json_to_datetime(&last_updated_time, last_updated_furi))
+    {
+        FURI_LOG_E(TAG, "Failed to convert JSON to DateTime");
+        furi_string_free(last_updated_furi);
+        return false;
+    }
+    furi_string_free(last_updated_furi); // Free after usage.
+
+    bool time_diff = false;
+    // If the date is different assume more than one hour has passed.
+    if (time_current->year != last_updated_time.year ||
+        time_current->month != last_updated_time.month ||
+        time_current->day != last_updated_time.day)
+    {
+        time_diff = true;
+    }
+    else
+    {
+        // For the same day, compute seconds from midnight.
+        int seconds_current = time_current->hour * 3600 + time_current->minute * 60 + time_current->second;
+        int seconds_last = last_updated_time.hour * 3600 + last_updated_time.minute * 60 + last_updated_time.second;
+        if ((seconds_current - seconds_last) >= 3600)
+        {
+            time_diff = true;
+        }
+    }
+
+    return time_diff;
+}
+
+// Sends a request to fetch the last updated date of the app.
+static bool update_last_app_update(FlipperHTTP *fhttp, bool flipper_server)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "fhttp is NULL");
+        return false;
+    }
+    char url[256];
+    if (flipper_server)
+    {
+        // make sure folder is created
+        char directory_path[256];
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store");
+
+        // 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/flip_store/data");
+        storage_common_mkdir(storage, directory_path);
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/last_update_request.txt");
+        storage_simply_remove_recursive(storage, directory_path); // ensure the file is empty
+        furi_record_close(RECORD_STORAGE);
+
+        fhttp->save_received_data = true;
+        fhttp->is_bytes_request = false;
+
+        snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/last_update_request.txt");
+        snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/0/application/%s?is_latest_release_version=true", BUILD_ID);
+        return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
+    }
+    else
+    {
+        snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/last-updated/flip_downloader/");
+        return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
+    }
+}
+
+// Parses the server response and returns true if an update is available.
+static bool update_parse_last_app_update(FlipperHTTP *fhttp, DateTime *time_current, bool flipper_server)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "fhttp is NULL");
+        return false;
+    }
+    if (fhttp->state == ISSUE)
+    {
+        FURI_LOG_E(TAG, "Failed to fetch last app update");
+        return false;
+    }
+    char version_str[32];
+    if (!flipper_server)
+    {
+        if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
+        {
+            FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty");
+            return false;
+        }
+
+        char *app_version = get_json_value("version", fhttp->last_response);
+        if (app_version)
+        {
+            // Save the server app version: it should save something like: 0.8
+            save_char("server_app_version", app_version);
+            snprintf(version_str, sizeof(version_str), "%s", app_version);
+            free(app_version);
+        }
+        else
+        {
+            FURI_LOG_E(TAG, "Failed to get app version");
+            return false;
+        }
+    }
+    else
+    {
+        FuriString *app_data = flipper_http_load_from_file_with_limit(fhttp->file_path, memmgr_heap_get_max_free_block());
+        if (!app_data)
+        {
+            FURI_LOG_E(TAG, "Failed to load app data");
+            return false;
+        }
+        FuriString *current_version = get_json_value_furi("current_version", app_data);
+        if (!current_version)
+        {
+            FURI_LOG_E(TAG, "Failed to get current version");
+            furi_string_free(app_data);
+            return false;
+        }
+        furi_string_free(app_data);
+        FuriString *version = get_json_value_furi("version", current_version);
+        if (!version)
+        {
+            FURI_LOG_E(TAG, "Failed to get version");
+            furi_string_free(current_version);
+            furi_string_free(app_data);
+            return false;
+        }
+        // Save the server app version: it should save something like: 0.8
+        save_char("server_app_version", furi_string_get_cstr(version));
+        snprintf(version_str, sizeof(version_str), "%s", furi_string_get_cstr(version));
+        furi_string_free(current_version);
+        furi_string_free(version);
+        // furi_string_free(app_data);
+    }
+    // Only check for an update if an hour or more has passed.
+    if (update_is_update_time(time_current))
+    {
+        char app_version[32];
+        if (!load_char("app_version", app_version, sizeof(app_version)))
+        {
+            FURI_LOG_E(TAG, "Failed to load app version");
+            return false;
+        }
+        FURI_LOG_I(TAG, "App version: %s", app_version);
+        FURI_LOG_I(TAG, "Server version: %s", version_str);
+        // Check if the app version is different from the server version.
+        if (!update_is_str(app_version, version_str))
+        {
+            easy_flipper_dialog("Update available", "New update available!\nPress BACK to download.");
+            return true; // Update available.
+        }
+        FURI_LOG_I(TAG, "No update available");
+        return false; // No update available.
+    }
+    FURI_LOG_I(TAG, "Not enough time has passed since the last update check");
+    return false; // Not yet time to update.
+}
+
+static bool update_get_fap_file(FlipperHTTP *fhttp, bool flipper_server)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL.");
+        return false;
+    }
+    char url[256];
+    fhttp->save_received_data = false;
+    fhttp->is_bytes_request = true;
+#ifndef FW_ORIGIN_Momentum
+    snprintf(
+        fhttp->file_path,
+        sizeof(fhttp->file_path),
+        STORAGE_EXT_PATH_PREFIX "/apps/GPIO/flip_downloader.fap");
+#else
+    snprintf(
+        fhttp->file_path,
+        sizeof(fhttp->file_path),
+        STORAGE_EXT_PATH_PREFIX "/apps/GPIO/FlipperHTTP/flip_downloader.fap");
+#endif
+    if (flipper_server)
+    {
+        char build_id[32];
+        snprintf(build_id, sizeof(build_id), "%s", BUILD_ID);
+        uint8_t target;
+        target = furi_hal_version_get_hw_target();
+        uint16_t api_major, api_minor;
+        furi_hal_info_get_api_version(&api_major, &api_minor);
+        snprintf(
+            url,
+            sizeof(url),
+            "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d",
+            build_id,
+            target,
+            api_major,
+            api_minor);
+    }
+    else
+    {
+        snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/download/flip_downloader/");
+    }
+    return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\": \"application/octet-stream\"}", NULL);
+}
+
+// Updates the app. Uses the supplied current time for validating if update check should proceed.
+static bool update_update_app(FlipperHTTP *fhttp, DateTime *time_current, bool use_flipper_api)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "fhttp is NULL");
+        return false;
+    }
+    if (!update_last_app_update(fhttp, use_flipper_api))
+    {
+        FURI_LOG_E(TAG, "Failed to fetch last app update");
+        return false;
+    }
+    fhttp->state = RECEIVING;
+    furi_timer_start(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS);
+    while (fhttp->state == RECEIVING && furi_timer_is_running(fhttp->get_timeout_timer) > 0)
+    {
+        furi_delay_ms(100);
+    }
+    furi_timer_stop(fhttp->get_timeout_timer);
+    if (update_parse_last_app_update(fhttp, time_current, use_flipper_api))
+    {
+        if (!update_get_fap_file(fhttp, false))
+        {
+            FURI_LOG_E(TAG, "Failed to fetch fap file 1");
+            return false;
+        }
+        fhttp->state = RECEIVING;
+
+        while (fhttp->state == RECEIVING)
+        {
+            furi_delay_ms(100);
+        }
+
+        if (fhttp->state == ISSUE)
+        {
+            FURI_LOG_E(TAG, "Failed to fetch fap file 2");
+            easy_flipper_dialog("Update Error", "Failed to download the\nupdate file.\nPlease try again.");
+            return false;
+        }
+        return true;
+    }
+
+    FURI_LOG_I(TAG, "No update available");
+    return false; // No update available.
+}
+
+// Handles the app update routine. This function obtains the current RTC time,
+// checks the "last_checked" value, and if it is more than one hour old, calls for an update.
+bool update_is_ready(FlipperHTTP *fhttp, bool use_flipper_api)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "fhttp is NULL");
+        return false;
+    }
+    DateTime rtc_time;
+    furi_hal_rtc_get_datetime(&rtc_time);
+    char last_checked[32];
+    if (!load_char("last_checked", last_checked, sizeof(last_checked)))
+    {
+        // First time – save the current time and check for an update.
+        if (!update_save_rtc_time(&rtc_time))
+        {
+            FURI_LOG_E(TAG, "Failed to save RTC time");
+            return false;
+        }
+        return update_update_app(fhttp, &rtc_time, use_flipper_api);
+    }
+    else
+    {
+        // Check if the current RTC time is at least one hour past the stored time.
+        if (update_is_update_time(&rtc_time))
+        {
+            if (!update_update_app(fhttp, &rtc_time, use_flipper_api))
+            {
+                FURI_LOG_E(TAG, "Failed to update app");
+                // save the current time for the next check.
+                if (!update_save_rtc_time(&rtc_time))
+                {
+                    FURI_LOG_E(TAG, "Failed to save RTC time");
+                    return false;
+                }
+                return false;
+            }
+            // Save the current time for the next check.
+            if (!update_save_rtc_time(&rtc_time))
+            {
+                FURI_LOG_E(TAG, "Failed to save RTC time");
+                return false;
+            }
+            return true;
+        }
+        return false; // No update necessary.
+    }
+}

+ 4 - 0
flip_wifi/update/update.h

@@ -0,0 +1,4 @@
+#pragma once
+#include <flip_wifi.h>
+#define BUILD_ID "67290f9eb81fc7da553f5f81"
+bool update_is_ready(FlipperHTTP *fhttp, bool use_flipper_api);