subghz_playlist_creator.c 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. #include "subghz_playlist_creator.h"
  2. #include "scenes/scene_menu.h"
  3. #include "scenes/scene_popup.h"
  4. #include "scenes/scene_text_input.h"
  5. #include "scenes/scene_dialog.h"
  6. #include "scenes/scene_file_browser.h"
  7. #include "scenes/scene_playlist_edit.h"
  8. #include <furi_hal.h>
  9. #include <input/input.h>
  10. #include <gui/view.h>
  11. #include <gui/view_dispatcher.h>
  12. #include <gui/modules/submenu.h>
  13. #include <gui/modules/variable_item_list.h>
  14. /* Logging */
  15. #include <furi_hal.h>
  16. #define TAG "PlaylistCreatorApp"
  17. /* generated by fbt from .png files in images folder */
  18. #include <subghz_playlist_creator_icons.h>
  19. #define POPUP_DISPLAY_TIME 2000 // 2 seconds in milliseconds
  20. #define PLAYLIST_EXTENSION ".txt"
  21. #define PLAYLIST_DIRECTORY "/ext/subghz/playlist"
  22. #define MAX_TEXT_LENGTH 128
  23. // Forward declarations
  24. static void create_playlist_file(SubGhzPlaylistCreator* app);
  25. static void subghz_playlist_creator_dialog_callback(DialogExResult result, void* context);
  26. // Replace the back event callback
  27. typedef enum {
  28. BackEventTypeShort,
  29. BackEventTypeLong,
  30. } BackEventType;
  31. // Custom navigation event callback
  32. typedef struct {
  33. SubGhzPlaylistCreator* app;
  34. ViewDispatcher* dispatcher;
  35. } BackEventContext;
  36. // Add forward declaration for custom back event handler
  37. bool scene_playlist_edit_back_event_callback(void* context);
  38. // Helper to read a line from file (since storage_file_read_line is not available)
  39. static bool file_read_line(File* file, char* buffer, size_t max_len) {
  40. size_t i = 0;
  41. char c = 0;
  42. while(i + 1 < max_len) {
  43. if(storage_file_read(file, &c, 1) != 1) break;
  44. if(c == '\n') break;
  45. buffer[i++] = c;
  46. }
  47. buffer[i] = 0;
  48. return (i > 0) || (c == '\n');
  49. }
  50. static void show_popup(SubGhzPlaylistCreator* app, const char* header, const char* text) {
  51. scene_popup_show(app, header, text);
  52. }
  53. static void create_playlist_file(SubGhzPlaylistCreator* app) {
  54. if(!storage_simply_mkdir(app->storage, PLAYLIST_DIRECTORY)) {
  55. show_popup(app, "Error", "Failed to create directory");
  56. scene_menu_show(app);
  57. return;
  58. }
  59. File* file = storage_file_alloc(app->storage);
  60. if(!file) {
  61. show_popup(app, "Error", "Failed to alloc file");
  62. scene_menu_show(app);
  63. return;
  64. }
  65. if(storage_file_open(file, furi_string_get_cstr(app->playlist_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
  66. const char* header = "# SubGhz Playlist\n";
  67. if(storage_file_write(file, header, strlen(header)) == strlen(header)) {
  68. storage_file_close(file);
  69. show_popup(app, "Success", "File created!");
  70. scene_playlist_edit_show(app);
  71. storage_file_free(file);
  72. return;
  73. } else {
  74. storage_file_close(file);
  75. show_popup(app, "Error", "Failed to write file");
  76. }
  77. } else {
  78. show_popup(app, "Error", "Failed to open file");
  79. }
  80. storage_file_free(file);
  81. scene_menu_show(app);
  82. }
  83. static void subghz_playlist_creator_dialog_callback(DialogExResult result, void* context) {
  84. SubGhzPlaylistCreator* app = context;
  85. if(result == DialogExResultRight) {
  86. create_playlist_file(app);
  87. } else {
  88. scene_text_input_show(app);
  89. }
  90. }
  91. // Callback for file selection from Edit
  92. static void on_edit_file_selected(SubGhzPlaylistCreator* app, const char* path) {
  93. furi_string_set_str(app->playlist_path, path);
  94. const char* filename = strrchr(path, '/');
  95. if(filename) {
  96. filename++;
  97. furi_string_set_str(app->playlist_name, filename);
  98. }
  99. // Clear previous playlist state
  100. if(app->playlist_entries) {
  101. for(size_t i = 0; i < app->playlist_entry_count; ++i) {
  102. free(app->playlist_entries[i]);
  103. }
  104. free(app->playlist_entries);
  105. app->playlist_entries = NULL;
  106. app->playlist_entry_count = 0;
  107. app->playlist_entry_capacity = 0;
  108. }
  109. // Open and parse the playlist file
  110. File* file = storage_file_alloc(app->storage);
  111. if(file && storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) {
  112. char line[256];
  113. while(file_read_line(file, line, sizeof(line))) {
  114. // Ignore lines starting with '#'
  115. if(line[0] == '#') continue;
  116. // Only accept lines starting with 'sub: '
  117. if(strncmp(line, "sub: ", 5) == 0) {
  118. char* entry_path = line + 5;
  119. // Remove trailing newline (already handled by file_read_line)
  120. char* nl = strchr(entry_path, '\n');
  121. if(nl) *nl = 0;
  122. // Add to playlist state
  123. if(app->playlist_entry_count == app->playlist_entry_capacity) {
  124. app->playlist_entry_capacity = app->playlist_entry_capacity ? app->playlist_entry_capacity * 2 : 8;
  125. app->playlist_entries = realloc(app->playlist_entries, app->playlist_entry_capacity * sizeof(char*));
  126. }
  127. app->playlist_entries[app->playlist_entry_count++] = strdup(entry_path);
  128. }
  129. }
  130. storage_file_close(file);
  131. }
  132. if(file) storage_file_free(file);
  133. // Open the PlaylistEdit scene for the selected playlist
  134. scene_playlist_edit_show(app);
  135. }
  136. static void subghz_playlist_creator_submenu_callback(void* context, uint32_t index) {
  137. SubGhzPlaylistCreator* app = context;
  138. if(index == 2) { // Exit
  139. view_dispatcher_stop(app->view_dispatcher);
  140. } else if(index == 0) { // Create
  141. memset(app->text_buffer, 0, MAX_TEXT_LENGTH);
  142. scene_text_input_show(app);
  143. } else if(index == 1) { // Edit
  144. scene_file_browser_select(app, PLAYLIST_DIRECTORY, PLAYLIST_EXTENSION, on_edit_file_selected);
  145. }
  146. }
  147. static void subghz_playlist_creator_text_input_callback(void* context) {
  148. SubGhzPlaylistCreator* app = context;
  149. if(strlen(app->text_buffer) == 0) {
  150. show_popup(app, "Error", "Name cannot be empty");
  151. return;
  152. }
  153. furi_string_set_str(app->playlist_name, app->text_buffer);
  154. furi_string_printf(app->playlist_path, "%s/%s%s", PLAYLIST_DIRECTORY, app->text_buffer, PLAYLIST_EXTENSION);
  155. File* file = storage_file_alloc(app->storage);
  156. bool exists = storage_file_exists(app->storage, furi_string_get_cstr(app->playlist_path));
  157. storage_file_free(file);
  158. if(exists) {
  159. dialog_ex_set_header(app->dialog, "File exists", 64, 0, AlignCenter, AlignTop);
  160. dialog_ex_set_text(app->dialog, "Overwrite?", 64, 32, AlignCenter, AlignCenter);
  161. dialog_ex_set_left_button_text(app->dialog, "No");
  162. dialog_ex_set_right_button_text(app->dialog, "Yes");
  163. dialog_ex_set_result_callback(app->dialog, subghz_playlist_creator_dialog_callback);
  164. dialog_ex_set_context(app->dialog, app);
  165. scene_dialog_show(app);
  166. } else {
  167. create_playlist_file(app);
  168. }
  169. }
  170. SubGhzPlaylistCreator* subghz_playlist_creator_alloc(void) {
  171. SubGhzPlaylistCreator* app = malloc(sizeof(SubGhzPlaylistCreator));
  172. FURI_LOG_D(TAG, "Allocating app");
  173. if(!app) {
  174. FURI_LOG_E(TAG, "Failed to allocate app");
  175. return NULL;
  176. }
  177. app->gui = furi_record_open(RECORD_GUI);
  178. FURI_LOG_D(TAG, "Opened GUI record: %p", app->gui);
  179. app->storage = furi_record_open(RECORD_STORAGE);
  180. FURI_LOG_D(TAG, "Opened Storage record: %p", app->storage);
  181. app->view_dispatcher = view_dispatcher_alloc();
  182. FURI_LOG_D(TAG, "Allocated view dispatcher: %p", app->view_dispatcher);
  183. app->submenu = submenu_alloc();
  184. FURI_LOG_D(TAG, "Allocated submenu: %p", app->submenu);
  185. app->playlist_edit_submenu = submenu_alloc();
  186. FURI_LOG_D(TAG, "Allocated playlist_edit_submenu: %p", app->playlist_edit_submenu);
  187. app->popup = popup_alloc();
  188. FURI_LOG_D(TAG, "Allocated popup: %p", app->popup);
  189. app->text_input = text_input_alloc();
  190. FURI_LOG_D(TAG, "Allocated text input: %p", app->text_input);
  191. app->dialog = dialog_ex_alloc();
  192. FURI_LOG_D(TAG, "Allocated dialog: %p", app->dialog);
  193. app->file_browser_result = furi_string_alloc();
  194. FURI_LOG_D(TAG, "Allocated file browser result string: %p", app->file_browser_result);
  195. app->file_browser = file_browser_alloc(app->file_browser_result);
  196. FURI_LOG_D(TAG, "Allocated file browser: %p", app->file_browser);
  197. app->playlist_name = furi_string_alloc();
  198. FURI_LOG_D(TAG, "Allocated playlist name string: %p", app->playlist_name);
  199. app->playlist_path = furi_string_alloc();
  200. FURI_LOG_D(TAG, "Allocated playlist path string: %p", app->playlist_path);
  201. memset(app->text_buffer, 0, MAX_TEXT_LENGTH);
  202. app->is_stopped = false;
  203. app->current_view = SubGhzPlaylistCreatorViewSubmenu;
  204. // Initialize all views and add them to the dispatcher
  205. scene_menu_init_view(app);
  206. FURI_LOG_D(TAG, "Initialized menu view");
  207. scene_playlist_edit_init_view(app);
  208. FURI_LOG_D(TAG, "Initialized playlist edit view");
  209. scene_file_browser_init_view(app);
  210. FURI_LOG_D(TAG, "Initialized file browser view");
  211. scene_text_input_init_view(app);
  212. FURI_LOG_D(TAG, "Initialized text input view");
  213. scene_dialog_init_view(app);
  214. FURI_LOG_D(TAG, "Initialized dialog view");
  215. // Add views to the dispatcher
  216. FURI_LOG_D(TAG, "Adding submenu view %p with ID %lu", submenu_get_view(app->submenu), (uint32_t)SubGhzPlaylistCreatorViewSubmenu);
  217. view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewSubmenu, submenu_get_view(app->submenu));
  218. FURI_LOG_D(TAG, "Adding playlist_edit_submenu view %p with ID %lu", submenu_get_view(app->playlist_edit_submenu), (uint32_t)SubGhzPlaylistCreatorViewPlaylistEdit);
  219. view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPlaylistEdit, submenu_get_view(app->playlist_edit_submenu));
  220. FURI_LOG_D(TAG, "Adding popup view %p with ID %lu", popup_get_view(app->popup), (uint32_t)SubGhzPlaylistCreatorViewPopup);
  221. view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPopup, popup_get_view(app->popup));
  222. FURI_LOG_D(TAG, "Adding text input view %p with ID %lu", text_input_get_view(app->text_input), (uint32_t)SubGhzPlaylistCreatorViewTextInput);
  223. view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewTextInput, text_input_get_view(app->text_input));
  224. FURI_LOG_D(TAG, "Adding dialog view %p with ID %lu", dialog_ex_get_view(app->dialog), (uint32_t)SubGhzPlaylistCreatorViewDialog);
  225. view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewDialog, dialog_ex_get_view(app->dialog));
  226. FURI_LOG_D(TAG, "Adding file browser view %p with ID %lu", file_browser_get_view(app->file_browser), (uint32_t)SubGhzPlaylistCreatorViewFileBrowser);
  227. view_dispatcher_add_view(app->view_dispatcher, SubGhzPlaylistCreatorViewFileBrowser, file_browser_get_view(app->file_browser));
  228. // Set initial scene
  229. scene_menu_show(app);
  230. FURI_LOG_D(TAG, "Showing menu scene");
  231. view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
  232. view_dispatcher_set_custom_event_callback(app->view_dispatcher, subghz_playlist_creator_custom_callback);
  233. view_dispatcher_set_navigation_event_callback(app->view_dispatcher, subghz_playlist_creator_back_event_callback);
  234. popup_set_header(app->popup, "SubGhz Playlist Creator", 64, 26, AlignCenter, AlignCenter);
  235. FURI_LOG_D(TAG, "Set popup header");
  236. popup_set_text(app->popup, "Welcome!", 64, 40, AlignCenter, AlignCenter);
  237. FURI_LOG_D(TAG, "Set popup text");
  238. text_input_set_header_text(app->text_input, "Enter playlist name");
  239. FURI_LOG_D(TAG, "Set text input header");
  240. text_input_set_result_callback(
  241. app->text_input,
  242. subghz_playlist_creator_text_input_callback,
  243. app,
  244. app->text_buffer,
  245. MAX_TEXT_LENGTH,
  246. true);
  247. FURI_LOG_D(TAG, "Set text input result callback");
  248. submenu_add_item(app->submenu, "Create", 0, subghz_playlist_creator_submenu_callback, app);
  249. FURI_LOG_D(TAG, "Added submenu item 0");
  250. submenu_add_item(app->submenu, "Edit", 1, subghz_playlist_creator_submenu_callback, app);
  251. FURI_LOG_D(TAG, "Added submenu item 1");
  252. submenu_add_item(app->submenu, "", 99, NULL, NULL); // blank line
  253. FURI_LOG_D(TAG, "Added submenu item 99");
  254. submenu_add_item(app->submenu, "Exit", 2, subghz_playlist_creator_submenu_callback, app);
  255. FURI_LOG_D(TAG, "Added submenu item 2");
  256. return app;
  257. }
  258. void subghz_playlist_creator_free(SubGhzPlaylistCreator* app) {
  259. furi_assert(app);
  260. FURI_LOG_D(TAG, "Freeing app");
  261. view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewSubmenu);
  262. FURI_LOG_D(TAG, "Removed submenu view");
  263. view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPlaylistEdit);
  264. FURI_LOG_D(TAG, "Removed playlist_edit_submenu view");
  265. view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPopup);
  266. FURI_LOG_D(TAG, "Removed popup view");
  267. view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewTextInput);
  268. FURI_LOG_D(TAG, "Removed text input view");
  269. view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewDialog);
  270. FURI_LOG_D(TAG, "Removed dialog view");
  271. view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewFileBrowser);
  272. FURI_LOG_D(TAG, "Removed file browser view");
  273. // Remove playlist edit view if allocated
  274. if(app->playlist_edit_list) {
  275. view_dispatcher_remove_view(app->view_dispatcher, SubGhzPlaylistCreatorViewPlaylistEdit);
  276. variable_item_list_free(app->playlist_edit_list);
  277. app->playlist_edit_list = NULL;
  278. }
  279. view_dispatcher_free(app->view_dispatcher);
  280. FURI_LOG_D(TAG, "Freed view dispatcher");
  281. submenu_free(app->submenu);
  282. FURI_LOG_D(TAG, "Freed submenu");
  283. submenu_free(app->playlist_edit_submenu);
  284. FURI_LOG_D(TAG, "Freed playlist_edit_submenu");
  285. popup_free(app->popup);
  286. FURI_LOG_D(TAG, "Freed popup");
  287. text_input_free(app->text_input);
  288. FURI_LOG_D(TAG, "Freed text input");
  289. dialog_ex_free(app->dialog);
  290. FURI_LOG_D(TAG, "Freed dialog");
  291. file_browser_free(app->file_browser);
  292. FURI_LOG_D(TAG, "Freed file browser");
  293. furi_string_free(app->file_browser_result);
  294. FURI_LOG_D(TAG, "Freed file browser result string");
  295. furi_string_free(app->playlist_name);
  296. FURI_LOG_D(TAG, "Freed playlist name string");
  297. furi_string_free(app->playlist_path);
  298. FURI_LOG_D(TAG, "Freed playlist path string");
  299. // Free playlist entries
  300. if(app->playlist_entries) {
  301. for(size_t i = 0; i < app->playlist_entry_count; ++i) {
  302. free(app->playlist_entries[i]);
  303. }
  304. free(app->playlist_entries);
  305. app->playlist_entries = NULL;
  306. app->playlist_entry_count = 0;
  307. app->playlist_entry_capacity = 0;
  308. }
  309. furi_record_close(RECORD_GUI);
  310. FURI_LOG_D(TAG, "Closed GUI record");
  311. furi_record_close(RECORD_STORAGE);
  312. FURI_LOG_D(TAG, "Closed Storage record");
  313. free(app);
  314. FURI_LOG_D(TAG, "Freed app");
  315. }
  316. bool subghz_playlist_creator_custom_callback(void* context, uint32_t custom_event) {
  317. UNUSED(context);
  318. // The custom callback is now primarily for the timer event
  319. SubGhzPlaylistCreator* app = context;
  320. if (custom_event == SubGhzPlaylistCreatorCustomEventShowMenu) {
  321. FURI_LOG_D(TAG, "Received custom event to show menu");
  322. // Stop the timer once the event is received
  323. furi_timer_stop(app->popup_timer);
  324. furi_timer_free(app->popup_timer);
  325. app->popup_timer = NULL;
  326. // Switch to the main menu scene
  327. scene_menu_show(app);
  328. return true;
  329. }
  330. return false;
  331. }
  332. // Replace the back event callback
  333. bool subghz_playlist_creator_back_event_callback(void* context) {
  334. SubGhzPlaylistCreator* app = context;
  335. FURI_LOG_D(TAG, "Back event callback. is_stopped: %d, current_view: %d", app->is_stopped, app->current_view);
  336. if(app->is_stopped) return true;
  337. // If in PlaylistEdit, show discard dialog
  338. if(app->current_view == SubGhzPlaylistCreatorViewPlaylistEdit) {
  339. scene_playlist_edit_back_event_callback(app);
  340. return true;
  341. }
  342. // If not in main menu, go to main menu
  343. if(app->current_view != SubGhzPlaylistCreatorViewSubmenu) {
  344. FURI_LOG_D(TAG, "Switching to menu scene from view: %d", app->current_view);
  345. scene_menu_show(app);
  346. return true;
  347. }
  348. FURI_LOG_D(TAG, "Stopping view dispatcher");
  349. app->is_stopped = true;
  350. view_dispatcher_stop(app->view_dispatcher);
  351. return true;
  352. }
  353. int32_t subghz_playlist_creator_app(void* p) {
  354. UNUSED(p);
  355. FURI_LOG_D(TAG, "App starting");
  356. SubGhzPlaylistCreator* app = subghz_playlist_creator_alloc();
  357. // Allocate the timer here as part of the app struct
  358. app->popup_timer = NULL;
  359. if(!app) {
  360. FURI_LOG_E(TAG, "App allocation failed, exiting");
  361. return 255;
  362. }
  363. FURI_LOG_D(TAG, "App allocated: %p", app);
  364. view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
  365. FURI_LOG_D(TAG, "View dispatcher attached to GUI");
  366. // Set initial scene to menu immediately after attaching dispatcher
  367. scene_menu_show(app);
  368. FURI_LOG_D(TAG, "Showing menu scene immediately after attach");
  369. view_dispatcher_run(app->view_dispatcher);
  370. FURI_LOG_D(TAG, "View dispatcher stopped, freeing app");
  371. subghz_playlist_creator_free(app);
  372. FURI_LOG_D(TAG, "App freeing complete, exiting");
  373. return 0;
  374. }