Преглед на файлове

Merge flip_store from https://github.com/jblanked/FlipStore

# Conflicts:
#	flip_store/text_input/uart_text_input.c
Willy-JL преди 9 месеца
родител
ревизия
bd712e9c2a

BIN
flip_store/.DS_Store


+ 11 - 34
flip_store/README.md

@@ -1,45 +1,22 @@
-# FlipStore
-Download Flipper Zero apps directly to your Flipper Zero using WiFi. 
+# FlipDownloader
+Download apps and assets directly to your Flipper Zero using WiFi. This app is not affiliated with Flipper Devices.
 
 ## Features
 - App Catalog
 - Install Apps
 - Delete Apps 
-- Install WiFi Developer Board Firmware
-- Install Video Game Module Firmware
-- Install GitHub Repositories (Beta)
-- Install Official Firmware (coming soon)
+- Install WiFi Developer Board Firmware (installs in /apps_data/esp_flasher/)
+- Install Video Game Module Firmware (installs in /apps_data/vgm/)
+- Install GitHub Repositories (coming soon)
+- Install Flipper Firmware (coming soon)
 
 ## Installation
 1. Flash your WiFi Dveloper Board or Raspberry Pi Pico W: https://github.com/jblanked/FlipperHTTP
 2. Install the app.
 3. Enjoy :D
 
-## Roadmap
-**v0.2**
-- Stability Patch
-- App Categories
-
-**v0.3**
-- Improved memory allocation
-- Stability Patch 2
-- App Catalog Patch (add in required functionalility)
-
-**v0.4**
-- Delete Apps
-
-**v0.5**
-- App short description
-- App version
-
-**v0.6**
-- Download flash firmware (Marauder, Black Magic, FlipperHTTP)
-
-**v0.7**
-- UX Improvements
-
-**v0.8**
-- Download GitHub repositories
-
-**1.0**
-- Download Official Flipper Zero Firmware
+## Connect Online
+- Discord: https://discord.gg/5aN9qwkEc6
+- YouTube: https://www.youtube.com/@jblanked
+- Instagram: https://www.instagram.com/jblanked
+- Other: https://www.jblanked.com/social/

+ 1 - 1
flip_store/alloc/flip_store_alloc.c

@@ -55,7 +55,7 @@ FlipStoreApp *flip_store_app_alloc()
     submenu_add_item(app->submenu_options, "App Catalog", FlipStoreSubmenuIndexAppList, callback_submenu_choices, app);
     submenu_add_item(app->submenu_options, "ESP32 Firmware", FlipStoreSubmenuIndexFirmwares, callback_submenu_choices, app);
     submenu_add_item(app->submenu_options, "VGM Firmware", FlipStoreSubmenuIndexVGMFirmwares, callback_submenu_choices, app);
-    submenu_add_item(app->submenu_options, "GitHub Repository", FlipStoreSubmenuIndexGitHub, callback_submenu_choices, app);
+    // submenu_add_item(app->submenu_options, "GitHub Repository", FlipStoreSubmenuIndexGitHub, callback_submenu_choices, app);
     //
     submenu_add_item(app->submenu_app_list, "Bluetooth", FlipStoreSubmenuIndexAppListBluetooth, callback_submenu_choices, app);
     submenu_add_item(app->submenu_app_list, "Games", FlipStoreSubmenuIndexAppListGames, callback_submenu_choices, app);

+ 1 - 1
flip_store/alloc/flip_store_alloc.h

@@ -1,7 +1,7 @@
 #ifndef FLIP_STORE_I_H
 #define FLIP_STORE_I_H
 
-#include <flip_store.h>
+#include <flip_downloader.h>
 #include <callback/flip_store_callback.h>
 
 // Function to allocate resources for the FlipStoreApp

+ 4 - 3
flip_store/app.c

@@ -1,8 +1,8 @@
-#include <flip_store.h>
+#include <flip_downloader.h>
 #include <alloc/flip_store_alloc.h>
 
 // Entry point for the Hello World application
-int32_t main_flip_store(void *p)
+int32_t main_flip_downloader(void *p)
 {
     // Suppress unused parameter warning
     UNUSED(p);
@@ -23,7 +23,7 @@ int32_t main_flip_store(void *p)
         return -1;
     }
 
-    if (!flipper_http_ping(fhttp))
+    if (!flipper_http_send_command(fhttp, HTTP_CMD_PING))
     {
         FURI_LOG_E(TAG, "Failed to ping the device");
         flipper_http_free(fhttp);
@@ -39,6 +39,7 @@ int32_t main_flip_store(void *p)
     }
 
     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.");

+ 6 - 6
flip_store/application.fam

@@ -1,14 +1,14 @@
 App(
-    appid="flip_store",
-    name="FlipStore",
+    appid="flip_downloader",
+    name="FlipDownloader",
     apptype=FlipperAppType.EXTERNAL,
-    entry_point="main_flip_store",
+    entry_point="main_flip_downloader",
     stack_size=4 * 1024,
     fap_icon="app.png",
     fap_category="GPIO/FlipperHTTP",
     fap_icon_assets="assets",
-    fap_description="Download apps via WiFi directly to your Flipper Zero",
+    fap_description="Download apps and assets via WiFi directly to your Flipper Zero",
     fap_author="JBlanked",
-    fap_weburl="https://github.com/jblanked/FlipStore",
-    fap_version="0.8",
+    fap_weburl="https://github.com/jblanked/FlipDownloader",
+    fap_version="1.0",
 )

+ 4 - 3
flip_store/apps/flip_store_apps.c

