ソースを参照

Merge flip_trader from https://github.com/jblanked/FlipTrader

# Conflicts:
#	flip_trader/alloc/flip_trader_alloc.c
#	flip_trader/app.c
#	flip_trader/application.fam
#	flip_trader/callback/flip_trader_callback.c
#	flip_trader/callback/flip_trader_callback.h
#	flip_trader/flip_trader.c
#	flip_trader/flip_trader.h
Willy-JL 1 年間 前
コミット
8930885fba

+ 5 - 0
flip_trader/CHANGELOG.md

@@ -1,3 +1,8 @@
+## v1.2
+Updates from Derek Jamison:
+- Improved progress display.
+- Added connectivity check on startup.
+
 ## v1.1
 - Improved memory allocation
 - Added more assets 

+ 3 - 3
flip_trader/README.md

@@ -64,11 +64,11 @@ FlipTrader automatically allocates necessary resources and initializes settings
 1. **Flash the WiFi Devboard**: Follow the instructions to flash the WiFi Devboard with FlipperHTTP:https://github.com/jblanked/FlipperHTTP
 2. **Install the App**: Download FlipTrader from the App Store.
 3. **Launch FlipTrader**: Open the app on your Flipper Zero.
-4. **Explore the Features**:
+4. Click **Settings** to manage your network configurations.
+5. **Explore the Features**:
    - Browse the **Assets** section and select an asset to fetch its current price.
    - Visit **About** for app information and version history.
-   - Use **WiFi Settings** to manage your network configurations.
 
 ## Known Issues
 1. **Asset Screen Delay**: Occasionally, the Asset Price screen may get stuck on "Loading Data".
-    - If it takes longer than 10 seconds, restart your Flipper Zero.
+    - Update to version 1.2 (or higher)

+ 17 - 7
flip_trader/alloc/flip_trader_alloc.c

@@ -42,28 +42,38 @@ FlipTraderApp* flip_trader_app_alloc() {
     if(!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app)) {
         return NULL;
     }
-
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, flip_trader_custom_event_callback);
     // Main view
     if(!easy_flipper_set_view(
-           &app->view_main,
-           FlipTraderViewMain,
-           flip_trader_view_draw_callback,
+           &app->view_loader,
+           FlipTraderViewLoader,
+           flip_trader_loader_draw_callback,
            NULL,
            callback_to_assets_submenu,
            &app->view_dispatcher,
            app)) {
         return NULL;
     }
+    flip_trader_loader_init(app->view_loader);
 
     // Widget
     if(!easy_flipper_set_widget(
-           &app->widget,
+           &app->widget_about,
            FlipTraderViewAbout,
-           "FlipTrader v1.1\n-----\nUse WiFi to get the price of\nstocks and currency pairs.\n-----\nwww.github.com/jblanked",
+           "FlipTrader v1.2\n-----\nUse WiFi to get the price of\nstocks and currency pairs.\n-----\nwww.github.com/jblanked",
            callback_to_submenu,
            &app->view_dispatcher)) {
         return NULL;
     }
