#include "subghz_playlist_creator.h" #include "scenes/scene_menu.h" #include "scenes/scene_popup.h" #include "scenes/scene_text_input.h" #include "scenes/scene_dialog.h" #include "scenes/scene_file_browser.h" #include "scenes/scene_playlist_edit.h" #include #include #include #include #include #include /* Logging */ #include #define TAG "PlaylistCreatorApp" /* generated by fbt from .png files in images folder */ #include #define POPUP_DISPLAY_TIME 2000 // 2 seconds in milliseconds #define PLAYLIST_EXTENSION ".txt" #define PLAYLIST_DIRECTORY "/ext/subghz/playlist" #define MAX_TEXT_LENGTH 128 // Forward declarations static void create_playlist_file(SubGhzPlaylistCreator* app); static void subghz_playlist_creator_dialog_callback(DialogExResult result, void* context); // Replace the back event callback typedef enum { BackEventTypeShort, BackEventTypeLong, } BackEventType; // Custom navigation event callback typedef struct { SubGhzPlaylistCreator* app; ViewDispatcher* dispatcher; } BackEventContext; // Add forward declaration for custom back event handler bool scene_playlist_edit_back_event_callback(void* context); // Helper to read a line from file (since storage_file_read_line is not available) static bool file_read_line(File* file, char* buffer, size_t max_len) { size_t i = 0; char c = 0; while(i + 1 < max_len) { if(storage_file_read(file, &c, 1) != 1) break; if(c == '\n') break; buffer[i++] = c; } buffer[i] = 0; return (i > 0) || (c == '\n'); } static void show_popup(SubGhzPlaylistCreator* app, const char* header, const char* text) { scene_popup_show(app, header, text); } static void create_playlist_file(SubGhzPlaylistCreator* app) { if(!storage_simply_mkdir(app->storage, PLAYLIST_DIRECTORY)) { show_popup(app, "Error", "Failed to create directory"); scene_menu_show(app); return; } File* file = storage_file_alloc(app->storage); if(!file) { show_popup(app, "Error", "Failed to alloc file"); scene_menu_show(app); return; } if(storage_file_open(file, furi_string_get_cstr(app->playlist_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) { const char* header = "# SubGhz Playlist\n"; if(storage_file_write(file, header, strlen(header)) == strlen(header)) { storage_file_close(file); show_popup(app, "Success", "File created!"); scene_playlist_edit_show(app); storage_file_free(file); return; } else { storage_file_close(file); show_popup(app, "Error", "Failed to write file"); } } else { show_popup(app, "Error", "Failed to open file"); } storage_file_free(file); scene_menu_show(app); } static void subghz_playlist_creator_dialog_callback(DialogExResult result, void* context) { SubGhzPlaylistCreator* app = context; if(result == DialogExResultRight) { create_playlist_file(app); } else { scene_text_input_show(app); } } // Callback for file selection from Edit static void on_edit_file_selected(SubGhzPlaylistCreator* app, const char* path) { furi_string_set_str(app->playlist_path, path); const char* filename = strrchr(path, '/'); if(filename) { filename++; furi_string_set_str(app->playlist_name, filename); } // Clear previous playlist state if(app->playlist_entries) { for(size_t i = 0; i < app->playlist_entry_count; ++i) { free(app->playlist_entries[i]); } free(app->playlist_entries); app->playlist_entries = NULL; app->playlist_entry_count = 0; app->playlist_entry_capacity = 0; } // Open and parse the playlist file File* file = storage_file_alloc(app->storage); if(file && storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { char line[256]; while(file_read_line(file, line, sizeof(line))) { // Ignore lines starting with '#' if(line[0] == '#') continue; // Only accept lines starting with 'sub: ' if(strncmp(line, "sub: ", 5) == 0) { char* entry_path = line + 5; // Remove trailing newline (already handled by file_read_line) char* nl = strchr(entry_path, '\n'); if(nl) *nl = 0; // Add to playlist state if(app->playlist_entry_count == app->playlist_entry_capacity) { app->playlist_entry_capacity = app->playlist_entry_capacity ? app->playlist_entry_capacity * 2 : 8; app->playlist_entries = realloc(app->playlist_entries, app->playlist_entry_capacity * sizeof(char*)); } app->playlist_entries[app->playlist_entry_count++] = strdup(entry_path); } } storage_file_close(file); } if(file) storage_file_free(file); // Open the PlaylistEdit scene for the selected playlist scene_playlist_edit_show(app); } static void subghz_playlist_creator_submenu_callback(void* context, uint32_t index) { SubGhzPlaylistCreator* app = context; if(index == 2) { // Exit view_dispatcher_stop(app->view_dispatcher); } else if(index == 0) { // Create memset(app->text_buffer, 0, MAX_TEXT_LENGTH); scene_text_input_show(app); } else if(index == 1) { // Edit scene_file_browser_select(app, PLAYLIST_DIRECTORY, PLAYLIST_EXTENSION, on_edit_file_selected); } } static void subghz_playlist_creator_text_input_callback(void* context) { SubGhzPlaylistCreator* app = context; if(strlen(app->text_buffer) == 0) { show_popup(app, "Error", "Name cannot be empty"); return; } furi_string_set_str(app->playlist_name, app->text_buffer); furi_string_printf(app->playlist_path, "%s/%s%s", PLAYLIST_DIRECTORY, app->text_buffer, PLAYLIST_EXTENSION); File* file = storage_file_alloc(app->storage); bool exists = storage_file_exists(app->storage, furi_string_get_cstr(app->playlist_path)); storage_file_free(file); if(exists) { dialog_ex_set_header(app->dialog, "File exists", 64, 0, AlignCenter, AlignTop); dialog_ex_set_text(app->dialog, "Overwrite?", 64, 32, AlignCenter, AlignCenter); dialog_ex_set_left_button_text(app->dialog, "No"); dialog_ex_set_right_button_text(app->dialog, "Yes"); dialog_ex_set_result_callback(app->dialog, subghz_playlist_creator_dialog_callback); dialog_ex_set_context(app->dialog, app); scene_dialog_show(app); } else { create_playlist_file(app); } } SubGhzPlaylistCreator* subghz_playlist_creator_alloc(void) { SubGhzPlaylistCreator* app = malloc(sizeof(SubGhzPlaylistCreator)); FURI_LOG_D(TAG, "Allocating app"); if(!app) { FURI_LOG_E(TAG, "Failed to allocate app"); return NULL; } app->gui = furi_record_open(RECORD_GUI); FURI_LOG_D(TAG, "Opened GUI record: %p", app->gui); app->storage = furi_record_open(RECORD_STORAGE); FURI_LOG_D(TAG, "Opened Storage record: %p", app->storage); app->view_dispatcher = view_dispatcher_alloc(); FURI_LOG_D(TAG, "Allocated view dispatcher: %p", app->view_dispatcher); app->submenu = submenu_alloc(); FURI_LOG_D(TAG, "Allocated submenu: %p", app->submenu); app->playlist_edit_submenu = submenu_alloc(); FURI_LOG_D(TAG, "Allocated playlist_edit_submenu: %p", app->playlist_edit_submenu); app->popup = popup_alloc(); FURI_LOG_D(TAG, "Allocated popup: %p", app->popup); app->text_input = text_input_alloc(); FURI_LOG_D(TAG, "Allocated text input: %p", app->text_input); app->dialog = dialog_ex_alloc(); FURI_LOG_D(TAG, "Allocated dialog: %p", app->dialog); app->file_browser_result = furi_string_alloc(); FURI_LOG_D(TAG, "Allocated file browser result string: %p", app->file_browser_result); app->file_browser = file_browser_alloc(app->file_browser_result); FURI_LOG_D(TAG, "Allocated file browser: %p", app->file_browser); app->playlist_name = furi_string_alloc(); FURI_LOG_D(TAG, "Allocated playlist name string: %p", app->playlist_name); app->playlist_path = furi_string_alloc(); FURI_LOG_D(TAG, "Allocated playlist path string: %p", app->playlist_path); memset(app->text_buffer, 0, MAX_TEXT_LENGTH); app->is_stopped = false; app->current_view = SubGhzPlaylistCreatorViewSubmenu; // Initialize all views and add them to the dispatcher scene_menu_init_view(app); FURI_LOG_D(TAG, "Initialized menu view"); scene_playlist_edit_init_view(app); FURI_LOG_D(TAG, "Initialized playlist edit view"); scene_file_browser_init_view(app); FURI_LOG_D(TAG, "Initialized file browser view"); scene_text_input_init_view(app); FURI_LOG_D(TAG, "Initialized text input view"); scene_dialog_init_view(app); FURI_LOG_D(TAG, "Initialized dialog view"); // Add views to the dispatcher FURI_LOG_D(TAG, "Adding submenu view %p with ID %lu", submenu_get_view(app->submenu), (uint32_t)SubGhzPlaylistCreatorViewSubmenu); view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewSubmenu, submenu_get_view(app->submenu)); FURI_LOG_D(TAG, "Adding playlist_edit_submenu view %p with ID %lu", submenu_get_view(app->playlist_edit_submenu), (uint32_t)SubGhzPlaylistCreatorViewPlaylistEdit); view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPlaylistEdit, submenu_get_view(app->playlist_edit_submenu)); FURI_LOG_D(TAG, "Adding popup view %p with ID %lu", popup_get_view(app->popup), (uint32_t)SubGhzPlaylistCreatorViewPopup); view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPopup, popup_get_view(app->popup)); FURI_LOG_D(TAG, "Adding text input view %p with ID %lu", text_input_get_view(app->text_input), (uint32_t)SubGhzPlaylistCreatorViewTextInput); view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewTextInput, text_input_get_view(app->text_input)); FURI_LOG_D(TAG, "Adding dialog view %p with ID %lu", dialog_ex_get_view(app->dialog), (uint32_t)SubGhzPlaylistCreatorViewDialog); view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewDialog, dialog_ex_get_view(app->dialog)); FURI_LOG_D(TAG, "Adding file browser view %p with ID %lu", file_browser_get_view(app->file_browser), (uint32_t)SubGhzPlaylistCreatorViewFileBrowser); view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewFileBrowser, file_browser_get_view(app->file_browser)); // Set initial scene scene_menu_show(app); FURI_LOG_D(TAG, "Showing menu scene"); view_dispatcher_set_event_callback_context(app->view_dispatcher, app); view_dispatcher_set_custom_event_callback(app->view_dispatcher, subghz_playlist_creator_custom_callback); view_dispatcher_set_navigation_event_callback(app->view_dispatcher, subghz_playlist_creator_back_event_callback); popup_set_header(app->popup, "SubGhz Playlist Creator", 64, 26, AlignCenter, AlignCenter); FURI_LOG_D(TAG, "Set popup header"); popup_set_text(app->popup, "Welcome!", 64, 40, AlignCenter, AlignCenter); FURI_LOG_D(TAG, "Set popup text"); text_input_set_header_text(app->text_input, "Enter playlist name"); FURI_LOG_D(TAG, "Set text input header"); text_input_set_result_callback( app->text_input, subghz_playlist_creator_text_input_callback, app, app->text_buffer, MAX_TEXT_LENGTH, true); FURI_LOG_D(TAG, "Set text input result callback"); submenu_add_item(app->submenu, "Create", 0, subghz_playlist_creator_submenu_callback, app); FURI_LOG_D(TAG, "Added submenu item 0"); submenu_add_item(app->submenu, "Edit", 1, subghz_playlist_creator_submenu_callback, app); FURI_LOG_D(TAG, "Added submenu item 1"); submenu_add_item(app->submenu, "", 99, NULL, NULL); // blank line FURI_LOG_D(TAG, "Added submenu item 99"); submenu_add_item(app->submenu, "Exit", 2, subghz_playlist_creator_submenu_callback, app); FURI_LOG_D(TAG, "Added submenu item 2"); return app; } void subghz_playlist_creator_free(SubGhzPlaylistCreator* app) { furi_assert(app); FURI_LOG_D(TAG, "Freeing app"); view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewSubmenu); FURI_LOG_D(TAG, "Removed submenu view"); view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPlaylistEdit); FURI_LOG_D(TAG, "Removed playlist_edit_submenu view"); view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPopup); FURI_LOG_D(TAG, "Removed popup view"); view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewTextInput); FURI_LOG_D(TAG, "Removed text input view"); view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewDialog); FURI_LOG_D(TAG, "Removed dialog view"); view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewFileBrowser); FURI_LOG_D(TAG, "Removed file browser view"); // Remove playlist edit view if allocated if(app->playlist_edit_list) { view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPlaylistEdit); variable_item_list_free(app->playlist_edit_list); app->playlist_edit_list = NULL; } view_dispatcher_free(app->view_dispatcher); FURI_LOG_D(TAG, "Freed view dispatcher"); submenu_free(app->submenu); FURI_LOG_D(TAG, "Freed submenu"); submenu_free(app->playlist_edit_submenu); FURI_LOG_D(TAG, "Freed playlist_edit_submenu"); popup_free(app->popup); FURI_LOG_D(TAG, "Freed popup"); text_input_free(app->text_input); FURI_LOG_D(TAG, "Freed text input"); dialog_ex_free(app->dialog); FURI_LOG_D(TAG, "Freed dialog"); file_browser_free(app->file_browser); FURI_LOG_D(TAG, "Freed file browser"); furi_string_free(app->file_browser_result); FURI_LOG_D(TAG, "Freed file browser result string"); furi_string_free(app->playlist_name); FURI_LOG_D(TAG, "Freed playlist name string"); furi_string_free(app->playlist_path); FURI_LOG_D(TAG, "Freed playlist path string"); // Free playlist entries if(app->playlist_entries) { for(size_t i = 0; i < app->playlist_entry_count; ++i) { free(app->playlist_entries[i]); } free(app->playlist_entries); app->playlist_entries = NULL; app->playlist_entry_count = 0; app->playlist_entry_capacity = 0; } furi_record_close(RECORD_GUI); FURI_LOG_D(TAG, "Closed GUI record"); furi_record_close(RECORD_STORAGE); FURI_LOG_D(TAG, "Closed Storage record"); free(app); FURI_LOG_D(TAG, "Freed app"); } bool subghz_playlist_creator_custom_callback(void* context, uint32_t custom_event) { UNUSED(context); // The custom callback is now primarily for the timer event SubGhzPlaylistCreator* app = context; if (custom_event == SubGhzPlaylistCreatorCustomEventShowMenu) { FURI_LOG_D(TAG, "Received custom event to show menu"); // Stop the timer once the event is received furi_timer_stop(app->popup_timer); furi_timer_free(app->popup_timer); app->popup_timer = NULL; // Switch to the main menu scene scene_menu_show(app); return true; } return false; } // Replace the back event callback bool subghz_playlist_creator_back_event_callback(void* context) { SubGhzPlaylistCreator* app = context; FURI_LOG_D(TAG, "Back event callback. is_stopped: %d, current_view: %d", app->is_stopped, app->current_view); if(app->is_stopped) return true; // If in PlaylistEdit, show discard dialog if(app->current_view == SubGhzPlaylistCreatorViewPlaylistEdit) { scene_playlist_edit_back_event_callback(app); return true; } // If not in main menu, go to main menu if(app->current_view != SubGhzPlaylistCreatorViewSubmenu) { FURI_LOG_D(TAG, "Switching to menu scene from view: %d", app->current_view); scene_menu_show(app); return true; } FURI_LOG_D(TAG, "Stopping view dispatcher"); app->is_stopped = true; view_dispatcher_stop(app->view_dispatcher); return true; } int32_t subghz_playlist_creator_app(void* p) { UNUSED(p); FURI_LOG_D(TAG, "App starting"); SubGhzPlaylistCreator* app = subghz_playlist_creator_alloc(); // Allocate the timer here as part of the app struct app->popup_timer = NULL; if(!app) { FURI_LOG_E(TAG, "App allocation failed, exiting"); return 255; } FURI_LOG_D(TAG, "App allocated: %p", app); view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); FURI_LOG_D(TAG, "View dispatcher attached to GUI"); // Set initial scene to menu immediately after attaching dispatcher scene_menu_show(app); FURI_LOG_D(TAG, "Showing menu scene immediately after attach"); view_dispatcher_run(app->view_dispatcher); FURI_LOG_D(TAG, "View dispatcher stopped, freeing app"); subghz_playlist_creator_free(app); FURI_LOG_D(TAG, "App freeing complete, exiting"); return 0; }