archive.c 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. #include "archive_i.h"
  2. static bool archive_get_filenames(ArchiveApp* archive);
  3. static bool is_favorite(ArchiveApp* archive, ArchiveFile_t* file) {
  4. FS_Common_Api* common_api = &archive->fs_api->common;
  5. FileInfo file_info;
  6. FS_Error fr;
  7. string_t path;
  8. string_init_printf(path, "favorites/%s", string_get_cstr(file->name));
  9. fr = common_api->info(string_get_cstr(path), &file_info, NULL, 0);
  10. FURI_LOG_I("FAV", "%d", fr);
  11. string_clear(path);
  12. return fr == 0 || fr == 2;
  13. }
  14. static void update_offset(ArchiveApp* archive) {
  15. furi_assert(archive);
  16. with_view_model(
  17. archive->view_archive_main, (ArchiveViewModel * model) {
  18. size_t array_size = files_array_size(model->files);
  19. uint16_t bounds = array_size > 3 ? 2 : array_size;
  20. if(array_size > 3 && model->idx >= array_size - 1) {
  21. model->list_offset = model->idx - 3;
  22. } else if(model->list_offset < model->idx - bounds) {
  23. model->list_offset = CLAMP(model->list_offset + 1, array_size - bounds, 0);
  24. } else if(model->list_offset > model->idx - bounds) {
  25. model->list_offset = CLAMP(model->idx - 1, array_size - bounds, 0);
  26. }
  27. return true;
  28. });
  29. }
  30. static void archive_update_last_idx(ArchiveApp* archive) {
  31. furi_assert(archive);
  32. with_view_model(
  33. archive->view_archive_main, (ArchiveViewModel * model) {
  34. archive->browser.last_idx[archive->browser.depth] =
  35. CLAMP(model->idx, files_array_size(model->files) - 1, 0);
  36. model->idx = 0;
  37. return true;
  38. });
  39. }
  40. static void archive_switch_dir(ArchiveApp* archive, const char* path) {
  41. furi_assert(archive);
  42. furi_assert(path);
  43. string_set(archive->browser.path, path);
  44. archive_get_filenames(archive);
  45. update_offset(archive);
  46. }
  47. static void archive_switch_tab(ArchiveApp* archive) {
  48. furi_assert(archive);
  49. with_view_model(
  50. archive->view_archive_main, (ArchiveViewModel * model) {
  51. model->tab_idx = archive->browser.tab_id;
  52. model->idx = 0;
  53. return true;
  54. });
  55. archive->browser.depth = 0;
  56. archive_switch_dir(archive, tab_default_paths[archive->browser.tab_id]);
  57. }
  58. static void archive_leave_dir(ArchiveApp* archive) {
  59. furi_assert(archive);
  60. char* last_char_ptr = strrchr(string_get_cstr(archive->browser.path), '/');
  61. if(last_char_ptr) {
  62. size_t pos = last_char_ptr - string_get_cstr(archive->browser.path);
  63. string_left(archive->browser.path, pos);
  64. }
  65. archive->browser.depth = CLAMP(archive->browser.depth - 1, MAX_DEPTH, 0);
  66. with_view_model(
  67. archive->view_archive_main, (ArchiveViewModel * model) {
  68. model->idx = archive->browser.last_idx[archive->browser.depth];
  69. model->list_offset =
  70. model->idx -
  71. (files_array_size(model->files) > 3 ? 3 : files_array_size(model->files));
  72. return true;
  73. });
  74. archive_switch_dir(archive, string_get_cstr(archive->browser.path));
  75. }
  76. static void archive_enter_dir(ArchiveApp* archive, string_t name) {
  77. furi_assert(archive);
  78. furi_assert(name);
  79. archive_update_last_idx(archive);
  80. archive->browser.depth = CLAMP(archive->browser.depth + 1, MAX_DEPTH, 0);
  81. string_cat(archive->browser.path, "/");
  82. string_cat(archive->browser.path, archive->browser.name);
  83. archive_switch_dir(archive, string_get_cstr(archive->browser.path));
  84. }
  85. static bool filter_by_extension(ArchiveApp* archive, FileInfo* file_info, const char* name) {
  86. furi_assert(archive);
  87. furi_assert(file_info);
  88. furi_assert(name);
  89. bool result = false;
  90. const char* filter_ext_ptr = get_tab_ext(archive->browser.tab_id);
  91. if(strcmp(filter_ext_ptr, "*") == 0) {
  92. result = true;
  93. } else if(strstr(name, filter_ext_ptr) != NULL) {
  94. result = true;
  95. } else if(file_info->flags & FSF_DIRECTORY) {
  96. result = true;
  97. }
  98. return result;
  99. }
  100. static void set_file_type(ArchiveFile_t* file, FileInfo* file_info) {
  101. furi_assert(file);
  102. furi_assert(file_info);
  103. for(size_t i = 0; i < SIZEOF_ARRAY(known_ext); i++) {
  104. if(string_search_str(file->name, known_ext[i], 0) != STRING_FAILURE) {
  105. file->type = i;
  106. return;
  107. }
  108. }
  109. if(file_info->flags & FSF_DIRECTORY) {
  110. file->type = ArchiveFileTypeFolder;
  111. } else {
  112. file->type = ArchiveFileTypeUnknown;
  113. }
  114. }
  115. static bool archive_get_filenames(ArchiveApp* archive) {
  116. furi_assert(archive);
  117. FS_Dir_Api* dir_api = &archive->fs_api->dir;
  118. ArchiveFile_t item;
  119. FileInfo file_info;
  120. File directory;
  121. char name[MAX_NAME_LEN];
  122. bool result;
  123. result = dir_api->open(&directory, string_get_cstr(archive->browser.path));
  124. with_view_model(
  125. archive->view_archive_main, (ArchiveViewModel * model) {
  126. files_array_clean(model->files);
  127. return true;
  128. });
  129. if(!result) {
  130. dir_api->close(&directory);
  131. return false;
  132. }
  133. while(1) {
  134. result = dir_api->read(&directory, &file_info, name, MAX_NAME_LEN);
  135. if(directory.error_id == FSE_NOT_EXIST || name[0] == 0) {
  136. break;
  137. }
  138. if(result) {
  139. uint16_t files_cnt;
  140. with_view_model(
  141. archive->view_archive_main, (ArchiveViewModel * model) {
  142. files_cnt = files_array_size(model->files);
  143. return true;
  144. });
  145. if(files_cnt > MAX_FILES) {
  146. break;
  147. } else if(directory.error_id == FSE_OK) {
  148. if(filter_by_extension(archive, &file_info, name)) {
  149. ArchiveFile_t_init(&item);
  150. string_init_set(item.name, name);
  151. set_file_type(&item, &file_info);
  152. with_view_model(
  153. archive->view_archive_main, (ArchiveViewModel * model) {
  154. files_array_push_back(model->files, item);
  155. return true;
  156. });
  157. ArchiveFile_t_clear(&item);
  158. }
  159. } else {
  160. dir_api->close(&directory);
  161. return false;
  162. }
  163. }
  164. }
  165. dir_api->close(&directory);
  166. return true;
  167. }
  168. static void archive_exit_callback(ArchiveApp* archive) {
  169. furi_assert(archive);
  170. AppEvent event;
  171. event.type = EventTypeExit;
  172. furi_check(osMessageQueuePut(archive->event_queue, &event, 0, osWaitForever) == osOK);
  173. }
  174. static uint32_t archive_previous_callback(void* context) {
  175. return ArchiveViewMain;
  176. }
  177. /* file menu */
  178. static void archive_add_to_favorites(ArchiveApp* archive) {
  179. furi_assert(archive);
  180. FS_Common_Api* common_api = &archive->fs_api->common;
  181. common_api->mkdir("favorites");
  182. FS_File_Api* file_api = &archive->fs_api->file;
  183. File src;
  184. File dst;
  185. bool fr;
  186. uint16_t buffer[MAX_FILE_SIZE];
  187. uint16_t bw = 0;
  188. uint16_t br = 0;
  189. string_t buffer_src;
  190. string_t buffer_dst;
  191. string_init_printf(
  192. buffer_src,
  193. "%s/%s",
  194. string_get_cstr(archive->browser.path),
  195. string_get_cstr(archive->browser.name));
  196. string_init_printf(buffer_dst, "/favorites/%s", string_get_cstr(archive->browser.name));
  197. fr = file_api->open(&src, string_get_cstr(buffer_src), FSAM_READ, FSOM_OPEN_EXISTING);
  198. FURI_LOG_I("FATFS", "OPEN: %d", fr);
  199. fr = file_api->open(&dst, string_get_cstr(buffer_dst), FSAM_WRITE, FSOM_CREATE_ALWAYS);
  200. FURI_LOG_I("FATFS", "CREATE: %d", fr);
  201. for(;;) {
  202. br = file_api->read(&src, &buffer, sizeof(buffer));
  203. if(br == 0) break;
  204. bw = file_api->write(&dst, &buffer, sizeof(buffer));
  205. if(bw < br) break;
  206. }
  207. file_api->close(&src);
  208. file_api->close(&dst);
  209. string_clear(buffer_src);
  210. string_clear(buffer_dst);
  211. }
  212. static void archive_text_input_callback(void* context) {
  213. furi_assert(context);
  214. ArchiveApp* archive = (ArchiveApp*)context;
  215. FS_Common_Api* common_api = &archive->fs_api->common;
  216. string_t buffer_src;
  217. string_t buffer_dst;
  218. string_init_printf(
  219. buffer_src,
  220. "%s/%s",
  221. string_get_cstr(archive->browser.path),
  222. string_get_cstr(archive->browser.name));
  223. string_init_printf(
  224. buffer_dst,
  225. "%s/%s",
  226. string_get_cstr(archive->browser.path),
  227. archive->browser.text_input_buffer);
  228. // append extension
  229. ArchiveFile_t* file;
  230. with_view_model(
  231. archive->view_archive_main, (ArchiveViewModel * model) {
  232. file = files_array_get(
  233. model->files, CLAMP(model->idx, files_array_size(model->files) - 1, 0));
  234. return true;
  235. });
  236. string_cat(buffer_dst, known_ext[file->type]);
  237. common_api->rename(string_get_cstr(buffer_src), string_get_cstr(buffer_dst));
  238. view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewMain);
  239. string_clear(buffer_src);
  240. string_clear(buffer_dst);
  241. archive_get_filenames(archive);
  242. }
  243. static void archive_enter_text_input(ArchiveApp* archive) {
  244. furi_assert(archive);
  245. *archive->browser.text_input_buffer = '\0';
  246. strlcpy(
  247. archive->browser.text_input_buffer, string_get_cstr(archive->browser.name), MAX_NAME_LEN);
  248. archive_trim_file_ext(archive->browser.text_input_buffer);
  249. text_input_set_header_text(archive->text_input, "Rename:");
  250. text_input_set_result_callback(
  251. archive->text_input,
  252. archive_text_input_callback,
  253. archive,
  254. archive->browser.text_input_buffer,
  255. MAX_NAME_LEN);
  256. view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewTextInput);
  257. }
  258. static void archive_show_file_menu(ArchiveApp* archive) {
  259. furi_assert(archive);
  260. archive->browser.menu = true;
  261. with_view_model(
  262. archive->view_archive_main, (ArchiveViewModel * model) {
  263. ArchiveFile_t* selected;
  264. selected = files_array_get(model->files, model->idx);
  265. model->menu = true;
  266. model->menu_idx = 0;
  267. selected->fav = is_favorite(archive, selected);
  268. return true;
  269. });
  270. }
  271. static void archive_close_file_menu(ArchiveApp* archive) {
  272. furi_assert(archive);
  273. archive->browser.menu = false;
  274. with_view_model(
  275. archive->view_archive_main, (ArchiveViewModel * model) {
  276. model->menu = false;
  277. model->menu_idx = 0;
  278. return true;
  279. });
  280. }
  281. static void archive_open_app(ArchiveApp* archive, const char* app_name, const char* args) {
  282. furi_assert(archive);
  283. furi_assert(app_name);
  284. loader_start(archive->loader, app_name, args);
  285. }
  286. static void archive_delete_file(ArchiveApp* archive, ArchiveFile_t* file, bool fav, bool orig) {
  287. furi_assert(archive);
  288. furi_assert(file);
  289. FS_Common_Api* common_api = &archive->fs_api->common;
  290. string_t path;
  291. string_init(path);
  292. if(!fav && !orig) {
  293. string_printf(
  294. path, "%s/%s", string_get_cstr(archive->browser.path), string_get_cstr(file->name));
  295. common_api->remove(string_get_cstr(path));
  296. } else { // remove from favorites
  297. string_printf(path, "favorites/%s", string_get_cstr(file->name));
  298. common_api->remove(string_get_cstr(path));
  299. if(orig) { // remove original file
  300. string_printf(
  301. path, "%s/%s", get_default_path(file->type), string_get_cstr(file->name));
  302. common_api->remove(string_get_cstr(path));
  303. }
  304. }
  305. string_clear(path);
  306. archive_get_filenames(archive);
  307. with_view_model(
  308. archive->view_archive_main, (ArchiveViewModel * model) {
  309. model->idx = CLAMP(model->idx, files_array_size(model->files) - 1, 0);
  310. return true;
  311. });
  312. update_offset(archive);
  313. }
  314. static void archive_file_menu_callback(ArchiveApp* archive) {
  315. furi_assert(archive);
  316. ArchiveFile_t* selected;
  317. uint8_t idx = 0;
  318. with_view_model(
  319. archive->view_archive_main, (ArchiveViewModel * model) {
  320. selected = files_array_get(model->files, model->idx);
  321. idx = model->menu_idx;
  322. return true;
  323. });
  324. switch(idx) {
  325. case 0:
  326. if(is_known_app(selected->type)) {
  327. string_t full_path;
  328. string_init_printf(
  329. full_path,
  330. "%s/%s",
  331. string_get_cstr(archive->browser.path),
  332. string_get_cstr(selected->name));
  333. archive_open_app(
  334. archive, flipper_app_name[selected->type], string_get_cstr(full_path));
  335. string_clear(full_path);
  336. }
  337. break;
  338. case 1:
  339. if(is_known_app(selected->type)) {
  340. if(!is_favorite(archive, selected)) {
  341. string_set(archive->browser.name, selected->name);
  342. archive_add_to_favorites(archive);
  343. } else {
  344. // delete from favorites
  345. archive_delete_file(archive, selected, true, false);
  346. }
  347. archive_close_file_menu(archive);
  348. }
  349. break;
  350. case 2:
  351. // open rename view
  352. if(is_known_app(selected->type)) {
  353. archive_enter_text_input(archive);
  354. }
  355. break;
  356. case 3:
  357. // confirmation?
  358. if(is_favorite(archive, selected)) {
  359. //delete both fav & original
  360. archive_delete_file(archive, selected, true, true);
  361. } else {
  362. archive_delete_file(archive, selected, false, false);
  363. }
  364. archive_close_file_menu(archive);
  365. break;
  366. default:
  367. archive_close_file_menu(archive);
  368. break;
  369. }
  370. selected = NULL;
  371. }
  372. static void menu_input_handler(ArchiveApp* archive, InputEvent* event) {
  373. furi_assert(archive);
  374. furi_assert(archive);
  375. if(event->type == InputTypeShort) {
  376. if(event->key == InputKeyUp || event->key == InputKeyDown) {
  377. with_view_model(
  378. archive->view_archive_main, (ArchiveViewModel * model) {
  379. if(event->key == InputKeyUp) {
  380. model->menu_idx = ((model->menu_idx - 1) + MENU_ITEMS) % MENU_ITEMS;
  381. } else if(event->key == InputKeyDown) {
  382. model->menu_idx = (model->menu_idx + 1) % MENU_ITEMS;
  383. }
  384. return true;
  385. });
  386. }
  387. if(event->key == InputKeyOk) {
  388. archive_file_menu_callback(archive);
  389. } else if(event->key == InputKeyBack) {
  390. archive_close_file_menu(archive);
  391. }
  392. }
  393. }
  394. /* main controls */
  395. static bool archive_view_input(InputEvent* event, void* context) {
  396. furi_assert(event);
  397. furi_assert(context);
  398. ArchiveApp* archive = context;
  399. bool in_menu = archive->browser.menu;
  400. if(in_menu) {
  401. menu_input_handler(archive, event);
  402. return true;
  403. }
  404. if(event->type == InputTypeShort) {
  405. if(event->key == InputKeyLeft) {
  406. if(archive->browser.tab_id > 0) {
  407. archive->browser.tab_id = CLAMP(archive->browser.tab_id - 1, ArchiveTabTotal, 0);
  408. archive_switch_tab(archive);
  409. return true;
  410. }
  411. } else if(event->key == InputKeyRight) {
  412. if(archive->browser.tab_id < ArchiveTabTotal - 1) {
  413. archive->browser.tab_id =
  414. CLAMP(archive->browser.tab_id + 1, ArchiveTabTotal - 1, 0);
  415. archive_switch_tab(archive);
  416. return true;
  417. }
  418. } else if(event->key == InputKeyBack) {
  419. if(archive->browser.depth == 0) {
  420. archive_exit_callback(archive);
  421. } else {
  422. archive_leave_dir(archive);
  423. }
  424. return true;
  425. }
  426. }
  427. if(event->key == InputKeyUp || event->key == InputKeyDown) {
  428. with_view_model(
  429. archive->view_archive_main, (ArchiveViewModel * model) {
  430. uint16_t num_elements = (uint16_t)files_array_size(model->files);
  431. if((event->type == InputTypeShort || event->type == InputTypeRepeat)) {
  432. if(event->key == InputKeyUp) {
  433. model->idx = ((model->idx - 1) + num_elements) % num_elements;
  434. } else if(event->key == InputKeyDown) {
  435. model->idx = (model->idx + 1) % num_elements;
  436. }
  437. }
  438. return true;
  439. });
  440. update_offset(archive);
  441. }
  442. if(event->key == InputKeyOk) {
  443. ArchiveFile_t* selected;
  444. with_view_model(
  445. archive->view_archive_main, (ArchiveViewModel * model) {
  446. selected = files_array_size(model->files) > 0 ?
  447. files_array_get(model->files, model->idx) :
  448. NULL;
  449. return true;
  450. });
  451. if(selected) {
  452. string_set(archive->browser.name, selected->name);
  453. if(selected->type == ArchiveFileTypeFolder) {
  454. if(event->type == InputTypeShort) {
  455. archive_enter_dir(archive, archive->browser.name);
  456. } else if(event->type == InputTypeLong) {
  457. archive_show_file_menu(archive);
  458. }
  459. } else {
  460. if(event->type == InputTypeShort) {
  461. archive_show_file_menu(archive);
  462. }
  463. }
  464. }
  465. }
  466. update_offset(archive);
  467. return true;
  468. }
  469. void archive_free(ArchiveApp* archive) {
  470. furi_assert(archive);
  471. view_dispatcher_remove_view(archive->view_dispatcher, ArchiveViewMain);
  472. view_dispatcher_remove_view(archive->view_dispatcher, ArchiveViewTextInput);
  473. view_dispatcher_free(archive->view_dispatcher);
  474. with_view_model(
  475. archive->view_archive_main, (ArchiveViewModel * model) {
  476. files_array_clear(model->files);
  477. return false;
  478. });
  479. view_free(archive->view_archive_main);
  480. string_clear(archive->browser.name);
  481. string_clear(archive->browser.path);
  482. text_input_free(archive->text_input);
  483. furi_record_close("sdcard");
  484. archive->fs_api = NULL;
  485. furi_record_close("gui");
  486. archive->gui = NULL;
  487. furi_record_close("loader");
  488. archive->loader = NULL;
  489. furi_thread_free(archive->app_thread);
  490. furi_check(osMessageQueueDelete(archive->event_queue) == osOK);
  491. free(archive);
  492. }
  493. ArchiveApp* archive_alloc() {
  494. ArchiveApp* archive = furi_alloc(sizeof(ArchiveApp));
  495. archive->event_queue = osMessageQueueNew(8, sizeof(AppEvent), NULL);
  496. archive->app_thread = furi_thread_alloc();
  497. archive->gui = furi_record_open("gui");
  498. archive->loader = furi_record_open("loader");
  499. archive->fs_api = furi_record_open("sdcard");
  500. archive->text_input = text_input_alloc();
  501. archive->view_archive_main = view_alloc();
  502. furi_check(archive->event_queue);
  503. view_allocate_model(
  504. archive->view_archive_main, ViewModelTypeLocking, sizeof(ArchiveViewModel));
  505. with_view_model(
  506. archive->view_archive_main, (ArchiveViewModel * model) {
  507. files_array_init(model->files);
  508. return false;
  509. });
  510. view_set_context(archive->view_archive_main, archive);
  511. view_set_draw_callback(archive->view_archive_main, archive_view_render);
  512. view_set_input_callback(archive->view_archive_main, archive_view_input);
  513. view_set_previous_callback(
  514. text_input_get_view(archive->text_input), archive_previous_callback);
  515. // View Dispatcher
  516. archive->view_dispatcher = view_dispatcher_alloc();
  517. view_dispatcher_add_view(
  518. archive->view_dispatcher, ArchiveViewMain, archive->view_archive_main);
  519. view_dispatcher_add_view(
  520. archive->view_dispatcher, ArchiveViewTextInput, text_input_get_view(archive->text_input));
  521. view_dispatcher_attach_to_gui(
  522. archive->view_dispatcher, archive->gui, ViewDispatcherTypeFullscreen);
  523. view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveTabFavorites);
  524. return archive;
  525. }
  526. int32_t app_archive(void* p) {
  527. ArchiveApp* archive = archive_alloc();
  528. // default tab
  529. archive_switch_tab(archive);
  530. AppEvent event;
  531. while(1) {
  532. furi_check(osMessageQueueGet(archive->event_queue, &event, NULL, osWaitForever) == osOK);
  533. if(event.type == EventTypeExit) {
  534. break;
  535. }
  536. }
  537. archive_free(archive);
  538. return 0;
  539. }