@@ -37,7 +37,7 @@ char *categories[] = {
 
 FlipStoreAppInfo *flip_catalog_alloc()
 {
-    if (memmgr_get_free_heap() < MAX_APP_COUNT * sizeof(FlipStoreAppInfo))
+    if (memmgr_heap_get_max_free_block() < MAX_APP_COUNT * sizeof(FlipStoreAppInfo))
     {
         FURI_LOG_E(TAG, "Not enough memory to allocate flip_catalog.");
         return NULL;
@@ -91,7 +91,7 @@ bool flip_store_process_app_list(FlipperHTTP *fhttp)
         return NULL;
     }
     furi_string_cat_str(json_data_str, "{\"json_data\":");
-    if (memmgr_get_free_heap() < furi_string_size(feed_data) + furi_string_size(json_data_str) + 2)
+    if (memmgr_heap_get_max_free_block() < furi_string_size(feed_data) + furi_string_size(json_data_str) + 2)
     {
         FURI_LOG_E(TAG, "Not enough memory to allocate json_data_str.");
         furi_string_free(feed_data);
@@ -195,7 +195,8 @@ static bool flip_store_get_fap_file(FlipperHTTP *fhttp, char *build_id, uint8_t
     fhttp->save_received_data = false;
     fhttp->is_bytes_request = true;
     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);
-    return flipper_http_get_request_bytes(fhttp, url, "{\"Content-Type\": \"application/octet-stream\"}");
+    // return flipper_http_get_request_bytes(fhttp, url, "{\"Content-Type\": \"application/octet-stream\"}");
+    return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\": \"application/octet-stream\"}", NULL);
 }
 
 bool flip_store_install_app(FlipperHTTP *fhttp, char *category)

+ 2 - 1
flip_store/apps/flip_store_apps.h

@@ -1,7 +1,7 @@
 #ifndef FLIP_STORE_APPS_H
 #define FLIP_STORE_APPS_H
 
-#include <flip_store.h>
+#include <flip_downloader.h>
 #include <flip_storage/flip_store_storage.h>
 #include <callback/flip_store_callback.h>
 
@@ -11,6 +11,7 @@
 #define MAX_APP_COUNT 50
 #define MAX_APP_DESCRIPTION_LENGTH 100
 #define MAX_APP_VERSION_LENGTH 5
+#define MAX_RECEIVED_APPS 4
 
 // define the list of categories
 extern char *category_ids[];

BIN
flip_store/assets/01-main-menu.png


+ 10 - 1
flip_store/assets/CHANGELOG.md

@@ -1,3 +1,12 @@
+## v1.0
+- Changed app name to FlipDownloader (for official firmware)
+- Updated Marauder to 1.4
+- Added all firmwares from VGM-Library: https://github.com/jblanked/VGM-Library
+- Improved memory allocation
+
+## v0.8.1
+- Updated GitHub repository downloads
+
 ## v0.8
 - Updated FlipperHTTP to the latest library.
 - Switched to use Flipper catalog API.
@@ -34,7 +43,7 @@
 - Stability patch
 
 ## v0.2
-- Added categories: Users can now navigate through categories, and when FlipStore installs a selected app, it downloads directly to the corresponding category folder in the apps directory
+- Added categories: Users can now navigate through categories, and when FlipDownloader installs a selected app, it downloads directly to the corresponding category folder in the apps directory
 - Improved memory allocation to prevent "Out of Memory" warnings
 - Updated installation messages
 

+ 10 - 33
flip_store/assets/README.md

@@ -1,44 +1,21 @@
-Download Flipper Zero apps directly to your Flipper Zero using WiFi. 
+Download apps and assets directly to your Flipper Zero using WiFi. This app is not affiliated with Flipper Devices.
 
 ## Features
 - App Catalog
 - Install Apps
 - Delete Apps 
-- Install WiFi Developer Board Firmware
-- Install Video Game Module Firmware
-- Install GitHub Repositories (Beta)
-- Install Official Firmware (coming soon)
+- Install WiFi Developer Board Firmware (installs in /apps_data/esp_flasher/)
+- Install Video Game Module Firmware (installs in /apps_data/vgm/)
+- Install GitHub Repositories (coming soon)
+- Install Flipper Firmware (coming soon)
 
 ## Installation
 1. Flash your WiFi Dveloper Board or Raspberry Pi Pico W: https://github.com/jblanked/FlipperHTTP
 2. Install the app.
 3. Enjoy :D
 
-## Roadmap
-**v0.2**
-- Stability Patch
-- App Categories
-
-**v0.3**
-- Improved memory allocation
-- Stability Patch 2
-- App Catalog Patch (add in required functionalility)
-
-**v0.4**
-- Delete Apps
-
-**v0.5**
-- App short description
-- App version
-
-**v0.6**
-- Download flash firmware (Marauder, Black Magic, FlipperHTTP)
-
-**v0.7**
-- UX Improvements
-
-**v0.8**
-- Download GitHub repositories
-
-**1.0**
-- Download Official Flipper Zero Firmware
+## Connect Online
+- Discord: https://discord.gg/5aN9qwkEc6
+- YouTube: https://www.youtube.com/@jblanked
+- Instagram: https://www.instagram.com/jblanked
+- Other: https://www.jblanked.com/social/

+ 9 - 5
flip_store/callback/flip_store_callback.c

@@ -587,7 +587,7 @@ static bool alloc_about_view(FlipStoreApp *app)
         if (!easy_flipper_set_widget(
                 &app->widget_about,
                 FlipStoreViewAbout,
-                "Welcome to the FlipStore!\n------\nDownload apps via WiFi and\nrun them on your Flipper!\n------\nwww.github.com/jblanked",
+                "Welcome to FlipDownloader!\n------\nDownload apps and assets via WiFi\nand run them on your Flipper!\n------\nwww.github.com/jblanked",
                 callback_to_submenu,
                 &app->view_dispatcher))
         {
@@ -973,19 +973,21 @@ static void fetch_appropiate_app_list(FlipStoreApp *app, int iteration)
         char dir[256];
         snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data");
         storage_common_mkdir(storage, dir);
-        furi_record_close(RECORD_STORAGE);
         fhttp->state = IDLE;
         flip_catalog_free();
         snprintf(
             fhttp->file_path,
             sizeof(fhttp->file_path),
             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s.json", categories[flip_store_category_index]);
+        storage_simply_remove_recursive(storage, fhttp->file_path);
+        furi_record_close(RECORD_STORAGE);
         fhttp->save_received_data = true;
         fhttp->is_bytes_request = false;
         char url[256];
         // load 8 at a time
-        snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/0/application?limit=8&is_latest_release_version=true&offset=%d&sort_by=updated_at&sort_order=-1&category_id=%s", iteration, category_ids[flip_store_category_index]);
-        return flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\":\"application/json\"}");
+        snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/0/application?limit=%d&is_latest_release_version=true&offset=%d&sort_by=updated_at&sort_order=-1&category_id=%s", MAX_RECEIVED_APPS, iteration, category_ids[flip_store_category_index]);
+        // return flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\":\"application/json\"}");
+        return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
     }
     bool parse_app_list()
     {
@@ -1452,7 +1454,7 @@ void flip_store_loader_draw_callback(Canvas *canvas, void *model)
     }
 
     DataLoaderModel *data_loader_model = (DataLoaderModel *)model;
-    SerialState http_state = data_loader_model->fhttp->state;
+    HTTPState http_state = data_loader_model->fhttp->state;
     DataState data_state = data_loader_model->data_state;
     char *title = data_loader_model->title;
 
@@ -1492,6 +1494,8 @@ void flip_store_loader_draw_callback(Canvas *canvas, void *model)
     if (http_state == RECEIVING || data_state == DataStateRequested)
     {
         canvas_draw_str(canvas, 0, 27, "Receiving...");
+        canvas_draw_str(canvas, 0, 37, "This may take two minutes...");
+        canvas_draw_str(canvas, 0, 47, "Please wait...");
         return;
     }
 

+ 1 - 1
flip_store/callback/flip_store_callback.h

@@ -1,6 +1,6 @@
 #ifndef FLIP_STORE_CALLBACK_H
 #define FLIP_STORE_CALLBACK_H
-#include <flip_store.h>
+#include <flip_downloader.h>
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>

+ 86 - 6
flip_store/firmwares/flip_store_firmwares.c

@@ -25,11 +25,11 @@ Firmware *firmware_alloc()
     snprintf(fw[1].links[1], sizeof(fw[1].links[1]), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_firmware_a.bin");
     snprintf(fw[1].links[2], sizeof(fw[1].links[2]), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/WiFi%20Developer%20Board%20(ESP32S2)/flipper_http_partitions.bin");
 
-    // Marauder
+    // Marauder (this changes too often.. we need a static link for the third one. maybe I'll fork the repo and host it myself)
     snprintf(fw[2].name, sizeof(fw[2].name), "%s", "Marauder");
     snprintf(fw[2].links[0], sizeof(fw[2].links[0]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.bootloader.bin");
     snprintf(fw[2].links[1], sizeof(fw[2].links[1]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/STATIC/M/FLIPDEV/esp32_marauder.ino.partitions.bin");
-    snprintf(fw[2].links[2], sizeof(fw[2].links[2]), "%s", "https://raw.githubusercontent.com/FZEEFlasher/fzeeflasher.github.io/main/resources/CURRENT/esp32_marauder_v1_2_0_12192024_flipper.bin");
+    snprintf(fw[2].links[2], sizeof(fw[2].links[2]), "%s", "https://raw.githubusercontent.com/jblanked/fzeeflasher.github.io/main/resources/CURRENT/esp32_marauder_v1_4_1_20250406_flipper.bin");
 
     return fw;
 }
@@ -47,6 +47,87 @@ VGMFirmware *vgm_firmware_alloc()
     snprintf(fw[0].name, sizeof(fw[0].name), "%s", "FlipperHTTP");
     snprintf(fw[0].link, sizeof(fw[0].link), "%s", "https://raw.githubusercontent.com/jblanked/FlipperHTTP/main/Video%20Game%20Module/MicroPython/flipper_http_vgm_micro_python.uf2");
 
+    /* VGM Library specs
+    Screensavers: https://github.com/jblanked/VGM-Library/tree/main/Screensavers
+        - aquarium.uf2
+        - boing_ball.uf2
+        - bouncing_balls.uf2
+        - dvi_logo_bounce.uf2
+        - flying_toasters.uf2
+        - trippy_tvhost.uf2
+    Games: https://github.com/jblanked/VGM-Library/tree/main/engine/Arduino
+        - AirLabyrinth-VGM-Engine.uf2
+        - Arkanoid-VGM-Engine.uf2
+        - Doom-VGM-Engine.uf2
+        - Doom_8bit-VGM-Engine.uf2
+        - FlappyBird-VGM-Engine.uf2
+        - FlightAssault-VGM-Engine.uf2
+        - FlipWorld-VGM-Engine.uf2
+        - FuriousBirds-VGM-Engine.uf2
+        - Hawaii-VGM-Engine.uf2
+        - Pong-VGM-Engine.uf2
+        - T-Rex-Runner-VGM-Engine.uf2
+        - Tetris-VGM-Engine.uf2
+        - example_8bit-VGM-Engine.uf2
+    */
+    // VGM Library
+    snprintf(fw[1].name, sizeof(fw[1].name), "%s", "Aquarium - Screensaver");
+    snprintf(fw[1].link, sizeof(fw[1].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/Screensavers/aquarium.uf2");
+    //
+    snprintf(fw[2].name, sizeof(fw[2].name), "%s", "Boing Ball - Screensaver");
+    snprintf(fw[2].link, sizeof(fw[2].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/Screensavers/boing_ball.uf2");
+    //
+    snprintf(fw[3].name, sizeof(fw[3].name), "%s", "Bouncing Balls - Screensaver");
+    snprintf(fw[3].link, sizeof(fw[3].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/Screensavers/bouncing_balls.uf2");
+    //
+    snprintf(fw[4].name, sizeof(fw[4].name), "%s", "DVI Logo Bounce - Screensaver");
+    snprintf(fw[4].link, sizeof(fw[4].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/Screensavers/dvi_logo_bounce.uf2");
+    //
+    snprintf(fw[5].name, sizeof(fw[5].name), "%s", "Flying Toasters - Screensaver");
+    snprintf(fw[5].link, sizeof(fw[5].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/Screensavers/flying_toasters.uf2");
+    //
+    snprintf(fw[6].name, sizeof(fw[6].name), "%s", "Trippy TV Host - Screensaver");
+    snprintf(fw[6].link, sizeof(fw[6].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/Screensavers/trippy_tvhost.uf2");
+    //
+    snprintf(fw[7].name, sizeof(fw[7].name), "%s", "Air Labyrinth - Game");
+    snprintf(fw[7].link, sizeof(fw[7].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/AirLabyrinth-VGM-Engine.uf2");
+    //
+    snprintf(fw[8].name, sizeof(fw[8].name), "%s", "Arkanoid - Game");
+    snprintf(fw[8].link, sizeof(fw[8].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/Arkanoid-VGM-Engine.uf2");
+    //
+    snprintf(fw[9].name, sizeof(fw[9].name), "%s", "Doom - Game");
+    snprintf(fw[9].link, sizeof(fw[9].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/Doom-VGM-Engine.uf2");
+    //
+    snprintf(fw[10].name, sizeof(fw[10].name), "%s", "Doom 8bit - Game");
+    snprintf(fw[10].link, sizeof(fw[10].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/Doom_8bit-VGM-Engine.uf2");
+    //
+    snprintf(fw[11].name, sizeof(fw[11].name), "%s", "Flappy Bird - Game");
+    snprintf(fw[11].link, sizeof(fw[11].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/FlappyBird-VGM-Engine.uf2");
+    //
+    snprintf(fw[12].name, sizeof(fw[12].name), "%s", "Flight Assault - Game");
+    snprintf(fw[12].link, sizeof(fw[12].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/FlightAssault-VGM-Engine.uf2");
+    //
+    snprintf(fw[13].name, sizeof(fw[13].name), "%s", "Flip World - Game");
+    snprintf(fw[13].link, sizeof(fw[13].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/FlipWorld-VGM-Engine.uf2");
+    //
+    snprintf(fw[14].name, sizeof(fw[14].name), "%s", "Furious Birds - Game");
+    snprintf(fw[14].link, sizeof(fw[14].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/FuriousBirds-VGM-Engine.uf2");
+    //
+    snprintf(fw[15].name, sizeof(fw[15].name), "%s", "Hawaii - Game");
+    snprintf(fw[15].link, sizeof(fw[15].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/Hawaii-VGM-Engine.uf2");
+    //
+    snprintf(fw[16].name, sizeof(fw[16].name), "%s", "Pong - Game");
+    snprintf(fw[16].link, sizeof(fw[16].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/Pong-VGM-Engine.uf2");
+    //
+    snprintf(fw[17].name, sizeof(fw[17].name), "%s", "T-Rex Runner - Game");
+    snprintf(fw[17].link, sizeof(fw[17].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/T-Rex-Runner-VGM-Engine.uf2");
+    //
+    snprintf(fw[18].name, sizeof(fw[18].name), "%s", "Tetris - Game");
+    snprintf(fw[18].link, sizeof(fw[18].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/Tetris-VGM-Engine.uf2");
+    //
+    snprintf(fw[19].name, sizeof(fw[19].name), "%s", "Example 8bit - Game");
+    snprintf(fw[19].link, sizeof(fw[19].link), "%s", "https://raw.githubusercontent.com/jblanked/VGM-Library/main/engine/Arduino/example_8bit-VGM-Engine.uf2");
+
     return fw;
 }
 
@@ -95,12 +176,11 @@ bool flip_store_get_firmware_file(FlipperHTTP *fhttp, char *link, char *name, ch
     {
         snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm");
         storage_common_mkdir(storage, directory_path);
-        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm/%s", name);
-        storage_common_mkdir(storage, directory_path);
-        snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm/%s/%s", name, filename);
+        snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/vgm/%s", filename);
     }
     furi_record_close(RECORD_STORAGE);
     fhttp->save_received_data = false;
     fhttp->is_bytes_request = true;
-    return flipper_http_get_request_bytes(fhttp, link, "{\"Content-Type\":\"application/octet-stream\"}");
+    // return flipper_http_get_request_bytes(fhttp, link, "{\"Content-Type\":\"application/octet-stream\"}");
+    return flipper_http_request(fhttp, BYTES, link, "{\"Content-Type\":\"application/octet-stream\"}", NULL);
 }

+ 4 - 4
flip_store/firmwares/flip_store_firmwares.h

@@ -1,20 +1,20 @@
 #ifndef FLIP_STORE_FIRMWARES_H
 #define FLIP_STORE_FIRMWARES_H
 
-#include <flip_store.h>
+#include <flip_downloader.h>
 #include <flip_storage/flip_store_storage.h>
 #include <callback/flip_store_callback.h>
 
 typedef struct
 {
     char name[16];
-    char links[FIRMWARE_LINKS][256];
+    char links[FIRMWARE_LINKS][136];
 } Firmware;
 
 typedef struct
 {
-    char name[16];
-    char link[256];
+    char name[32];
+    char link[128];
 } VGMFirmware;
 
 extern Firmware *firmwares;

+ 1 - 1
flip_store/flip_store.c → flip_store/flip_downloader.c

@@ -1,4 +1,4 @@
-#include <flip_store.h>
+#include <flip_downloader.h>
 #include <apps/flip_store_apps.h>
 
 // Function to free the resources used by FlipStoreApp

+ 7 - 5
flip_store/flip_store.h → flip_store/flip_downloader.h

@@ -12,16 +12,18 @@
 #include <dialogs/dialogs.h>
 #include <jsmn/jsmn.h>
 #include <jsmn/jsmn_furi.h>
-#include <flip_store_icons.h>
+#include <flip_downloader_icons.h>
 
-#define TAG "FlipStore"
-#define VERSION_TAG "FlipStore v0.8"
+#define TAG "FlipDownloader"
+#define VERSION_TAG "FlipDownloader v1.0"
 
+// 1 for Black Magic, 1 for FlipperHTTP, 1 for Marauder
 #define FIRMWARE_COUNT 3
 #define FIRMWARE_LINKS 3
 
-#define VGM_FIRMWARE_COUNT 1
-#define VGM_FIRMWARE_LINKS 1
+// 1 for FlipperHTTP, 6 for VGM-Library screensavers, 13 for VGM-Library games
+#define VGM_FIRMWARE_COUNT 20
+#define VGM_FIRMWARE_LINKS 20
 
 #define MAX_GITHUB_FILES 30
 

+ 1 - 1
flip_store/flip_storage/flip_store_storage.h

@@ -3,7 +3,7 @@
 
 #include <furi.h>
 #include <storage/storage.h>
-#include <flip_store.h>
+#include <flip_downloader.h>
 
 #define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/settings.bin"
 #define BUFFER_SIZE 64

Файловите разлики са ограничени, защото са твърде много
+ 551 - 780
flip_store/flipper_http/flipper_http.c


+ 109 - 273
flip_store/flipper_http/flipper_http.h

@@ -15,8 +15,6 @@
 #include <storage/storage.h>
 #include <momentum/settings.h>
 
-// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext
-
 #define HTTP_TAG "FlipStore"              // change this to your app name
 #define http_tag "flip_store"             // change this to your app id
 #define UART_CH (momentum_settings.uart_esp_channel)    // UART channel
@@ -38,7 +36,7 @@ typedef enum
     RECEIVING, // Receiving data
     SENDING,   // Sending data
     ISSUE,     // Issue with connection
-} SerialState;
+} HTTPState;
 
 // Event Flags for UART Worker Thread
 typedef enum
@@ -47,97 +45,59 @@ typedef enum
     WorkerEvtRxDone = (1 << 1),
 } WorkerEvtFlags;
 
-// FlipperHTTP Structure
-typedef struct
+typedef enum
 {
-    FuriStreamBuffer *flipper_http_stream;  // Stream buffer for UART communication
-    FuriHalSerialHandle *serial_handle;     // Serial handle for UART communication
-    FuriThread *rx_thread;                  // Worker thread for UART
-    FuriThreadId rx_thread_id;              // Worker thread ID
-    FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines
-    void *callback_context;                 // Context for the callback
-    SerialState state;                      // State of the UART
-
-    // variable to store the last received data from the UART
-    char *last_response;
-    char file_path[256]; // Path to save the received data
-
-    // Timer-related members
-    FuriTimer *get_timeout_timer; // Timer for HTTP request timeout
-
-    bool started_receiving_get; // Indicates if a GET request has started
-    bool just_started_get;      // Indicates if GET data reception has just started
-
-    bool started_receiving_post; // Indicates if a POST request has started
-    bool just_started_post;      // Indicates if POST data reception has just started
-
-    bool started_receiving_put; // Indicates if a PUT request has started
-    bool just_started_put;      // Indicates if PUT data reception has just started
-
-    bool started_receiving_delete; // Indicates if a DELETE request has started
-    bool just_started_delete;      // Indicates if DELETE data reception has just started
+    GET,    // GET request
+    POST,   // POST request
+    PUT,    // PUT request
+    DELETE, // DELETE request
+    //
+    BYTES,      // Stream bytes to file
+    BYTES_POST, // Stream bytes to file after a POST request
+} HTTPMethod;
 
-    // Buffer to hold the raw bytes received from the UART
-    uint8_t *received_bytes;
-    size_t received_bytes_len; // Length of the received bytes
-    bool is_bytes_request;     // Flag to indicate if the request is for bytes
-    bool save_bytes;           // Flag to save the received data to a file
-    bool save_received_data;   // Flag to save the received data to a file
-
-    bool just_started_bytes; // Indicates if bytes data reception has just started
+typedef enum
+{
+    HTTP_CMD_WIFI_CONNECT,
+    HTTP_CMD_WIFI_DISCONNECT,
+    HTTP_CMD_IP_ADDRESS,
+    HTTP_CMD_IP_WIFI,
+    HTTP_CMD_SCAN,
+    HTTP_CMD_LIST_COMMANDS,
+    HTTP_CMD_LED_ON,
+    HTTP_CMD_LED_OFF,
+    HTTP_CMD_PING,
+    HTTP_CMD_REBOOT
+} HTTPCommand; // list of non-input commands
 
-    char rx_line_buffer[RX_LINE_BUFFER_SIZE];
-    uint8_t file_buffer[FILE_BUFFER_SIZE];
-    size_t file_buffer_len;
+// 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
+    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
+    size_t content_length;                    // Length of the content received
+    int status_code;                          // HTTP status code
 } FlipperHTTP;
 
-// fhttp.last_response holds the last received data from the UART
-
-// Function to append received data to file
-// make sure to initialize the file path before calling this function
-bool flipper_http_append_to_file(
-    const void *data,
-    size_t data_size,
-    bool start_new_file,
-    char *file_path);
-
-FuriString *flipper_http_load_from_file(char *file_path);
-FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit);
-
-// UART worker thread
-/**
- * @brief      Worker thread to handle UART data asynchronously.
- * @return     0
- * @param      context   The context to pass to the callback.
- * @note       This function will handle received data asynchronously via the callback.
- */
-// UART worker thread
-int32_t flipper_http_worker(void *context);
-
-// Timer callback function
-/**
- * @brief      Callback function for the GET timeout timer.
- * @return     0
- * @param      context   The context to pass to the callback.
- * @note       This function will be called when the GET request times out.
- */
-void get_timeout_timer_callback(void *context);
-
-// UART RX Handler Callback (Interrupt Context)
-/**
- * @brief      A private callback function to handle received data asynchronously.
- * @return     void
- * @param      handle    The UART handle.
- * @param      event     The event type.
- * @param      context   The context to pass to the callback.
- * @note       This function will handle received data asynchronously via the callback.
- */
-void _flipper_http_rx_callback(
-    FuriHalSerialHandle *handle,
-    FuriHalSerialRxEvent event,
-    void *context);
-
-// UART initialization function
 /**
  * @brief      Initialize UART.
  * @return     FlipperHTTP context if the UART was initialized successfully, NULL otherwise.
@@ -145,7 +105,6 @@ void _flipper_http_rx_callback(
  */
 FlipperHTTP *flipper_http_alloc();
 
-// Deinitialize UART
 /**
  * @brief      Deinitialize UART.
  * @return     void
@@ -154,55 +113,49 @@ FlipperHTTP *flipper_http_alloc();
  */
 void flipper_http_free(FlipperHTTP *fhttp);
 
-// Function to send data over UART with newline termination
-/**
- * @brief      Send data over UART with newline termination.
- * @return     true if the data was sent successfully, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @param      data  The data to send over UART.
- * @note       The data will be sent over UART with a newline character appended.
- */
-bool flipper_http_send_data(FlipperHTTP *fhttp, const char *data);
-
-// Function to send a PING request
 /**
- * @brief      Send a PING request to check if the Wifi Dev Board is connected.
- * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
- * @note       This is best used to check if the Wifi Dev Board is connected.
- * @note       The state will remain INACTIVE until a PONG is received.
+ * @brief      Append received data to a file.
+ * @return     true if the data was appended successfully, false otherwise.
+ * @param      data        The data to append to the file.
+ * @param      data_size   The size of the data to append.
+ * @param      start_new_file  Flag to indicate if a new file should be created.
+ * @param      file_path   The path to the file.
+ * @note       Make sure to initialize the file path before calling this function.
  */
-bool flipper_http_ping(FlipperHTTP *fhttp);
+bool flipper_http_append_to_file(const void *data, size_t data_size, bool start_new_file, char *file_path);
 
-// Function to list available commands
 /**
- * @brief      Send a command to list available commands.
- * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
+ * @brief      Load data from a file.
+ * @return     The loaded data as a FuriString.
+ * @param      file_path The path to the file to load.
  */
-bool flipper_http_list_commands(FlipperHTTP *fhttp);
+FuriString *flipper_http_load_from_file(char *file_path);
 
-// Function to turn on the LED
 /**
- * @brief      Allow the LED to display while processing.
- * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
+ * @brief      Load data from a file with a size limit.
+ * @return     The loaded data as a FuriString.
+ * @param      file_path The path to the file to load.
+ * @param      limit     The size limit for loading data.
  */
-bool flipper_http_led_on(FlipperHTTP *fhttp);
+FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit);
 
-// Function to turn off the LED
 /**
- * @brief      Disable the LED from displaying while processing.
- * @return     true if the request was successful, false otherwise.
+ * @brief Perform a task while displaying a loading screen
  * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
+ * @param http_request The function to send the request
+ * @param parse_response The function to parse the response
+ * @param success_view_id The view ID to switch to on success
+ * @param failure_view_id The view ID to switch to on failure
+ * @param view_dispatcher The view dispatcher to use
+ * @return
  */
-bool flipper_http_led_off(FlipperHTTP *fhttp);
+void flipper_http_loading_task(FlipperHTTP *fhttp,
+                               bool (*http_request)(void),
+                               bool (*parse_response)(void),
+                               uint32_t success_view_id,
+                               uint32_t failure_view_id,
+                               ViewDispatcher **view_dispatcher);
 
-// Function to parse JSON data
 /**
  * @brief      Parse JSON data.
  * @return     true if the JSON data was parsed successfully, false otherwise.
@@ -213,7 +166,6 @@ bool flipper_http_led_off(FlipperHTTP *fhttp);
  */
 bool flipper_http_parse_json(FlipperHTTP *fhttp, const char *key, const char *json_data);
 
-// Function to parse JSON array data
 /**
  * @brief      Parse JSON array data.
  * @return     true if the JSON array data was parsed successfully, false otherwise.
@@ -225,184 +177,68 @@ bool flipper_http_parse_json(FlipperHTTP *fhttp, const char *key, const char *js
  */
 bool flipper_http_parse_json_array(FlipperHTTP *fhttp, const char *key, int index, const char *json_data);
 
-// Function to scan for WiFi networks
-/**
- * @brief      Send a command to scan for WiFi networks.
- * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
- */
-bool flipper_http_scan_wifi(FlipperHTTP *fhttp);
-
-// Function to save WiFi settings (returns true if successful)
-/**
- * @brief      Send a command to save WiFi settings.
- * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
- */
-bool flipper_http_save_wifi(FlipperHTTP *fhttp, const char *ssid, const char *password);
-
-// Function to get IP address of WiFi Devboard
-/**
- * @brief      Send a command to get the IP address of the WiFi Devboard
- * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
- */
-bool flipper_http_ip_address(FlipperHTTP *fhttp);
-
-// Function to get IP address of the connected WiFi network
-/**
- * @brief      Send a command to get the IP address of the connected WiFi network.
- * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
- */
-bool flipper_http_ip_wifi(FlipperHTTP *fhttp);
-
-// Function to disconnect from WiFi (returns true if successful)
-/**
- * @brief      Send a command to disconnect from WiFi.
- * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
- */
-bool flipper_http_disconnect_wifi(FlipperHTTP *fhttp);
-
-// Function to connect to WiFi (returns true if successful)
-/**
- * @brief      Send a command to connect to WiFi.
- * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @note       The received data will be handled asynchronously via the callback.
- */
-bool flipper_http_connect_wifi(FlipperHTTP *fhttp);
-
-// Function to send a GET request
 /**
- * @brief      Send a GET request to the specified URL.
- * @return     true if the request was successful, false otherwise.
+ * @brief Process requests and parse JSON data asynchronously
  * @param fhttp The FlipperHTTP context
- * @param      url  The URL to send the GET request to.
- * @note       The received data will be handled asynchronously via the callback.
+ * @param http_request The function to send the request
+ * @param parse_json The function to parse the JSON
+ * @return true if successful, false otherwise
  */
-bool flipper_http_get_request(FlipperHTTP *fhttp, const char *url);
+bool flipper_http_process_response_async(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void));
 
-// Function to send a GET request with headers
 /**
- * @brief      Send a GET request to the specified URL.
+ * @brief      Send a request to the specified URL.
  * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @param      url  The URL to send the GET request to.
- * @param      headers  The headers to send with the GET request.
+ * @param      fhttp The FlipperHTTP context
+ * @param      method The HTTP method to use.
+ * @param      url  The URL to send the request to.
+ * @param      headers  The headers to send with the request.
+ * @param      payload  The data to send with the request.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_get_request_with_headers(FlipperHTTP *fhttp, const char *url, const char *headers);
+bool flipper_http_request(FlipperHTTP *fhttp, HTTPMethod method, const char *url, const char *headers, const char *payload);
 
-// Function to send a GET request with headers and return bytes
 /**
- * @brief      Send a GET request to the specified URL.
+ * @brief      Send a command to save WiFi settings.
  * @return     true if the request was successful, false otherwise.
  * @param fhttp The FlipperHTTP context
- * @param      url  The URL to send the GET request to.
- * @param      headers  The headers to send with the GET request.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_get_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers);
+bool flipper_http_save_wifi(FlipperHTTP *fhttp, const char *ssid, const char *password);
 
-// Function to send a POST request with headers
 /**
- * @brief      Send a POST request to the specified URL.
+ * @brief      Send a command.
  * @return     true if the request was successful, false otherwise.
- * @param fhttp The FlipperHTTP context
- * @param      url  The URL to send the POST request to.
- * @param      headers  The headers to send with the POST request.
- * @param      data  The data to send with the POST request.
+ * @param      fhttp The FlipperHTTP context
+ * @param      command The command to send.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_post_request_with_headers(
-    FlipperHTTP *fhttp,
-    const char *url,
-    const char *headers,
-    const char *payload);
+bool flipper_http_send_command(FlipperHTTP *fhttp, HTTPCommand command);
 
-// Function to send a POST request with headers and return bytes
 /**
- * @brief      Send a POST request to the specified URL.
- * @return     true if the request was successful, false otherwise.
+ * @brief      Send data over UART with newline termination.
+ * @return     true if the data was sent successfully, false otherwise.
  * @param fhttp The FlipperHTTP context
- * @param      url  The URL to send the POST request to.
- * @param      headers  The headers to send with the POST request.
- * @param      payload  The data to send with the POST request.
- * @note       The received data will be handled asynchronously via the callback.
+ * @param      data  The data to send over UART.
+ * @note       The data will be sent over UART with a newline character appended.
  */
-bool flipper_http_post_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload);
+bool flipper_http_send_data(FlipperHTTP *fhttp, const char *data);
 
-// Function to send a PUT request with headers
 /**
- * @brief      Send a PUT request to the specified URL.
+ * @brief      Send a request to the specified URL to start a WebSocket connection.
  * @return     true if the request was successful, false otherwise.
  * @param fhttp The FlipperHTTP context
- * @param      url  The URL to send the PUT request to.
- * @param      headers  The headers to send with the PUT request.
- * @param      data  The data to send with the PUT request.
+ * @param      url  The URL to send the WebSocket request to.
+ * @param port The port to connect to
+ * @param headers The headers to send with the WebSocket request
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_put_request_with_headers(
-    FlipperHTTP *fhttp,
-    const char *url,
-    const char *headers,
-    const char *payload);
+bool flipper_http_websocket_start(FlipperHTTP *fhttp, const char *url, uint16_t port, const char *headers);
 
-// Function to send a DELETE request with headers
 /**
- * @brief      Send a DELETE request to the specified URL.
+ * @brief      Send a request to stop the WebSocket connection.
  * @return     true if the request was successful, false otherwise.
  * @param fhttp The FlipperHTTP context
- * @param      url  The URL to send the DELETE request to.
- * @param      headers  The headers to send with the DELETE request.
- * @param      data  The data to send with the DELETE request.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_delete_request_with_headers(
-    FlipperHTTP *fhttp,
-    const char *url,
-    const char *headers,
-    const char *payload);
-
-// Function to handle received data asynchronously
-/**
- * @brief      Callback function to handle received data asynchronously.
- * @return     void
- * @param      line     The received line.
- * @param      context  The FlipperHTTP context.
- * @note       The received data will be handled asynchronously via the callback and handles the state of the UART.
- */
-void flipper_http_rx_callback(const char *line, void *context);
-
-/**
- * @brief Process requests and parse JSON data asynchronously
- * @param fhttp The FlipperHTTP context
- * @param http_request The function to send the request
- * @param parse_json The function to parse the JSON
- * @return true if successful, false otherwise
- */
-bool flipper_http_process_response_async(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void));
-
-/**
- * @brief Perform a task while displaying a loading screen
- * @param fhttp The FlipperHTTP context
- * @param http_request The function to send the request
- * @param parse_response The function to parse the response
- * @param success_view_id The view ID to switch to on success
- * @param failure_view_id The view ID to switch to on failure
- * @param view_dispatcher The view dispatcher to use
- * @return
- */
-void flipper_http_loading_task(FlipperHTTP *fhttp,
-                               bool (*http_request)(void),
-                               bool (*parse_response)(void),
-                               uint32_t success_view_id,
-                               uint32_t failure_view_id,
-                               ViewDispatcher **view_dispatcher);
+bool flipper_http_websocket_stop(FlipperHTTP *fhttp);

+ 383 - 113
flip_store/github/flip_store_github.c

@@ -1,7 +1,73 @@
+// flip_store_github.c
 #include <github/flip_store_github.h>
 #include <flip_storage/flip_store_storage.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdbool.h>
+
+#define MAX_RECURSION_DEPTH 5  // maximum allowed "/" characters in repo path
+#define MAX_PENDING_FOLDERS 20 // maximum number of folders to process iteratively
+
+// --- Pending Folder Queue for iterative folder processing ---
+typedef struct
+{
+    char file_path[256]; // Folder JSON file path (downloaded folder info)
+    char repo[256];      // New repository path, e.g. "repo/subfolder"
+} PendingFolder;
+
+static PendingFolder pendingFolders[MAX_PENDING_FOLDERS];
+static int pendingFoldersCount = 0;
+
+static int count_char(const char *s, char c)
+{
+    int count = 0;
+    for (; *s; s++)
+    {
+        if (*s == c)
+        {
+            count++;
+        }
+    }
+    return count;
+}
+
+// Enqueue a folder for later processing.
+// currentRepo is the repository path received so far (e.g. "repo" at top level)
+// folderName is the name of the folder (e.g. "alloc") that is appended.
+static bool enqueue_folder(const char *file_path, const char *currentRepo, const char *folderName)
+{
+    if (pendingFoldersCount >= MAX_PENDING_FOLDERS)
+    {
+        FURI_LOG_E(TAG, "Pending folder queue full!");
+        return false;
+    }
+    PendingFolder *pf = &pendingFolders[pendingFoldersCount++];
+    strncpy(pf->file_path, file_path, sizeof(pf->file_path) - 1);
+    pf->file_path[sizeof(pf->file_path) - 1] = '\0';
+    // New repo path = currentRepo + "/" + folderName.
+    snprintf(pf->repo, sizeof(pf->repo), "%s/%s", currentRepo, folderName);
+    FURI_LOG_I(TAG, "Enqueued folder: %s (file: %s)", pf->repo, pf->file_path);
+    return true;
+}
+
+// Process all enqueued folders iteratively.
+static void process_pending_folders(const char *author)
+{
+    int i = 0;
+    while (i < pendingFoldersCount)
+    {
+        PendingFolder pf = pendingFolders[i];
+        FURI_LOG_I(TAG, "Processing pending folder: %s", pf.repo);
+        if (!flip_store_parse_github_contents(pf.file_path, author, pf.repo))
+        {
+            FURI_LOG_E(TAG, "Failed to process pending folder: %s", pf.repo);
+        }
+        i++;
+    }
+    pendingFoldersCount = 0; // Reset queue after processing.
+}
 
-// Helper to download a file from Github and save it to the storage
+// Helper to download a file from Github and save it to storage.
 bool flip_store_download_github_file(
     FlipperHTTP *fhttp,
     const char *filename,
@@ -14,62 +80,79 @@ bool flip_store_download_github_file(
         FURI_LOG_E(TAG, "Invalid arguments.");
         return false;
     }
-
-    snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s.txt", author, repo, filename);
+    snprintf(fhttp->file_path, sizeof(fhttp->file_path),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s.txt",
+             author, repo, filename);
     fhttp->state = IDLE;
     fhttp->save_received_data = false;
     fhttp->is_bytes_request = true;
 
-    return flipper_http_get_request_bytes(fhttp, link, "{\"Content-Type\":\"application/octet-stream\"}");
+    // return flipper_http_get_request_bytes(fhttp, link, "{\"Content-Type\":\"application/octet-stream\"}");
+    return flipper_http_request(fhttp, BYTES, link, "{\"Content-Type\":\"application/octet-stream\"}", NULL);
 }
 
-bool flip_store_get_github_contents(FlipperHTTP *fhttp, const char *author, const char *repo)
+static bool save_directory(const char *dir)
 {
-    // Create Initial directory
     Storage *storage = furi_record_open(RECORD_STORAGE);
+    if (!storage_common_exists(storage, dir) && storage_common_mkdir(storage, dir) != FSE_OK)
+    {
+        FURI_LOG_E(TAG, "Failed to create directory %s", dir);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    furi_record_close(RECORD_STORAGE);
+    return true;
+}
 
+bool flip_store_get_github_contents(FlipperHTTP *fhttp, const char *author, const char *repo)
+{
+    // Create Initial directories
     char dir[256];
 
     // create a data directory: /ext/apps_data/flip_store/data
     snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data");
-    storage_common_mkdir(storage, dir);
+    save_directory(STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data");
 
     // create a data directory for the author: /ext/apps_data/flip_store/data/author
     snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s", author);
-    storage_common_mkdir(storage, dir);
+    save_directory(dir);
 
-    // example path: /ext/apps_data/flip_store/data/author/info.json
-    snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/info.json", author);
+    // info path: /ext/apps_data/flip_store/data/author/info.json
+    snprintf(fhttp->file_path, sizeof(fhttp->file_path),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/info.json",
+             author);
 
     // create a data directory for the repo: /ext/apps_data/flip_store/data/author/repo
     snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s", author, repo);
-    storage_common_mkdir(storage, dir);
+    save_directory(dir);
 
-    // example path: /ext/apps_data/flip_store/author
+    // create author folder: /ext/apps_data/flip_store/author
     snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s", author);
-    storage_common_mkdir(storage, dir);
+    save_directory(dir);
 
-    // example path: /ext/apps_data/flip_store/author/repo
+    // create repo folder: /ext/apps_data/flip_store/author/repo
     snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s", author, repo);
-    storage_common_mkdir(storage, dir);
-
-    furi_record_close(RECORD_STORAGE);
+    save_directory(dir);
 
     // get the contents of the repo
     char link[256];
     snprintf(link, sizeof(link), "https://api.github.com/repos/%s/%s/contents", author, repo);
     fhttp->save_received_data = true;
-    return flipper_http_get_request_with_headers(fhttp, link, "{\"Content-Type\":\"application/json\"}");
+    // return flipper_http_get_request_with_headers(fhttp, link, "{\"Content-Type\":\"application/json\"}");
+    return flipper_http_request(fhttp, GET, link, "{\"Content-Type\":\"application/json\"}", NULL);
 }
 
-#include <stdio.h>
-#include <string.h>
-#include <stdbool.h>
-
-// Assuming necessary headers and definitions for FuriString, FURI_LOG, etc., are included.
-
+// Recursively (but now iteratively, via queue) parse GitHub contents.
+// 'repo' is the repository path relative to the author’s root.
 bool flip_store_parse_github_contents(char *file_path, const char *author, const char *repo)
 {
+    // Check recursion depth by counting '/' characters.
+    if (count_char(repo, '/') >= MAX_RECURSION_DEPTH)
+    {
+        FURI_LOG_I(TAG, "Max recursion depth reached for repo path: %s", repo);
+        return true;
+    }
+
     FURI_LOG_I(TAG, "Parsing Github contents from %s - %s.", author, repo);
     if (!file_path || !author || !repo)
     {
@@ -77,7 +160,7 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
         return false;
     }
 
-    // Load JSON data from file
+    // Load JSON data from file.
     FuriString *git_data = flipper_http_load_from_file(file_path);
     if (git_data == NULL)
     {
@@ -85,7 +168,7 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
         return false;
     }
 
-    // Allocate a new FuriString to hold the entire JSON structure
+    // Wrap the JSON array in an object for easier parsing.
     FuriString *git_data_str = furi_string_alloc();
     if (!git_data_str)
     {
@@ -93,16 +176,13 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
         furi_string_free(git_data);
         return false;
     }
-
-    // Construct the full JSON string
     furi_string_cat_str(git_data_str, "{\"json_data\":");
     furi_string_cat(git_data_str, git_data);
     furi_string_cat_str(git_data_str, "}");
-    furi_string_free(git_data); // Free the original git_data as it's now part of git_data_str
+    furi_string_free(git_data);
 
-    // Check available memory
-    const size_t additional_bytes = strlen("{\"json_data\":") + 1; // +1 for the closing "}"
-    if (memmgr_get_free_heap() < furi_string_size(git_data_str) + additional_bytes)
+    const size_t additional_bytes = strlen("{\"json_data\":") + 1;
+    if (memmgr_heap_get_max_free_block() < furi_string_size(git_data_str) + additional_bytes)
     {
         FURI_LOG_E(TAG, "Not enough memory to allocate git_data_str.");
         furi_string_free(git_data_str);
@@ -110,16 +190,14 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
     }
 
     int file_count = 0;
-    char dir[512]; // Increased size to accommodate longer paths if necessary
-    FURI_LOG_I(TAG, "Looping through Github files.");
-    FURI_LOG_I(TAG, "Available memory: %d bytes", memmgr_get_free_heap());
+    int folder_count = 0;
+    char dir[256];
+
+    FURI_LOG_I(TAG, "Looping through Github files/folders. Available memory: %d bytes", memmgr_heap_get_max_free_block());
 
-    // Get the C-string and its length for processing
     char *data = (char *)furi_string_get_cstr(git_data_str);
     size_t data_len = furi_string_size(git_data_str);
-
-    size_t pos = 0; // Current position in the data string
-    // Locate the start of the JSON array
+    size_t pos = 0;
     char *array_start = strchr(data, '[');
     if (!array_start)
     {
@@ -127,28 +205,25 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
         furi_string_free(git_data_str);
         return false;
     }
-    pos = array_start - data; // Update position to the start of the array
+    pos = array_start - data;
     size_t brace_count = 0;
     size_t obj_start = 0;
-    bool in_string = false; // To handle braces inside strings
+    bool in_string = false;
 
     while (pos < data_len && file_count < MAX_GITHUB_FILES)
     {
         char current = data[pos];
-
-        // Toggle in_string flag if a quote is found (handling escaped quotes)
         if (current == '"' && (pos == 0 || data[pos - 1] != '\\'))
         {
             in_string = !in_string;
         }
-
         if (!in_string)
         {
             if (current == '{')
             {
                 if (brace_count == 0)
                 {
-                    obj_start = pos; // Potential start of a JSON object
+                    obj_start = pos;
                 }
                 brace_count++;
             }
@@ -159,7 +234,6 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
                 {
                     size_t obj_end = pos;
                     size_t obj_length = obj_end - obj_start + 1;
-                    // Extract the JSON object substring
                     char *obj_str = malloc(obj_length + 1);
                     if (!obj_str)
                     {
@@ -167,21 +241,19 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
                         break;
                     }
                     strncpy(obj_str, data + obj_start, obj_length);
-                    obj_str[obj_length] = '\0'; // Null-terminate
+                    obj_str[obj_length] = '\0';
 
                     FuriString *json_data_array = furi_string_alloc();
-                    furi_string_set(json_data_array, obj_str); // Set the string to the allocated memory
-                    free(obj_str);                             // Free the temporary C-string
-
+                    furi_string_set(json_data_array, obj_str);
+                    free(obj_str);
                     if (!json_data_array)
                     {
                         FURI_LOG_E(TAG, "Failed to initialize json_data_array.");
                         break;
                     }
 
-                    FURI_LOG_I(TAG, "Loaded json data array value %d. Available memory: %d bytes", file_count, memmgr_get_free_heap());
-
-                    // Extract "type" field
+                    FURI_LOG_I(TAG, "Loaded json data object #%d. Available memory: %d bytes",
+                               file_count, memmgr_heap_get_max_free_block());
                     FuriString *type = get_json_value_furi("type", json_data_array);
                     if (!type)
                     {
@@ -189,18 +261,94 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
                         furi_string_free(json_data_array);
                         break;
                     }
-
-                    // Skip non-file types (e.g., directories)
+                    // If not a file, assume it is a folder.
                     if (strcmp(furi_string_get_cstr(type), "file") != 0)
                     {
+                        FuriString *name = get_json_value_furi("name", json_data_array);
+                        if (!name)
+                        {
+                            FURI_LOG_E(TAG, "Failed to get name.");
+                            furi_string_free(type);
+                            furi_string_free(json_data_array);
+                            break;
+                        }
+                        // skip undesired folders (e.g. ".git").
+                        if (strcmp(furi_string_get_cstr(name), ".git") == 0)
+                        {
+                            FURI_LOG_I(TAG, "Skipping folder %s.", furi_string_get_cstr(name));
+                            furi_string_free(name);
+                            furi_string_free(type);
+                            furi_string_free(json_data_array);
+                            size_t remaining_length = data_len - (obj_end + 1);
+                            memmove(data + obj_start, data + obj_end + 1, remaining_length + 1);
+                            data_len -= (obj_end + 1 - obj_start);
+                            pos = obj_start;
+                            continue;
+                        }
+                        FuriString *url = get_json_value_furi("url", json_data_array);
+                        if (!url)
+                        {
+                            FURI_LOG_E(TAG, "Failed to get url.");
+                            furi_string_free(name);
+                            furi_string_free(type);
+                            furi_string_free(json_data_array);
+                            break;
+                        }
+                        // Create the folder on storage.
+                        snprintf(dir, sizeof(dir),
+                                 STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s",
+                                 author, repo, furi_string_get_cstr(name));
+                        save_directory(dir);
+
+                        snprintf(dir, sizeof(dir),
+                                 STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/%s",
+                                 author, repo, furi_string_get_cstr(name));
+                        save_directory(dir);
+
+                        // Save folder JSON for later downloading its contents.
+                        snprintf(dir, sizeof(dir),
+                                 STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/folder%d.json",
+                                 author, repo, folder_count);
+                        FuriString *json = furi_string_alloc();
+                        if (!json)
+                        {
+                            FURI_LOG_E(TAG, "Failed to allocate json.");
+                            furi_string_free(name);
+                            furi_string_free(url);
+                            furi_string_free(type);
+                            furi_string_free(json_data_array);
+                            break;
+                        }
+                        furi_string_cat_str(json, "{\"name\":\"");
+                        furi_string_cat(json, name);
+                        furi_string_cat_str(json, "\",\"link\":\"");
+                        furi_string_cat(json, url);
+                        furi_string_cat_str(json, "\"}");
+
+                        if (!save_char_with_path(dir, furi_string_get_cstr(json)))
+                        {
+                            FURI_LOG_E(TAG, "Failed to save folder json.");
+                        }
+                        FURI_LOG_I(TAG, "Saved folder %s.", furi_string_get_cstr(name));
+                        // Enqueue the folder instead of recursing.
+                        enqueue_folder(dir, repo, furi_string_get_cstr(name));
+
+                        furi_string_free(name);
+                        furi_string_free(url);
                         furi_string_free(type);
+                        furi_string_free(json);
                         furi_string_free(json_data_array);
-                        pos = obj_end + 1; // Move past this object
+                        folder_count++;
+
+                        size_t remaining_length = data_len - (obj_end + 1);
+                        memmove(data + obj_start, data + obj_end + 1, remaining_length + 1);
+                        data_len -= (obj_end + 1 - obj_start);
+                        pos = obj_start;
                         continue;
                     }
                     furi_string_free(type);
 
-                    // Extract "download_url" and "name"
+                    // Process file: extract download_url and name.
                     FuriString *download_url = get_json_value_furi("download_url", json_data_array);
                     if (!download_url)
                     {
@@ -208,7 +356,6 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
                         furi_string_free(json_data_array);
                         break;
                     }
-
                     FuriString *name = get_json_value_furi("name", json_data_array);
                     if (!name)
                     {
@@ -217,11 +364,11 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
                         furi_string_free(download_url);
                         break;
                     }
-
                     furi_string_free(json_data_array);
-                    FURI_LOG_I(TAG, "Received name and download_url. Available memory: %d bytes", memmgr_get_free_heap());
 
-                    // Create JSON to save
+                    FURI_LOG_I(TAG, "Received file %s and download_url. Available memory: %d bytes",
+                               furi_string_get_cstr(name), memmgr_heap_get_max_free_block());
+
                     FuriString *json = furi_string_alloc();
                     if (!json)
                     {
@@ -230,54 +377,67 @@ bool flip_store_parse_github_contents(char *file_path, const char *author, const
                         furi_string_free(name);
                         break;
                     }
-
                     furi_string_cat_str(json, "{\"name\":\"");
                     furi_string_cat(json, name);
                     furi_string_cat_str(json, "\",\"link\":\"");
                     furi_string_cat(json, download_url);
                     furi_string_cat_str(json, "\"}");
 
-                    FURI_LOG_I(TAG, "Created json. Available memory: %d bytes", memmgr_get_free_heap());
-
-                    // Save the JSON to the data folder: /ext/apps_data/flip_store/data/author/repo/fileX.json
-                    snprintf(dir, sizeof(dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/file%d.json", author, repo, file_count);
+                    snprintf(dir, sizeof(dir),
+                             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/file%d.json",
+                             author, repo, file_count);
                     if (!save_char_with_path(dir, furi_string_get_cstr(json)))
                     {
-                        FURI_LOG_E(TAG, "Failed to save json.");
+                        FURI_LOG_E(TAG, "Failed to save file json.");
                     }
-
                     FURI_LOG_I(TAG, "Saved file %s.", furi_string_get_cstr(name));
 
-                    // Free allocated resources
                     furi_string_free(name);
                     furi_string_free(download_url);
                     furi_string_free(json);
-
                     file_count++;
 
-                    // This can be expensive for large strings; consider memory constraints
                     size_t remaining_length = data_len - (obj_end + 1);
-                    memmove(data + obj_start, data + obj_end + 1, remaining_length + 1); // +1 to include null terminator
+                    memmove(data + obj_start, data + obj_end + 1, remaining_length + 1);
                     data_len -= (obj_end + 1 - obj_start);
-                    pos = obj_start; // Reset position to the start of the modified string
+                    pos = obj_start;
                     continue;
                 }
             }
         }
-
         pos++;
     }
 
-    furi_string_free(git_data_str);
-
-    // Save file count
+    // Save file count.
     char file_count_str[16];
     snprintf(file_count_str, sizeof(file_count_str), "%d", file_count);
-    char file_count_dir[512]; // Increased size for longer paths
-    snprintf(file_count_dir, sizeof(file_count_dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/file_count.txt", author);
+    char file_count_dir[256];
+    snprintf(file_count_dir, sizeof(file_count_dir),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/file_count.txt", author);
+    if (!save_char_with_path(file_count_dir, file_count_str))
+    {
+        FURI_LOG_E(TAG, "Failed to save file count.");
+        furi_string_free(git_data_str);
+        return false;
+    }
 
-    FURI_LOG_I(TAG, "Successfully parsed %d files.", file_count);
-    return save_char_with_path(file_count_dir, file_count_str);
+    // Save folder count.
+    char folder_count_str[16];
+    snprintf(folder_count_str, sizeof(folder_count_str), "%d", folder_count);
+    char folder_count_dir[256];
+    snprintf(folder_count_dir, sizeof(folder_count_dir),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/folder_count.txt",
+             author, repo);
+    if (!save_char_with_path(folder_count_dir, folder_count_str))
+    {
+        FURI_LOG_E(TAG, "Failed to save folder count.");
+        furi_string_free(git_data_str);
+        return false;
+    }
+
+    furi_string_free(git_data_str);
+    FURI_LOG_I(TAG, "Successfully parsed %d files and %d folders.", file_count, folder_count);
+    return true;
 }
 
 bool flip_store_install_all_github_files(FlipperHTTP *fhttp, const char *author, const char *repo)
@@ -289,9 +449,11 @@ bool flip_store_install_all_github_files(FlipperHTTP *fhttp, const char *author,
         return false;
     }
     fhttp->state = RECEIVING;
-    // get the file count
-    char file_count_dir[256]; // /ext/apps_data/flip_store/data/author/file_count.txt
-    snprintf(file_count_dir, sizeof(file_count_dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/file_count.txt", author);
+
+    // --- Install files first ---
+    char file_count_dir[256];
+    snprintf(file_count_dir, sizeof(file_count_dir),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/file_count.txt", author);
     FuriString *file_count = flipper_http_load_from_file(file_count_dir);
     if (file_count == NULL)
     {
@@ -301,20 +463,22 @@ bool flip_store_install_all_github_files(FlipperHTTP *fhttp, const char *author,
     int count = atoi(furi_string_get_cstr(file_count));
     furi_string_free(file_count);
 
-    // install all files
-    char file_dir[256]; // /ext/apps_data/flip_store/data/author/repo/file.json
+    char file_dir[256];
     FURI_LOG_I(TAG, "Installing %d files.", count);
     for (int i = 0; i < count; i++)
     {
-        snprintf(file_dir, sizeof(file_dir), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/file%d.json", author, repo, i);
-        FURI_LOG_I(TAG, "Loading file %s. Available memory: %d bytes", file_dir, memmgr_get_free_heap());
-        FuriString *file = flipper_http_load_from_file_with_limit(file_dir, 512);
+        snprintf(file_dir, sizeof(file_dir),
+                 STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/file%d.json",
+                 author, repo, i);
+
+        FuriString *file = flipper_http_load_from_file(file_dir);
         if (!file)
         {
             FURI_LOG_E(TAG, "Failed to load file.");
             return false;
         }
         FURI_LOG_I(TAG, "Loaded file %s.", file_dir);
+
         FuriString *name = get_json_value_furi("name", file);
         if (!name)
         {
@@ -332,37 +496,53 @@ bool flip_store_install_all_github_files(FlipperHTTP *fhttp, const char *author,
         }
         furi_string_free(file);
 
-        bool fetch_file()
-        {
-            return flip_store_download_github_file(fhttp, furi_string_get_cstr(name), author, repo, furi_string_get_cstr(link));
-        }
+        FURI_LOG_I(TAG, "Downloading file %s", furi_string_get_cstr(name));
 
-        bool parse()
+        // fetch_file callback
+        bool fetch_file_result = false;
         {
-            // remove .txt from the filename
-            char current_file_path[512];
-            char new_file_path[512];
-            snprintf(current_file_path, sizeof(current_file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s.txt", author, repo, furi_string_get_cstr(name));
-            snprintf(new_file_path, sizeof(new_file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s", author, repo, furi_string_get_cstr(name));
-            Storage *storage = furi_record_open(RECORD_STORAGE);
-            if (!storage_file_exists(storage, current_file_path))
+            bool fetch_file()
             {
-                FURI_LOG_E(TAG, "Failed to download file.");
-                furi_record_close(RECORD_STORAGE);
-                return false;
+                return flip_store_download_github_file(
+                    fhttp,
+                    furi_string_get_cstr(name),
+                    author,
+                    repo,
+                    furi_string_get_cstr(link));
             }
-            if (storage_common_rename(storage, current_file_path, new_file_path) != FSE_OK)
+
+            // parse_file callback
+            bool parse_file()
             {
-                FURI_LOG_E(TAG, "Failed to rename file.");
+                char current_file_path[256];
+                char new_file_path[256];
+                snprintf(current_file_path, sizeof(current_file_path),
+                         STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s.txt",
+                         author, repo, furi_string_get_cstr(name));
+                snprintf(new_file_path, sizeof(new_file_path),
+                         STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/%s/%s/%s",
+                         author, repo, furi_string_get_cstr(name));
+                Storage *storage = furi_record_open(RECORD_STORAGE);
+                if (!storage_file_exists(storage, current_file_path))
+                {
+                    FURI_LOG_E(TAG, "Failed to download file.");
+                    furi_record_close(RECORD_STORAGE);
+                    return false;
+                }
+                if (storage_common_rename(storage, current_file_path, new_file_path) != FSE_OK)
+                {
+                    FURI_LOG_E(TAG, "Failed to rename file.");
+                    furi_record_close(RECORD_STORAGE);
+                    return false;
+                }
                 furi_record_close(RECORD_STORAGE);
-                return false;
+                return true;
             }
-            furi_record_close(RECORD_STORAGE);
-            return true;
+
+            fetch_file_result = flipper_http_process_response_async(fhttp, fetch_file, parse_file);
         }
-        // download the file and wait until it is downloaded
-        FURI_LOG_I(TAG, "Downloading file %s", furi_string_get_cstr(name));
-        if (!flipper_http_process_response_async(fhttp, fetch_file, parse))
+
+        if (!fetch_file_result)
         {
             FURI_LOG_E(TAG, "Failed to download file.");
             furi_string_free(name);
@@ -374,5 +554,95 @@ bool flip_store_install_all_github_files(FlipperHTTP *fhttp, const char *author,
         furi_string_free(link);
     }
     fhttp->state = IDLE;
+
+    // --- Now install folders ---
+    char folder_count_dir[256];
+    snprintf(folder_count_dir, sizeof(folder_count_dir),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/folder_count.txt",
+             author, repo);
+    FuriString *folder_count = flipper_http_load_from_file(folder_count_dir);
+    if (folder_count == NULL)
+    {
+        FURI_LOG_E(TAG, "Failed to load folder count.");
+        return false;
+    }
+    count = atoi(furi_string_get_cstr(folder_count));
+    furi_string_free(folder_count);
+
+    char folder_dir[256];
+    FURI_LOG_I(TAG, "Installing %d folders.", count);
+    for (int i = 0; i < count; i++)
+    {
+        snprintf(folder_dir, sizeof(folder_dir),
+                 STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/folder%d.json",
+                 author, repo, i);
+
+        FuriString *folder = flipper_http_load_from_file(folder_dir);
+        if (!folder)
+        {
+            FURI_LOG_E(TAG, "Failed to load folder.");
+            return false;
+        }
+        FURI_LOG_I(TAG, "Loaded folder %s.", folder_dir);
+
+        FuriString *name = get_json_value_furi("name", folder);
+        if (!name)
+        {
+            FURI_LOG_E(TAG, "Failed to get name.");
+            furi_string_free(folder);
+            return false;
+        }
+        FuriString *link = get_json_value_furi("link", folder);
+        if (!link)
+        {
+            FURI_LOG_E(TAG, "Failed to get link.");
+            furi_string_free(folder);
+            furi_string_free(name);
+            return false;
+        }
+        furi_string_free(folder);
+
+        FURI_LOG_I(TAG, "Downloading folder %s", furi_string_get_cstr(name));
+
+        // fetch_folder callback
+        bool fetch_folder_result = false;
+        {
+            bool fetch_folder()
+            {
+                fhttp->save_received_data = true;
+                snprintf(fhttp->file_path, sizeof(fhttp->file_path),
+                         STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/data/%s/%s/folder%d.json",
+                         author, repo, i);
+                // return flipper_http_get_request_with_headers(
+                //     fhttp,
+                //     furi_string_get_cstr(link),
+                //     "{\"Content-Type\":\"application/json\"}");
+                return flipper_http_request(fhttp, GET, furi_string_get_cstr(link), "{\"Content-Type\":\"application/json\"}", NULL);
+            }
+
+            // parse_folder callback (just enqueue)
+            bool parse_folder()
+            {
+                return enqueue_folder(fhttp->file_path, repo, furi_string_get_cstr(name));
+            }
+
+            fetch_folder_result = flipper_http_process_response_async(fhttp, fetch_folder, parse_folder);
+        }
+
+        if (!fetch_folder_result)
+        {
+            FURI_LOG_E(TAG, "Failed to download folder.");
+            furi_string_free(name);
+            furi_string_free(link);
+            return false;
+        }
+        FURI_LOG_I(TAG, "Downloaded folder %s", furi_string_get_cstr(name));
+        furi_string_free(name);
+        furi_string_free(link);
+    }
+    fhttp->state = IDLE;
+
+    // Finally, process all pending (enqueued) folders iteratively.
+    process_pending_folders(author);
     return true;
-}
+}

+ 1 - 1
flip_store/github/flip_store_github.h

@@ -1,5 +1,5 @@
 #pragma once
-#include <flip_store.h>
+#include <flip_downloader.h>
 
 /*
 I did try downloading a zip file from Github but a few issues

Някои файлове не бяха показани, защото твърде много файлове са промени