archive.c 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. #include "archive_i.h"
  2. static bool archive_get_filenames(ArchiveApp* archive);
  3. static bool is_favourite(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_set(path, "favourites/");
  9. string_cat(path, file->name);
  10. fr = common_api->info(string_get_cstr(path), &file_info, NULL, 0);
  11. FURI_LOG_I("FAV", "%d", fr);
  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_favourites(ArchiveApp* archive) {
  179. furi_assert(archive);
  180. FS_Common_Api* common_api = &archive->fs_api->common;
  181. common_api->mkdir("favourites");
  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_set(buffer_src, archive->browser.path);
  192. string_cat(buffer_src, "/");
  193. string_cat(buffer_src, archive->browser.name);
  194. string_init_set_str(buffer_dst, "/favourites/");
  195. string_cat(buffer_dst, archive->browser.name);
  196. fr = file_api->open(&src, string_get_cstr(buffer_src), FSAM_READ, FSOM_OPEN_EXISTING);
  197. FURI_LOG_I("FATFS", "OPEN: %d", fr);
  198. fr = file_api->open(&dst, string_get_cstr(buffer_dst), FSAM_WRITE, FSOM_CREATE_ALWAYS);
  199. FURI_LOG_I("FATFS", "CREATE: %d", fr);
  200. for(;;) {
  201. br = file_api->read(&src, &buffer, sizeof(buffer));
  202. if(br == 0) break;
  203. bw = file_api->write(&dst, &buffer, sizeof(buffer));
  204. if(bw < br) break;
  205. }
  206. file_api->close(&src);
  207. file_api->close(&dst);
  208. string_clear(buffer_src);
  209. string_clear(buffer_dst);
  210. }
  211. static void archive_text_input_callback(void* context) {
  212. furi_assert(context);
  213. ArchiveApp* archive = (ArchiveApp*)context;
  214. FS_Common_Api* common_api = &archive->fs_api->common;
  215. string_t buffer_src;
  216. string_t buffer_dst;
  217. string_init_set(buffer_src, archive->browser.path);
  218. string_init_set(buffer_dst, archive->browser.path);
  219. string_cat(buffer_src, "/");
  220. string_cat(buffer_dst, "/");
  221. string_cat(buffer_src, archive->browser.name);
  222. string_cat_str(buffer_dst, archive->browser.text_input_buffer);
  223. // append extension
  224. ArchiveFile_t* file;
  225. with_view_model(
  226. archive->view_archive_main, (ArchiveViewModel * model) {
  227. file = files_array_get(
  228. model->files, CLAMP(model->idx, files_array_size(model->files) - 1, 0));
  229. return true;
  230. });
  231. string_cat(buffer_dst, known_ext[file->type]);
  232. common_api->rename(string_get_cstr(buffer_src), string_get_cstr(buffer_dst));
  233. view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewMain);
  234. string_clear(buffer_src);
  235. string_clear(buffer_dst);
  236. archive_get_filenames(archive);
  237. }
  238. static void archive_enter_text_input(ArchiveApp* archive) {
  239. furi_assert(archive);
  240. *archive->browser.text_input_buffer = '\0';
  241. strlcpy(
  242. archive->browser.text_input_buffer,
  243. string_get_cstr(archive->browser.name),
  244. string_size(archive->browser.name));
  245. archive_trim_file_ext(archive->browser.text_input_buffer);
  246. text_input_set_header_text(archive->text_input, "Rename:");
  247. text_input_set_result_callback(
  248. archive->text_input,
  249. archive_text_input_callback,
  250. archive,
  251. archive->browser.text_input_buffer,
  252. MAX_NAME_LEN);
  253. view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveViewTextInput);
  254. }
  255. static void archive_show_file_menu(ArchiveApp* archive) {
  256. furi_assert(archive);
  257. archive->browser.menu = true;
  258. with_view_model(
  259. archive->view_archive_main, (ArchiveViewModel * model) {
  260. ArchiveFile_t* selected;
  261. selected = files_array_get(model->files, model->idx);
  262. model->menu = true;
  263. model->menu_idx = 0;
  264. selected->fav = is_favourite(archive, selected);
  265. return true;
  266. });
  267. }
  268. static void archive_close_file_menu(ArchiveApp* archive) {
  269. furi_assert(archive);
  270. archive->browser.menu = false;
  271. with_view_model(
  272. archive->view_archive_main, (ArchiveViewModel * model) {
  273. model->menu = false;
  274. model->menu_idx = 0;
  275. return true;
  276. });
  277. }
  278. static void archive_open_app(ArchiveApp* archive, const char* app_name, const char* args) {
  279. furi_assert(archive);
  280. furi_assert(app_name);
  281. app_loader_start(app_name, args);
  282. }
  283. static void archive_delete_file(ArchiveApp* archive, ArchiveFile_t* file, bool fav, bool orig) {
  284. furi_assert(archive);
  285. furi_assert(file);
  286. FS_Common_Api* common_api = &archive->fs_api->common;
  287. string_t path;
  288. string_init(path);
  289. if(!fav && !orig) {
  290. string_set(path, archive->browser.path);
  291. string_cat(path, "/");
  292. string_cat(path, file->name);
  293. common_api->remove(string_get_cstr(path));
  294. } else { // remove from favorites
  295. string_set(path, "favourites/");
  296. string_cat(path, file->name);
  297. common_api->remove(string_get_cstr(path));
  298. if(orig) { // remove original file
  299. string_set_str(path, get_default_path(file->type));
  300. string_cat(path, "/");
  301. string_cat(path, 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_set(full_path, archive->browser.path);
  329. string_cat(full_path, "/");
  330. string_cat(full_path, selected->name);
  331. archive_open_app(
  332. archive, flipper_app_name[selected->type], string_get_cstr(full_path));
  333. string_clear(full_path);
  334. }
  335. break;
  336. case 1:
  337. if(is_known_app(selected->type)) {
  338. if(!is_favourite(archive, selected)) {
  339. string_set(archive->browser.name, selected->name);
  340. archive_add_to_favourites(archive);
  341. } else {
  342. // delete from favourites
  343. archive_delete_file(archive, selected, true, false);
  344. }
  345. archive_close_file_menu(archive);
  346. }
  347. break;
  348. case 2:
  349. // open rename view
  350. if(is_known_app(selected->type)) {
  351. archive_enter_text_input(archive);
  352. }
  353. break;
  354. case 3:
  355. // confirmation?
  356. if(is_favourite(archive, selected)) {
  357. //delete both fav & original
  358. archive_delete_file(archive, selected, true, true);
  359. } else {
  360. archive_delete_file(archive, selected, false, false);
  361. }
  362. archive_close_file_menu(archive);
  363. break;
  364. default:
  365. archive_close_file_menu(archive);
  366. break;
  367. }
  368. selected = NULL;
  369. }
  370. static void menu_input_handler(ArchiveApp* archive, InputEvent* event) {
  371. furi_assert(archive);
  372. furi_assert(archive);
  373. if(event->type == InputTypeShort) {
  374. if(event->key == InputKeyUp || event->key == InputKeyDown) {
  375. with_view_model(
  376. archive->view_archive_main, (ArchiveViewModel * model) {
  377. if(event->key == InputKeyUp) {
  378. model->menu_idx = ((model->menu_idx - 1) + MENU_ITEMS) % MENU_ITEMS;
  379. } else if(event->key == InputKeyDown) {
  380. model->menu_idx = (model->menu_idx + 1) % MENU_ITEMS;
  381. }
  382. return true;
  383. });
  384. }
  385. if(event->key == InputKeyOk) {
  386. archive_file_menu_callback(archive);
  387. } else if(event->key == InputKeyBack) {
  388. archive_close_file_menu(archive);
  389. }
  390. }
  391. }
  392. /* main controls */
  393. static bool archive_view_input(InputEvent* event, void* context) {
  394. furi_assert(event);
  395. furi_assert(context);
  396. ArchiveApp* archive = context;
  397. bool in_menu = archive->browser.menu;
  398. if(in_menu) {
  399. menu_input_handler(archive, event);
  400. return true;
  401. }
  402. if(event->type == InputTypeShort) {
  403. if(event->key == InputKeyLeft) {
  404. if(archive->browser.tab_id > 0) {
  405. archive->browser.tab_id = CLAMP(archive->browser.tab_id - 1, ArchiveTabTotal, 0);
  406. archive_switch_tab(archive);
  407. return true;
  408. }
  409. } else if(event->key == InputKeyRight) {
  410. if(archive->browser.tab_id < ArchiveTabTotal - 1) {
  411. archive->browser.tab_id =
  412. CLAMP(archive->browser.tab_id + 1, ArchiveTabTotal - 1, 0);
  413. archive_switch_tab(archive);
  414. return true;
  415. }
  416. } else if(event->key == InputKeyBack) {
  417. if(archive->browser.depth == 0) {
  418. archive_exit_callback(archive);
  419. } else {
  420. archive_leave_dir(archive);
  421. }
  422. return true;
  423. }
  424. }
  425. if(event->key == InputKeyUp || event->key == InputKeyDown) {
  426. with_view_model(
  427. archive->view_archive_main, (ArchiveViewModel * model) {
  428. uint16_t num_elements = (uint16_t)files_array_size(model->files);
  429. if((event->type == InputTypeShort || event->type == InputTypeRepeat)) {
  430. if(event->key == InputKeyUp) {
  431. model->idx = ((model->idx - 1) + num_elements) % num_elements;
  432. } else if(event->key == InputKeyDown) {
  433. model->idx = (model->idx + 1) % num_elements;
  434. }
  435. }
  436. return true;
  437. });
  438. update_offset(archive);
  439. }
  440. if(event->key == InputKeyOk) {
  441. ArchiveFile_t* selected;
  442. with_view_model(
  443. archive->view_archive_main, (ArchiveViewModel * model) {
  444. selected = files_array_size(model->files) > 0 ?
  445. files_array_get(model->files, model->idx) :
  446. NULL;
  447. return true;
  448. });
  449. if(selected) {
  450. string_set(archive->browser.name, selected->name);
  451. if(selected->type == ArchiveFileTypeFolder) {
  452. if(event->type == InputTypeShort) {
  453. archive_enter_dir(archive, archive->browser.name);
  454. } else if(event->type == InputTypeLong) {
  455. archive_show_file_menu(archive);
  456. }
  457. } else {
  458. if(event->type == InputTypeShort) {
  459. archive_show_file_menu(archive);
  460. }
  461. }
  462. }
  463. }
  464. update_offset(archive);
  465. return true;
  466. }
  467. void archive_free(ArchiveApp* archive) {
  468. furi_assert(archive);
  469. view_dispatcher_remove_view(archive->view_dispatcher, ArchiveViewMain);
  470. view_dispatcher_remove_view(archive->view_dispatcher, ArchiveViewTextInput);
  471. view_dispatcher_free(archive->view_dispatcher);
  472. with_view_model(
  473. archive->view_archive_main, (ArchiveViewModel * model) {
  474. files_array_clear(model->files);
  475. return false;
  476. });
  477. view_free(archive->view_archive_main);
  478. string_clear(archive->browser.name);
  479. string_clear(archive->browser.path);
  480. text_input_free(archive->text_input);
  481. furi_record_close("sdcard");
  482. archive->fs_api = NULL;
  483. furi_record_close("gui");
  484. archive->gui = NULL;
  485. furi_thread_free(archive->app_thread);
  486. furi_check(osMessageQueueDelete(archive->event_queue) == osOK);
  487. free(archive);
  488. }
  489. ArchiveApp* archive_alloc() {
  490. ArchiveApp* archive = furi_alloc(sizeof(ArchiveApp));
  491. archive->event_queue = osMessageQueueNew(8, sizeof(AppEvent), NULL);
  492. archive->app_thread = furi_thread_alloc();
  493. archive->gui = furi_record_open("gui");
  494. archive->fs_api = furi_record_open("sdcard");
  495. archive->text_input = text_input_alloc();
  496. archive->view_archive_main = view_alloc();
  497. furi_check(archive->event_queue);
  498. view_allocate_model(
  499. archive->view_archive_main, ViewModelTypeLocking, sizeof(ArchiveViewModel));
  500. with_view_model(
  501. archive->view_archive_main, (ArchiveViewModel * model) {
  502. files_array_init(model->files);
  503. return false;
  504. });
  505. view_set_context(archive->view_archive_main, archive);
  506. view_set_draw_callback(archive->view_archive_main, archive_view_render);
  507. view_set_input_callback(archive->view_archive_main, archive_view_input);
  508. view_set_previous_callback(
  509. text_input_get_view(archive->text_input), archive_previous_callback);
  510. // View Dispatcher
  511. archive->view_dispatcher = view_dispatcher_alloc();
  512. view_dispatcher_add_view(
  513. archive->view_dispatcher, ArchiveViewMain, archive->view_archive_main);
  514. view_dispatcher_add_view(
  515. archive->view_dispatcher, ArchiveViewTextInput, text_input_get_view(archive->text_input));
  516. view_dispatcher_attach_to_gui(
  517. archive->view_dispatcher, archive->gui, ViewDispatcherTypeFullscreen);
  518. view_dispatcher_switch_to_view(archive->view_dispatcher, ArchiveTabFavourites);
  519. return archive;
  520. }
  521. int32_t app_archive(void* p) {
  522. ArchiveApp* archive = archive_alloc();
  523. // default tab
  524. archive_switch_tab(archive);
  525. AppEvent event;
  526. while(1) {
  527. furi_check(osMessageQueueGet(archive->event_queue, &event, NULL, osWaitForever) == osOK);
  528. if(event.type == EventTypeExit) {
  529. break;
  530. }
  531. }
  532. archive_free(archive);
  533. return 0;
  534. }