+    if(!easy_flipper_set_widget(
+           &app->widget_result,
+           FlipTraderViewWidgetResult,
+           "Error, try again.",
+           callback_to_assets_submenu,
+           &app->view_dispatcher)) {
+        return NULL;
+    }
 
     // Text Input
     if(!easy_flipper_set_uart_text_input(
@@ -112,7 +122,7 @@ FlipTraderApp* flip_trader_app_alloc() {
     if(!easy_flipper_set_submenu(
            &app->submenu_main,
            FlipTraderViewMainSubmenu,
-           "FlipTrader v1.1",
+           "FlipTrader v1.2",
            easy_flipper_callback_exit_app,
            &app->view_dispatcher)) {
         return NULL;

+ 31 - 4
flip_trader/app.c

@@ -7,8 +7,8 @@ int32_t flip_trader_app(void* p) {
     UNUSED(p);
 
     // Initialize the FlipTrader application
-    FlipTraderApp* app = flip_trader_app_alloc();
-    if(!app) {
+    app_instance = flip_trader_app_alloc();
+    if(!app_instance) {
         FURI_LOG_E(TAG, "Failed to allocate FlipTraderApp");
         return -1;
     }
@@ -18,11 +18,38 @@ int32_t flip_trader_app(void* p) {
         return -1;
     }
 
+    if(app_instance->uart_text_input_buffer_ssid != NULL &&
+       app_instance->uart_text_input_buffer_password != NULL) {
+        // Try to wait for pong response.
+        uint8_t counter = 10;
+        while(fhttp.state == INACTIVE && --counter > 0) {
+            FURI_LOG_D(TAG, "Waiting for PONG");
+            furi_delay_ms(100);
+        }
+
+        if(counter == 0) {
+            DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
+            DialogMessage* message = dialog_message_alloc();
+            dialog_message_set_header(
+                message, "[FlipperHTTP Error]", 64, 0, AlignCenter, AlignTop);
+            dialog_message_set_text(
+                message,
+                "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.",
+                0,
+                63,
+                AlignLeft,
+                AlignBottom);
+            dialog_message_show(dialogs, message);
+            dialog_message_free(message);
+            furi_record_close(RECORD_DIALOGS);
+        }
+    }
+
     // Run the view dispatcher
-    view_dispatcher_run(app->view_dispatcher);
+    view_dispatcher_run(app_instance->view_dispatcher);
 
     // Free the resources used by the FlipTrader application
-    flip_trader_app_free(app);
+    flip_trader_app_free(app_instance);
 
     // Return 0 to indicate success
     return 0;

+ 1 - 1
flip_trader/application.fam

@@ -10,5 +10,5 @@ App(
     fap_description="Use WiFi to get the price of stocks and currency pairs on your Flipper Zero.",
     fap_author="JBlanked",
     fap_weburl="https://github.com/jblanked/FlipTrader",
-    fap_version="1.1",
+    fap_version="1.2",
 )

BIN
flip_trader/assets/01-main.png


+ 422 - 62
flip_trader/callback/flip_trader_callback.c

@@ -1,17 +1,34 @@
 #include <callback/flip_trader_callback.h>
 
+// Below added by Derek Jamison
+// FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port.
+#ifdef DEVELOPMENT
+#define FURI_LOG_DEV(tag, format, ...) \
+    furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__)
+#define DEV_CRASH() furi_crash()
+#else
+#define FURI_LOG_DEV(tag, format, ...)
+#define DEV_CRASH()
+#endif
+
 // hold the price of the asset
 char asset_price[64];
 bool sent_get_request = false;
 bool get_request_success = false;
 bool request_processed = false;
 
-void flip_trader_request_error(Canvas* canvas) {
+static void flip_trader_request_error_draw(Canvas* canvas) {
+    if(canvas == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_request_error_draw - canvas is NULL");
+        DEV_CRASH();
+        return;
+    }
     if(fhttp.last_response != NULL) {
         if(strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") !=
            NULL) {
             canvas_clear(canvas);
             canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
+            canvas_draw_str(canvas, 0, 22, "Failed to reconnect.");
             canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
             canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
         } else if(strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL) {
@@ -19,11 +36,9 @@ void flip_trader_request_error(Canvas* canvas) {
             canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
             canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
             canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
-        } else if(strstr(fhttp.last_response, "[ERROR] WiFi SSID or Password is empty") != NULL) {
+        } else if(strstr(fhttp.last_response, "[PONG]") != 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.");
+            canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP...");
         } else {
             canvas_clear(canvas);
             FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response);
@@ -32,13 +47,13 @@ void flip_trader_request_error(Canvas* canvas) {
         }
     } else {
         canvas_clear(canvas);
-        canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error.");
+        canvas_draw_str(canvas, 0, 10, "Failed to receive data.");
         canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
         canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
     }
 }
 
-bool send_price_request() {
+static bool send_price_request() {
     if(fhttp.state == INACTIVE) {
         return false;
     }
@@ -69,21 +84,21 @@ bool send_price_request() {
     return true;
 }
 
-void process_asset_price() {
+static char* process_asset_price() {
     if(!request_processed) {
         // load the received data from the saved file
         FuriString* price_data = flipper_http_load_from_file(fhttp.file_path);
         if(price_data == NULL) {
             FURI_LOG_E(TAG, "Failed to load received data from file.");
             fhttp.state = ISSUE;
-            return;
+            return NULL;
         }
         char* data_cstr = (char*)furi_string_get_cstr(price_data);
         if(data_cstr == NULL) {
             FURI_LOG_E(TAG, "Failed to get C-string from FuriString.");
             furi_string_free(price_data);
             fhttp.state = ISSUE;
-            return;
+            return NULL;
         }
         request_processed = true;
         char* global_quote = get_json_value("Global Quote", data_cstr, MAX_TOKENS);
@@ -93,7 +108,7 @@ void process_asset_price() {
             furi_string_free(price_data);
             free(global_quote);
             free(data_cstr);
-            return;
+            return NULL;
         }
         char* price = get_json_value("05. price", global_quote, MAX_TOKENS);
         if(price == NULL) {
@@ -103,7 +118,7 @@ void process_asset_price() {
             free(global_quote);
             free(price);
             free(data_cstr);
-            return;
+            return NULL;
         }
         // store the price "Asset: $price"
         snprintf(asset_price, 64, "%s: $%s", asset_names[asset_index], price);
@@ -114,57 +129,18 @@ void process_asset_price() {
         free(price);
         free(data_cstr);
     }
+    return asset_price;
 }
 
-// Callback for drawing the main screen
-void flip_trader_view_draw_callback(Canvas* canvas, void* model) {
-    if(!canvas) {
-        return;
-    }
-    UNUSED(model);
-
-    canvas_set_font(canvas, FontSecondary);
-
-    if(fhttp.state == INACTIVE) {
-        canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected.");
-        canvas_draw_str(canvas, 0, 17, "Please connect to the board.");
-        canvas_draw_str(canvas, 0, 32, "If your board is connected,");
-        canvas_draw_str(canvas, 0, 42, "make sure you have flashed");
-        canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the");
-        canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash.");
-        return;
-    }
-
-    canvas_draw_str(canvas, 0, 10, "Loading...");
-    // canvas_draw_str(canvas, 0, 10, asset_names[asset_index]);
-
-    // start the process
-    if(!send_price_request()) {
-        flip_trader_request_error(canvas);
-    }
-    // wait until the request is processed
-    if(!sent_get_request || !get_request_success || fhttp.state == RECEIVING) {
-        return;
-    }
-    // check status
-    if(fhttp.state == ISSUE || fhttp.last_response == NULL) {
-        flip_trader_request_error(canvas);
-    }
-    // success, process the data
-    process_asset_price();
-    canvas_clear(canvas);
-    canvas_draw_str(canvas, 0, 10, asset_price);
-}
-
-// Input callback for the view (async input handling)
-bool flip_trader_view_input_callback(InputEvent* event, void* context) {
-    FlipTraderApp* app = (FlipTraderApp*)context;
-    if(event->type == InputTypePress && event->key == InputKeyBack) {
-        // Exit the app when the back button is pressed
-        view_dispatcher_stop(app->view_dispatcher);
-        return true;
-    }
-    return false;
+static void flip_trader_asset_switch_to_view(FlipTraderApp* app) {
+    flip_trader_generic_switch_to_view(
+        app,
+        "Fetching..",
+        send_price_request,
+        process_asset_price,
+        1,
+        callback_to_assets_submenu,
+        FlipTraderViewLoader);
 }
 
 void callback_submenu_choices(void* context, uint32_t index) {
@@ -190,7 +166,7 @@ void callback_submenu_choices(void* context, uint32_t index) {
         // handle FlipTraderSubmenuIndexAssetStartIndex + index
         if(index >= FlipTraderSubmenuIndexAssetStartIndex) {
             asset_index = index - FlipTraderSubmenuIndexAssetStartIndex;
-            view_dispatcher_switch_to_view(app->view_dispatcher, FlipTraderViewMain);
+            flip_trader_asset_switch_to_view(app);
         } else {
             FURI_LOG_E(TAG, "Unknown submenu index");
         }
@@ -327,3 +303,387 @@ void settings_item_selected(void* context, uint32_t index) {
         break;
     }
 }
+
+static void flip_trader_widget_set_text(char* message, Widget** widget) {
+    if(widget == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_set_widget_text - widget is NULL");
+        DEV_CRASH();
+        return;
+    }
+    if(message == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_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
+    if(!easy_flipper_set_buffer(&formatted_message, message_length * 2 + 1)) {
+        return;
+    }
+
+    while(i < message_length) {
+        // TODO: Use canvas_glyph_width to calculate the maximum characters for the line
+        uint32_t max_line_length = 29; // 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;
+
+        // Temporary buffer to hold the current line
+        char line[30];
+        strncpy(line, message + i, line_length);
+        line[line_length] = '\0';
+
+        // Check if the line ends in the middle of a word and adjust accordingly
+        if(line_length == 29 && message[i + line_length] != '\0' &&
+           message[i + line_length] != ' ') {
+            // Find the last space within the 30-character segment
+            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
+            }
+        }
+
+        // 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';
+
+        // Move i forward to the start of the next word
+        i += line_length;
+
+        // Skip spaces at the beginning of the next line
+        while(message[i] == ' ') {
+            i++;
+        }
+    }
+
+    // Add the formatted message to the widget
+    widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message);
+}
+
+void flip_trader_loader_draw_callback(Canvas* canvas, void* model) {
+    if(!canvas || !model) {
+        FURI_LOG_E(TAG, "flip_trader_loader_draw_callback - canvas or model is NULL");
+        return;
+    }
+
+    SerialState http_state = fhttp.state;
+    AssetLoaderModel* asset_loader_model = (AssetLoaderModel*)model;
+    AssetState asset_state = asset_loader_model->asset_state;
+    char* title = asset_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(asset_state == AssetStateError || asset_state == AssetStateParseError) {
+        flip_trader_request_error_draw(canvas);
+        return;
+    }
+
+    canvas_draw_str(canvas, 0, 7, title);
+    canvas_draw_str(canvas, 0, 17, "Loading...");
+
+    if(asset_state == AssetStateInitial) {
+        return;
+    }
+
+    if(http_state == SENDING) {
+        canvas_draw_str(canvas, 0, 27, "Sending...");
+        return;
+    }
+
+    if(http_state == RECEIVING || asset_state == AssetStateRequested) {
+        canvas_draw_str(canvas, 0, 27, "Receiving...");
+        return;
+    }
+
+    if(http_state == IDLE && asset_state == AssetStateReceived) {
+        canvas_draw_str(canvas, 0, 27, "Processing...");
+        return;
+    }
+
+    if(http_state == IDLE && asset_state == AssetStateParsed) {
+        canvas_draw_str(canvas, 0, 27, "Processed...");
+        return;
+    }
+}
+
+static void flip_trader_loader_process_callback(void* context) {
+    if(context == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_loader_process_callback - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    FlipTraderApp* app = (FlipTraderApp*)context;
+    View* view = app->view_loader;
+
+    AssetState current_asset_state;
+    with_view_model(
+        view, AssetLoaderModel * model, { current_asset_state = model->asset_state; }, false);
+
+    if(current_asset_state == AssetStateInitial) {
+        with_view_model(
+            view,
+            AssetLoaderModel * model,
+            {
+                model->asset_state = AssetStateRequested;
+                AssetLoaderFetch fetch = model->fetcher;
+                if(fetch == NULL) {
+                    FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned.");
+                    model->asset_state = AssetStateError;
+                    return;
+                }
+
+                // Clear any previous responses
+                strncpy(fhttp.last_response, "", 1);
+                bool request_status = fetch(model);
+                if(!request_status) {
+                    model->asset_state = AssetStateError;
+                }
+            },
+            true);
+    } else if(current_asset_state == AssetStateRequested || current_asset_state == AssetStateError) {
+        if(fhttp.state == IDLE && fhttp.last_response != NULL) {
+            if(strstr(fhttp.last_response, "[PONG]") != NULL) {
+                FURI_LOG_DEV(TAG, "PONG received.");
+            } else if(strncmp(fhttp.last_response, "[SUCCESS]", 9) == 0) {
+                FURI_LOG_DEV(
+                    TAG,
+                    "SUCCESS received. %s",
+                    fhttp.last_response ? fhttp.last_response : "NULL");
+            } else if(strncmp(fhttp.last_response, "[ERROR]", 9) == 0) {
+                FURI_LOG_DEV(
+                    TAG, "ERROR received. %s", fhttp.last_response ? fhttp.last_response : "NULL");
+            } else if(strlen(fhttp.last_response) == 0) {
+                // Still waiting on response
+            } else {
+                with_view_model(
+                    view,
+                    AssetLoaderModel * model,
+                    { model->asset_state = AssetStateReceived; },
+                    true);
+            }
+        } else if(fhttp.state == SENDING || fhttp.state == RECEIVING) {
+            // continue waiting
+        } else if(fhttp.state == INACTIVE) {
+            // inactive. try again
+        } else if(fhttp.state == ISSUE) {
+            with_view_model(
+                view, AssetLoaderModel * model, { model->asset_state = AssetStateError; }, true);
+        } else {
+            FURI_LOG_DEV(
+                TAG,
+                "Unexpected state: %d lastresp: %s",
+                fhttp.state,
+                fhttp.last_response ? fhttp.last_response : "NULL");
+            DEV_CRASH();
+        }
+    } else if(current_asset_state == AssetStateReceived) {
+        with_view_model(
+            view,
+            AssetLoaderModel * model,
+            {
+                char* asset_text;
+                if(model->parser == NULL) {
+                    asset_text = NULL;
+                    FURI_LOG_DEV(TAG, "Parser is NULL");
+                    DEV_CRASH();
+                } else {
+                    asset_text = model->parser(model);
+                }
+                FURI_LOG_DEV(
+                    TAG,
+                    "Parsed asset: %s\r\ntext: %s",
+                    fhttp.last_response ? fhttp.last_response : "NULL",
+                    asset_text ? asset_text : "NULL");
+                model->asset_text = asset_text;
+                if(asset_text == NULL) {
+                    model->asset_state = AssetStateParseError;
+                } else {
+                    model->asset_state = AssetStateParsed;
+                }
+            },
+            true);
+    } else if(current_asset_state == AssetStateParsed) {
+        with_view_model(
+            view,
+            AssetLoaderModel * model,
+            {
+                if(++model->request_index < model->request_count) {
+                    model->asset_state = AssetStateInitial;
+                } else {
+                    flip_trader_widget_set_text(
+                        model->asset_text != NULL ? model->asset_text : "Empty result",
+                        &app_instance->widget_result);
+                    if(model->asset_text != NULL) {
+                        free(model->asset_text);
+                        model->asset_text = NULL;
+                    }
+                    view_set_previous_callback(
+                        widget_get_view(app_instance->widget_result), model->back_callback);
+                    view_dispatcher_switch_to_view(
+                        app_instance->view_dispatcher, FlipTraderViewWidgetResult);
+                }
+            },
+            true);
+    }
+}
+
+static void flip_trader_loader_timer_callback(void* context) {
+    if(context == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_loader_timer_callback - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipTraderApp* app = (FlipTraderApp*)context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, FlipTraderCustomEventProcess);
+}
+
+static void flip_trader_loader_on_enter(void* context) {
+    if(context == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_loader_on_enter - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipTraderApp* app = (FlipTraderApp*)context;
+    View* view = app->view_loader;
+    with_view_model(
+        view,
+        AssetLoaderModel * model,
+        {
+            view_set_previous_callback(view, model->back_callback);
+            if(model->timer == NULL) {
+                model->timer = furi_timer_alloc(
+                    flip_trader_loader_timer_callback, FuriTimerTypePeriodic, app);
+            }
+            furi_timer_start(model->timer, 250);
+        },
+        true);
+}
+
+static void flip_trader_loader_on_exit(void* context) {
+    if(context == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_loader_on_exit - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipTraderApp* app = (FlipTraderApp*)context;
+    View* view = app->view_loader;
+    with_view_model(
+        view,
+        AssetLoaderModel * model,
+        {
+            if(model->timer) {
+                furi_timer_stop(model->timer);
+            }
+        },
+        false);
+}
+
+void flip_trader_loader_init(View* view) {
+    if(view == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_loader_init - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+    view_allocate_model(view, ViewModelTypeLocking, sizeof(AssetLoaderModel));
+    view_set_enter_callback(view, flip_trader_loader_on_enter);
+    view_set_exit_callback(view, flip_trader_loader_on_exit);
+}
+
+void flip_trader_loader_free_model(View* view) {
+    if(view == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_loader_free_model - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+    with_view_model(
+        view,
+        AssetLoaderModel * model,
+        {
+            if(model->timer) {
+                furi_timer_free(model->timer);
+                model->timer = NULL;
+            }
+            if(model->parser_context) {
+                free(model->parser_context);
+                model->parser_context = NULL;
+            }
+        },
+        false);
+}
+
+bool flip_trader_custom_event_callback(void* context, uint32_t index) {
+    if(context == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_custom_event_callback - context is NULL");
+        DEV_CRASH();
+        return false;
+    }
+
+    switch(index) {
+    case FlipTraderCustomEventProcess:
+        flip_trader_loader_process_callback(context);
+        return true;
+    default:
+        FURI_LOG_DEV(TAG, "flip_trader_custom_event_callback. Unknown index: %ld", index);
+        return false;
+    }
+}
+
+void flip_trader_generic_switch_to_view(
+    FlipTraderApp* app,
+    char* title,
+    AssetLoaderFetch fetcher,
+    AssetLoaderParser parser,
+    size_t request_count,
+    ViewNavigationCallback back,
+    uint32_t view_id) {
+    if(app == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_generic_switch_to_view - app is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    View* view = app->view_loader;
+    if(view == NULL) {
+        FURI_LOG_E(TAG, "flip_trader_generic_switch_to_view - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    with_view_model(
+        view,
+        AssetLoaderModel * model,
+        {
+            model->title = title;
+            model->fetcher = fetcher;
+            model->parser = parser;
+            model->request_index = 0;
+            model->request_count = request_count;
+            model->back_callback = back;
+            model->asset_state = AssetStateInitial;
+            model->asset_text = NULL;
+        },
+        true);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, view_id);
+}

+ 49 - 8
flip_trader/callback/flip_trader_callback.h

@@ -11,14 +11,6 @@ extern bool sent_get_request;
 extern bool get_request_success;
 extern bool request_processed;
 
-void flip_trader_request_error(Canvas* canvas);
-bool send_price_request();
-void process_asset_price();
-
-// Callback for drawing the main screen
-void flip_trader_view_draw_callback(Canvas* canvas, void* model);
-// Input callback for the view (async input handling)
-bool flip_trader_view_input_callback(InputEvent* event, void* context);
 void callback_submenu_choices(void* context, uint32_t index);
 void text_updated_ssid(void* context);
 
@@ -29,4 +21,53 @@ uint32_t callback_to_submenu(void* context);
 uint32_t callback_to_wifi_settings(void* context);
 uint32_t callback_to_assets_submenu(void* context);
 void settings_item_selected(void* context, uint32_t index);
+
+// Add edits by Derek Jamison
+typedef enum AssetState AssetState;
+enum AssetState {
+    AssetStateInitial,
+    AssetStateRequested,
+    AssetStateReceived,
+    AssetStateParsed,
+    AssetStateParseError,
+    AssetStateError,
+};
+
+typedef enum FlipTraderCustomEvent FlipTraderCustomEvent;
+enum FlipTraderCustomEvent {
+    FlipTraderCustomEventProcess,
+};
+
+typedef struct AssetLoaderModel AssetLoaderModel;
+typedef bool (*AssetLoaderFetch)(AssetLoaderModel* model);
+typedef char* (*AssetLoaderParser)(AssetLoaderModel* model);
+struct AssetLoaderModel {
+    char* title;
+    char* asset_text;
+    AssetState asset_state;
+    AssetLoaderFetch fetcher;
+    AssetLoaderParser parser;
+    void* parser_context;
+    size_t request_index;
+    size_t request_count;
+    ViewNavigationCallback back_callback;
+    FuriTimer* timer;
+};
+
+void flip_trader_generic_switch_to_view(
+    FlipTraderApp* app,
+    char* title,
+    AssetLoaderFetch fetcher,
+    AssetLoaderParser parser,
+    size_t request_count,
+    ViewNavigationCallback back,
+    uint32_t view_id);
+
+void flip_trader_loader_draw_callback(Canvas* canvas, void* model);
+
+void flip_trader_loader_init(View* view);
+
+void flip_trader_loader_free_model(View* view);
+
+bool flip_trader_custom_event_callback(void* context, uint32_t index);
 #endif // FLIP_TRADER_CALLBACK_H

+ 12 - 6
flip_trader/flip_trader.c

@@ -1,4 +1,5 @@
 #include <flip_trader.h>
+void flip_trader_loader_free_model(View* view);
 
 void asset_names_free(char** names) {
     if(names) {
@@ -67,7 +68,7 @@ char** asset_names_alloc() {
 char** asset_names = NULL;
 // index
 uint32_t asset_index = 0;
-
+FlipTraderApp* app_instance = NULL;
 // Function to free the resources used by FlipTraderApp
 void flip_trader_app_free(FlipTraderApp* app) {
     if(!app) {
@@ -76,9 +77,10 @@ void flip_trader_app_free(FlipTraderApp* app) {
     }
 
     // Free View(s)
-    if(app->view_main) {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewMain);
-        view_free(app->view_main);
+    if(app->view_loader) {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewLoader);
+        flip_trader_loader_free_model(app->view_loader);
+        view_free(app->view_loader);
     }
 
     // Free Submenu(s)
@@ -92,9 +94,13 @@ void flip_trader_app_free(FlipTraderApp* app) {
     }
 
     // Free Widget(s)
-    if(app->widget) {
+    if(app->widget_about) {
         view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewAbout);
-        widget_free(app->widget);
+        widget_free(app->widget_about);
+    }
+    if(app->widget_result) {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipTraderViewWidgetResult);
+        widget_free(app->widget_result);
     }
 
     // Free Variable Item List(s)

+ 6 - 4
flip_trader/flip_trader.h

@@ -35,21 +35,23 @@ typedef enum {
     FlipTraderViewTextInputPassword, // The text input screen for the password
     //
     FlipTraderViewAssetsSubmenu, // The submenu for the assets
+    FlipTraderViewWidgetResult, // The text box that displays the random fact
+    FlipTraderViewLoader, // The loader screen retrieves data from the internet
 } FlipTraderView;
 
 // Each screen will have its own view
 typedef struct {
     ViewDispatcher* view_dispatcher; // Switches between our views
-    View* view_main; // The main screen that displays "Hello, World!"
+    View* view_loader; // The screen that loads data from internet
     Submenu* submenu_main; // The submenu
     Submenu* submenu_assets; // The submenu for the assets
-    Widget* widget; // The widget
+    Widget* widget_about; // The widget
+    Widget* widget_result; // The widget that displays the result
     VariableItemList* variable_item_list_wifi; // The variable item list (settngs)
     VariableItem* variable_item_ssid; // The variable item for the SSID
     VariableItem* variable_item_password; // The variable item for the password
     TextInput* uart_text_input_ssid; // The text input for the SSID
     TextInput* uart_text_input_password; // The text input for the password
-
     char* uart_text_input_buffer_ssid; // Buffer for the text input (SSID)
     char* uart_text_input_temp_buffer_ssid; // Temporary buffer for the text input (SSID)
     uint32_t uart_text_input_buffer_size_ssid; // Size of the text input buffer (SSID)
@@ -70,5 +72,5 @@ extern uint32_t asset_index;
 void flip_trader_app_free(FlipTraderApp* app);
 char** asset_names_alloc();
 void asset_names_free(char** names);
-
+extern FlipTraderApp* app_instance;
 #endif // FLIP_TRADE_E_H