Files
fdskey/FdsKey/Core/Src/browser.c
2023-08-07 16:21:39 +04:00

544 lines
15 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <stdlib.h>
#include <string.h>
#include "main.h"
#include "browser.h"
#include "ff.h"
#include "oled.h"
#include "buttons.h"
#include "splash.h"
#include "fdsemu.h"
#include "settings.h"
static DYN_FILINFO** dir_list = 0;
static DYN_FILINFO** file_list = 0;
static int dir_count = 0;
static int file_count = 0;
static void browser_free();
#ifdef BROWSER_USE_RUSSIAN
// codepage conversions for russian characters
// FAT uses cp866 but my fonts use cp1251
static void cp866to1251(char *text)
{
while (*text)
{
if (*text >= 0x80 && *text <= 0xAF) // А-Я, а-п
*text += 0x40;
else if (*text >= 0xE0 && *text <= 0xEF) // р
*text += 0x10;
else if (*text == 0xF0) // Ё
*text = 0xA8;
else if (*text == 0xF1) // ё
*text = 0xB8;
text++;
}
}
static void cp1251to866(char *text)
{
while (*text)
{
if (*text >= 0xC0 && *text <= 0xEF) // А-Я, а-п
*text -= 0x40;
else if (*text >= 0xF0 && *text <= 0xFF) // р
*text -= 0x10;
else if (*text == 0xA8) // Ё
*text = 0xF0;
else if (*text == 0xB8) // ё
*text = 0xF1;
text++;
}
}
#endif
// compare filenames but without extension(s)
static int strcasecmp_no_extension(char *a, char *b)
{
// TODO: optimize?
int i;
char a_no_ext[FF_MAX_LFN + 1];
char b_no_ext[FF_MAX_LFN + 1];
strcpy(a_no_ext, a);
strcpy(b_no_ext, b);
char *a_dot = strstr(a_no_ext, ".");
char *b_dot = strstr(b_no_ext, ".");
if (a_dot) *a_dot = 0;
if (b_dot) *b_dot = 0;
i = strcasecmp(a_no_ext, b_no_ext);
if (i) return i;
return strcasecmp(a, b);
}
// Merge sort
// left source half is A[iBegin:iMiddle-1]
// right source half is A[iMiddle:iEnd-1]
// result is B[iBegin:iEnd-1]
static void top_down_merge(DYN_FILINFO** A, int iBegin, int iMiddle, int iEnd, DYN_FILINFO** B)
{
int i = iBegin, j = iMiddle, k;
// While there are elements in the left or right runs...
for (k = iBegin; k < iEnd; k++) {
// If left run head exists and is <= existing right run head.
if (i < iMiddle && (j >= iEnd || strcasecmp_no_extension(A[i]->filename, A[j]->filename) <= 0)) {
B[k] = A[i];
i = i + 1;
} else {
B[k] = A[j];
j = j + 1;
}
}
}
// split A[] into 2 runs, sort both runs into B[], merge both runs from B[] to A[]
// iBegin is inclusive; iEnd is exclusive (A[iEnd] is not in the set).
static void top_down_split_merge(DYN_FILINFO** B, int iBegin, int iEnd, DYN_FILINFO** A)
{
if (iEnd - iBegin <= 1) // if run size == 1
return; // consider it sorted
// split the run longer than 1 item into halves
int iMiddle = (iEnd + iBegin) / 2; // iMiddle = mid point
// recursively sort both runs from array A[] into B[]
top_down_split_merge(A, iBegin, iMiddle, B); // sort the left run
top_down_split_merge(A, iMiddle, iEnd, B); // sort the right run
// merge the resulting runs from array B[] into A[]
top_down_merge(B, iBegin, iMiddle, iEnd, A);
}
// start sort
static void top_down_merge_sort(DYN_FILINFO** A, int n)
{
DYN_FILINFO* B[n];
memcpy(B, A, sizeof(DYN_FILINFO*) * n);
top_down_split_merge(B, 0, n, A); // sort data from B[] into A[]
}
// draw single menu item
static void browser_draw_item(uint8_t line, int item, uint8_t is_selected, int text_scroll)
{
uint8_t is_dir;
int i, offset, max_width, text_width, total_scroll;
char* text;
if (item < dir_count)
text = dir_list[item]->filename;
else if (item < dir_count + file_count)
{
text = file_list[item - dir_count]->filename;
// hide extension if enabled and .fds file
if (fdskey_settings.hide_extensions && !strcasecmp(text + strlen(text) - 4, ".fds"))
{
char trimmed[FF_MAX_LFN + 1];
strlcpy(trimmed, text, sizeof(trimmed));
text = trimmed;
for (i = strlen(text) - 1; i >= 0; i--)
{
if (text[i] == '.')
{
text[i] = 0;
break;
}
}
}
}
else
text = "";
#ifdef BROWSER_USE_RUSSIAN
// temporary convert to cp1251
cp866to1251(text);
#endif
is_dir = item < dir_count;
oled_draw_rectangle(0, line * 8, OLED_WIDTH - 1, line * 8 + 7, 1, 0);
if (is_selected)
oled_draw_image(&IMAGE_CURSOR, 0, line * 8, 0, 0);
offset = IMAGE_CURSOR.width + (is_dir ? BROWSER_FOLDER_IMAGE.width + 1: 0);
max_width = OLED_WIDTH - offset - 1;
if (is_dir)
oled_draw_image(&BROWSER_FOLDER_IMAGE, IMAGE_CURSOR.width, line * 8, 0, 0);
text_width = oled_get_text_length(&BROWSER_FONT, text) - 1 /*spacing*/;
if (text_width > max_width)
{
total_scroll = BROWSER_HORIZONTAL_SCROLL_PAUSE + (text_width - max_width) + BROWSER_HORIZONTAL_SCROLL_PAUSE;
text_scroll /= BROWSER_HORIZONTAL_SCROLL_SPEED;
text_scroll %= total_scroll * 2;
// two-directional
if (text_scroll > total_scroll)
text_scroll = total_scroll * 2 - text_scroll;
if (text_scroll < BROWSER_HORIZONTAL_SCROLL_PAUSE)
text_scroll = 0;
else if (text_scroll >= BROWSER_HORIZONTAL_SCROLL_PAUSE + (text_width - max_width))
text_scroll = text_width - max_width;
else
text_scroll = text_scroll - BROWSER_HORIZONTAL_SCROLL_PAUSE;
} else {
text_scroll = 0;
}
oled_draw_text_cropped(&BROWSER_FONT, text,
offset, line * 8,
text_scroll, max_width,
0, 0,
0, 0);
oled_update(line, line);
#ifdef BROWSER_USE_RUSSIAN
// convert back
cp1251to866(text);
#endif
}
// show single directory menu
// selection - start selection
// is_selected - non-zero value written if right button pressed
// returns selected item id
static int browser_menu(int selection, uint8_t *is_selected)
{
int i;
int text_scroll = 0;
int item_count = dir_count + file_count;
int line = selection - 2;
if (line + 4 > item_count) line = item_count - 4;
if (line < 0) line = 0;
button_set_up_down_repeat_interval(1);
for (i = 0; i < 4; i++)
{
browser_draw_item((oled_get_line() + OLED_HEIGHT) / 8 + i, line + i, line + i == selection, 0);
}
oled_switch_to_invisible();
// oled_screenshot("ss_browser.bmp");
while (1)
{
if (button_up_newpress() && selection > 0)
{
browser_draw_item(oled_get_line() / 8 + selection - line, selection, 0, 0);
selection--;
browser_draw_item(oled_get_line() / 8 + selection - line, selection, 1, 0);
text_scroll = 0;
}
if (button_down_newpress() && selection + 1 < item_count)
{
browser_draw_item(oled_get_line() / 8 + selection - line, selection, 0, 0);
selection++;
browser_draw_item(oled_get_line() / 8 + selection - line, selection, 1, 0);
text_scroll = 0;
}
if (button_left_newpress()) {
*is_selected = 0;
button_set_up_down_repeat_interval(BUTTONS_DEFAULT_UP_DOWN_REPEAT_INTERVAL);
return selection; // back
}
if (button_right_newpress()) {
*is_selected = 1;
button_set_up_down_repeat_interval(BUTTONS_DEFAULT_UP_DOWN_REPEAT_INTERVAL);
return selection;
}
while (selection < line && line)
{
line--;
for (i = 0; i < 8; i++) {
oled_set_line(oled_get_line() - 1);
HAL_Delay(5);
}
}
while (selection > line + 3)
{
line++;
for (i = 0; i < 8; i++) {
oled_set_line(oled_get_line() + 1);
HAL_Delay(5);
}
}
browser_draw_item(oled_get_line() / 8 + selection - line, selection, 1, text_scroll);
text_scroll++;
button_check_screen_off();
HAL_Delay(1);
}
}
// load single directory
// path - directory
// output - output file/directory info
// result - output result
// select - default item to select
FRESULT browser(char *path, FILINFO *output, BROWSER_RESULT *result, char *select)
{
FRESULT fr;
DIR dir;
FILINFO fno;
int mem_dir_count = 512;
int mem_file_count = 512;
int i, r, selection;
uint8_t is_selected = 0;
dir_count = 0;
file_count = 0;
show_loading_screen();
dir_list = malloc(mem_dir_count * sizeof(char*));
if (!dir_list)
return FDSR_OUT_OF_MEMORY;
file_list = malloc(mem_file_count * sizeof(char*));
if (!file_list)
return FDSR_OUT_OF_MEMORY;
// load file and directory names
fr = f_opendir(&dir, path);
if (fr != FR_OK) {
browser_free();
return fr;
}
while (1)
{
fr = f_readdir(&dir, &fno);
if (fr != FR_OK)
{
browser_free();
return fr;
}
if (!fno.fname[0])
break;
if (!fdskey_settings.hide_hidden || (!(fno.fattrib & AM_HID) && strcmp(fno.fname, "EDN8")))
{
if (fno.fattrib & AM_DIR)
{
// directory
if (dir_count + 1 > mem_dir_count)
{
// reallocate memory for directories list
mem_dir_count *= 2;
dir_list = realloc(dir_list, mem_dir_count * sizeof(char*));
if (!dir_list)
return FDSR_OUT_OF_MEMORY;
}
// allocate memory for new directory entry
dir_list[dir_count] = malloc(sizeof(DYN_FILINFO));
if (!dir_list[dir_count]) return FDSR_OUT_OF_MEMORY;
dir_list[dir_count]->filename = malloc(strlen(fno.fname) + 1);
if (!dir_list[dir_count]->filename) return FDSR_OUT_OF_MEMORY;
strcpy(dir_list[dir_count]->filename, fno.fname);
dir_count++;
} else {
if (fdskey_settings.hide_non_fds)
{
if (strcasecmp(fno.fname + strlen(fno.fname) - 4, ".fds") != 0)
continue;
}
if (file_count + 1 > mem_file_count)
{
// reallocate memory for file list
mem_file_count *= 2;
file_list = realloc(file_list, mem_file_count * sizeof(char*));
if (!file_list)
return FDSR_OUT_OF_MEMORY;
}
// allocate memory for file entry
file_list[file_count] = malloc(sizeof(DYN_FILINFO));
if (!file_list[file_count]) return FDSR_OUT_OF_MEMORY;
file_list[file_count]->filename = malloc(strlen(fno.fname) + 1);
if (!file_list[file_count]->filename) return FDSR_OUT_OF_MEMORY;
strcpy(file_list[file_count]->filename, fno.fname);
file_list[file_count]->fsize = fno.fsize;
file_list[file_count]->fattrib = fno.fattrib;
file_count++;
}
}
}
// show_free_memory();
f_closedir(&dir);
// free some memory (do i readlly need it?)
dir_list = realloc(dir_list, dir_count * sizeof(char*));
if (dir_count && !dir_list) return FDSR_OUT_OF_MEMORY;
file_list = realloc(file_list, file_count * sizeof(char*));
if (file_count && !file_list) return FDSR_OUT_OF_MEMORY;
if (!dir_count && !file_count)
{
// nothing to show
browser_free();
show_message("The directory is empty", 1);
*result = BROWSER_BACK;
return FR_OK;
}
// sort them
top_down_merge_sort(dir_list, dir_count);
top_down_merge_sort(file_list, file_count);
selection = 0;
if (select && select[0])
{
// find default selection entry
for (i = 0; i < dir_count + file_count; i++)
{
char* name = i < dir_count ? dir_list[i]->filename : file_list[i - dir_count]->filename;
if (strcmp(name, select) == 0)
{
selection = i;
break;
}
}
}
r = browser_menu(selection, &is_selected);
if (r < dir_count)
{
*result = BROWSER_DIRECTORY;
strlcpy(output->fname, dir_list[r]->filename, sizeof(output->fname));
output->fname[sizeof(output->fname) - 1] = 0;
} else {
*result = BROWSER_FILE;
while (button_right_holding())
{
if (button_right_hold_time() >= BROWSER_LONGPRESS_TIME)
{
*result = BROWSER_FILE_LONGPRESS;
break;
}
}
strlcpy(output->fname, file_list[r - dir_count]->filename, sizeof(output->fname));
output->fname[sizeof(output->fname) - 1] = 0;
output->fsize = file_list[r - dir_count]->fsize;
output->fattrib = file_list[r - dir_count]->fattrib;
}
if (!is_selected)
{
*result = BROWSER_BACK;
while (button_left_holding())
{
if (button_left_hold_time() >= BROWSER_LONGPRESS_TIME)
{
*result = BROWSER_BACK_LONGPRESS;
break;
}
}
}
browser_free();
return FR_OK;
}
// state file tree browser
// directory - start directory and new directory path output
// dir_max_len - size of "directory"
// fno - start file and selected file output
// returns output with selection result
BROWSER_RESULT browser_tree(char *directory, int dir_max_len, FILINFO *fno)
{
BROWSER_RESULT br;
FRESULT fr;
int i;
// save state if need
if (fdskey_settings.remember_last_state_mode != REMEMBER_LAST_STATE_NONE
&& fdskey_settings.last_state != LAST_STATE_BROWSER)
{
fdskey_settings.last_state = LAST_STATE_BROWSER;
settings_save();
}
while (1)
{
fr = browser(directory, fno, &br, fno->fname);
if (fr == FR_NO_PATH) // directory not exists (anymore?)
{
// repeat from root
directory[0] = 0;
fr = browser(directory, fno, &br, fno->fname);
}
if (fr != FR_OK)
{
// reset last state to main menu and show fatal error
fdskey_settings.last_state = LAST_STATE_MAIN_MENU;
settings_save();
show_error_screen_fr(fr, 1);
}
// save state if need
if (fdskey_settings.remember_last_state_mode != REMEMBER_LAST_STATE_NONE)
{
strcpy(fdskey_settings.last_file, fno->fname);
fdskey_settings.last_state = LAST_STATE_BROWSER;
settings_save();
}
switch (br)
{
case BROWSER_BACK:
// "back" button pressed
if (!*directory)
{
// we are at the root already, return
return br;
}
for (i = strlen(directory) - 2; i >= 0; i--)
{
// return to the previous directory
if (i <= 0)
{
// root
strlcpy(fno->fname, directory + (*directory == '\\' ? 1 : 0), sizeof(fno->fname));
directory[0] = 0;
}
if (directory[i] == '\\')
{
// extract parent directory and current directory names
directory[i] = 0;
strlcpy(fno->fname, &directory[i + 1], sizeof(fno->fname));
break;
}
}
break;
case BROWSER_DIRECTORY:
// directory selected
strlcat(directory, "\\", dir_max_len);
strlcat(directory, fno->fname, dir_max_len);
fno->fname[0] = 0;
if (strlen(directory) >= dir_max_len - 1)
{
show_error_screen("Path is too long", 0);
// return to the root
directory[0] = 0;
}
break;
case BROWSER_FILE:
// file selected
case BROWSER_FILE_LONGPRESS:
// file selected using button longpress
case BROWSER_BACK_LONGPRESS:
// back longpress - return to the main menu
return br;
}
}
}
// free allocated memory
void browser_free()
{
int i;
for (i = 0; i < dir_count; i++)
{
free(dir_list[i]->filename);
free(dir_list[i]);
}for (i = 0; i < file_count; i++)
{
free(file_list[i]->filename);
free(file_list[i]);
}
if (dir_list)
free(dir_list);
if (file_list)
free(file_list);
file_list = 0;
dir_list = 0;
dir_count = 0;
file_count = 0;
}