diff --git a/ReadMe.md b/ReadMe.md index c6e29a6be..3ae674140 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -126,7 +126,8 @@ You can support us by using links or addresses below: - **iButton Fuzzer** [(by xMasterX)](https://github.com/xMasterX/ibutton-fuzzer) - HEX Viewer [(by QtRoS)](https://github.com/QtRoS/flipper-zero-hex-viewer) - POCSAG Pager [(by xMasterX & Shmuma)](https://github.com/xMasterX/flipper-pager) -- UART Terminal [(by cool4uma)](https://github.com/cool4uma/UART_Terminal/tree/main) +- Text Viewer [(by kowalski7cc & kyhwana)](https://github.com/kowalski7cc/flipper-zero-text-viewer/tree/refactor-text-app) +- **UART Terminal** [(by cool4uma)](https://github.com/cool4uma/UART_Terminal/tree/main) Games: - DOOM (fixed) [(by p4nic4ttack)](https://github.com/p4nic4ttack/doom-flipper-zero/) diff --git a/applications/plugins/text_viewer/LICENSE b/applications/plugins/text_viewer/LICENSE new file mode 100644 index 000000000..69004dc62 --- /dev/null +++ b/applications/plugins/text_viewer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Roman Shchekin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/applications/plugins/text_viewer/README.md b/applications/plugins/text_viewer/README.md new file mode 100644 index 000000000..cc41931be --- /dev/null +++ b/applications/plugins/text_viewer/README.md @@ -0,0 +1,9 @@ +# flipper-zero-text-viewer + +Text Viewer application for Flipper Zero! + +A fork with a few changes from [QTRoS' hex viewer](https://github.com/QtRoS/flipper-zero-hex-viewer) to just display text without any hex byte representation + +![Text Viewer app!](https://github.com/kyhwana/flipper-zero-hex-viewer/blob/master/textviewerflipper.PNG?raw=true) + +[Link to FAP](https://github.com/kyhwana/latest_flipper_zero_apps/raw/main/text_viewer.fap) diff --git a/applications/plugins/text_viewer/application.fam b/applications/plugins/text_viewer/application.fam new file mode 100644 index 000000000..dcd573c9d --- /dev/null +++ b/applications/plugins/text_viewer/application.fam @@ -0,0 +1,16 @@ +App( + appid="text_viewer", + name="Text Viewer", + apptype=FlipperAppType.EXTERNAL, + entry_point="text_viewer_app", + cdefines=["APP_TEXT_VIEWER"], + requires=[ + "gui", + "dialogs", + ], + stack_size=2 * 1024, + order=20, + fap_icon="icons/text_10px.png", + fap_category="Misc", + fap_icon_assets="icons", +) diff --git a/applications/plugins/text_viewer/icons/text_10px.png b/applications/plugins/text_viewer/icons/text_10px.png new file mode 100644 index 000000000..8e8a6183d Binary files /dev/null and b/applications/plugins/text_viewer/icons/text_10px.png differ diff --git a/applications/plugins/text_viewer/text_viewer.c b/applications/plugins/text_viewer/text_viewer.c new file mode 100644 index 000000000..6c7b46579 --- /dev/null +++ b/applications/plugins/text_viewer/text_viewer.c @@ -0,0 +1,282 @@ +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "TextViewer" + +#define TEXT_VIEWER_APP_PATH_FOLDER ANY_PATH("") +#define TEXT_VIEWER_APP_EXTENSION "*" + +#define TEXT_VIEWER_BYTES_PER_LINE 20u +#define TEXT_VIEWER_LINES_ON_SCREEN 5u +#define TEXT_VIEWER_BUF_SIZE (TEXT_VIEWER_LINES_ON_SCREEN * TEXT_VIEWER_BYTES_PER_LINE) + +typedef struct { + uint8_t file_bytes[TEXT_VIEWER_LINES_ON_SCREEN][TEXT_VIEWER_BYTES_PER_LINE]; + uint32_t file_offset; + uint32_t file_read_bytes; + uint32_t file_size; + Stream* stream; + bool mode; // Print address or content +} TextViewerModel; + +typedef struct { + TextViewerModel* model; + FuriMutex** mutex; + + FuriMessageQueue* input_queue; + + ViewPort* view_port; + Gui* gui; + Storage* storage; +} TextViewer; + +static void render_callback(Canvas* canvas, void* ctx) { + TextViewer* text_viewer = ctx; + furi_check(furi_mutex_acquire(text_viewer->mutex, FuriWaitForever) == FuriStatusOk); + + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + + //elements_button_left(canvas, text_viewer->model->mode ? "Addr" : "Text"); + text_viewer->model->mode = 1; //text mode + //elements_button_right(canvas, "Info"); + + int ROW_HEIGHT = 12; + int TOP_OFFSET = 10; + int LEFT_OFFSET = 3; + + uint32_t line_count = text_viewer->model->file_size / TEXT_VIEWER_BYTES_PER_LINE; + if(text_viewer->model->file_size % TEXT_VIEWER_BYTES_PER_LINE != 0) line_count += 1; + uint32_t first_line_on_screen = text_viewer->model->file_offset / TEXT_VIEWER_BYTES_PER_LINE; + if(line_count > TEXT_VIEWER_LINES_ON_SCREEN) { + uint8_t width = canvas_width(canvas); + elements_scrollbar_pos( + canvas, + width, + 0, + ROW_HEIGHT * TEXT_VIEWER_LINES_ON_SCREEN, + first_line_on_screen, // TODO + line_count - (TEXT_VIEWER_LINES_ON_SCREEN - 1)); + } + + char temp_buf[32]; + uint32_t row_iters = text_viewer->model->file_read_bytes / TEXT_VIEWER_BYTES_PER_LINE; + if(text_viewer->model->file_read_bytes % TEXT_VIEWER_BYTES_PER_LINE != 0) row_iters += 1; + + for(uint32_t i = 0; i < row_iters; ++i) { + uint32_t bytes_left_per_row = + text_viewer->model->file_read_bytes - i * TEXT_VIEWER_BYTES_PER_LINE; + bytes_left_per_row = MIN(bytes_left_per_row, TEXT_VIEWER_BYTES_PER_LINE); + + if(text_viewer->model->mode) { + memcpy(temp_buf, text_viewer->model->file_bytes[i], bytes_left_per_row); + temp_buf[bytes_left_per_row] = '\0'; + for(uint32_t j = 0; j < bytes_left_per_row; ++j) + if(!isprint((int)temp_buf[j])) temp_buf[j] = ' '; + + canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf); + } else { + uint32_t addr = text_viewer->model->file_offset + i * TEXT_VIEWER_BYTES_PER_LINE; + snprintf(temp_buf, 32, "%04lX", addr); + + canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf); + } + } + + furi_mutex_release(text_viewer->mutex); +} + +static void input_callback(InputEvent* input_event, void* ctx) { + TextViewer* text_viewer = ctx; + if(input_event->type == InputTypeShort || input_event->type == InputTypeRepeat) { + furi_message_queue_put(text_viewer->input_queue, input_event, 0); + } +} + +static TextViewer* text_viewer_alloc() { + TextViewer* instance = malloc(sizeof(TextViewer)); + + instance->model = malloc(sizeof(TextViewerModel)); + memset(instance->model, 0x0, sizeof(TextViewerModel)); + + instance->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + + instance->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + instance->view_port = view_port_alloc(); + view_port_draw_callback_set(instance->view_port, render_callback, instance); + view_port_input_callback_set(instance->view_port, input_callback, instance); + + instance->gui = furi_record_open(RECORD_GUI); + gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen); + + instance->storage = furi_record_open(RECORD_STORAGE); + + return instance; +} + +static void text_viewer_free(TextViewer* instance) { + furi_record_close(RECORD_STORAGE); + + gui_remove_view_port(instance->gui, instance->view_port); + furi_record_close(RECORD_GUI); + view_port_free(instance->view_port); + + furi_message_queue_free(instance->input_queue); + + furi_mutex_free(instance->mutex); + + if(instance->model->stream) buffered_file_stream_close(instance->model->stream); + + free(instance->model); + free(instance); +} + +static bool text_viewer_open_file(TextViewer* text_viewer, const char* file_path) { + furi_assert(text_viewer); + furi_assert(file_path); + + text_viewer->model->stream = buffered_file_stream_alloc(text_viewer->storage); + bool isOk = true; + + do { + if(!buffered_file_stream_open( + text_viewer->model->stream, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + FURI_LOG_E(TAG, "Unable to open stream: %s", file_path); + isOk = false; + break; + }; + + text_viewer->model->file_size = stream_size(text_viewer->model->stream); + } while(false); + + return isOk; +} + +static bool text_viewer_read_file(TextViewer* text_viewer) { + furi_assert(text_viewer); + furi_assert(text_viewer->model->stream); + furi_assert(text_viewer->model->file_offset % TEXT_VIEWER_BYTES_PER_LINE == 0); + + memset(text_viewer->model->file_bytes, 0x0, TEXT_VIEWER_BUF_SIZE); + bool isOk = true; + + do { + uint32_t offset = text_viewer->model->file_offset; + if(!stream_seek(text_viewer->model->stream, offset, true)) { + FURI_LOG_E(TAG, "Unable to seek stream"); + isOk = false; + break; + } + + text_viewer->model->file_read_bytes = stream_read( + text_viewer->model->stream, + (uint8_t*)text_viewer->model->file_bytes, + TEXT_VIEWER_BUF_SIZE); + } while(false); + + return isOk; +} + +int32_t text_viewer_app(void* p) { + TextViewer* text_viewer = text_viewer_alloc(); + + FuriString* file_path; + file_path = furi_string_alloc(); + + do { + if(p && strlen(p)) { + furi_string_set(file_path, (const char*)p); + } else { + furi_string_set(file_path, TEXT_VIEWER_APP_PATH_FOLDER); + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options( + &browser_options, TEXT_VIEWER_APP_EXTENSION, &I_text_10px); + browser_options.hide_ext = false; + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options); + + furi_record_close(RECORD_DIALOGS); + if(!res) { + FURI_LOG_I(TAG, "No file selected"); + break; + } + } + + FURI_LOG_I(TAG, "File selected: %s", furi_string_get_cstr(file_path)); + + if(!text_viewer_open_file(text_viewer, furi_string_get_cstr(file_path))) break; + text_viewer_read_file(text_viewer); + + InputEvent input; + while(furi_message_queue_get(text_viewer->input_queue, &input, FuriWaitForever) == + FuriStatusOk) { + if(input.key == InputKeyBack) { + break; + } else if(input.key == InputKeyUp) { + furi_check(furi_mutex_acquire(text_viewer->mutex, FuriWaitForever) == FuriStatusOk); + if(text_viewer->model->file_offset > 0) { + text_viewer->model->file_offset -= TEXT_VIEWER_BYTES_PER_LINE; + if(!text_viewer_read_file(text_viewer)) break; + } + furi_mutex_release(text_viewer->mutex); + } else if(input.key == InputKeyDown) { + furi_check(furi_mutex_acquire(text_viewer->mutex, FuriWaitForever) == FuriStatusOk); + uint32_t last_byte_on_screen = + text_viewer->model->file_offset + text_viewer->model->file_read_bytes; + + if(text_viewer->model->file_size > last_byte_on_screen) { + text_viewer->model->file_offset += TEXT_VIEWER_BYTES_PER_LINE; + if(!text_viewer_read_file(text_viewer)) break; + } + furi_mutex_release(text_viewer->mutex); + } else if(input.key == InputKeyLeft) { + furi_check(furi_mutex_acquire(text_viewer->mutex, FuriWaitForever) == FuriStatusOk); + text_viewer->model->mode = !text_viewer->model->mode; + furi_mutex_release(text_viewer->mutex); + } else if(input.key == InputKeyRight) { + FuriString* buffer; + buffer = furi_string_alloc(); + furi_string_printf( + buffer, + "File path: %s\nFile size: %lu (0x%lX)", + furi_string_get_cstr(file_path), + text_viewer->model->file_size, + text_viewer->model->file_size); + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header(message, "Text Viewer v1.1", 16, 2, AlignLeft, AlignTop); + dialog_message_set_icon(message, &I_text_10px, 3, 2); + dialog_message_set_text( + message, furi_string_get_cstr(buffer), 3, 16, AlignLeft, AlignTop); + dialog_message_set_buttons(message, NULL, NULL, "Back"); + dialog_message_show(dialogs, message); + + furi_string_free(buffer); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); + } + view_port_update(text_viewer->view_port); + } + } while(false); + + furi_string_free(file_path); + text_viewer_free(text_viewer); + + return 0; +} \ No newline at end of file diff --git a/applications/plugins/text_viewer/textviewerflipper.PNG b/applications/plugins/text_viewer/textviewerflipper.PNG new file mode 100644 index 000000000..d357b7455 Binary files /dev/null and b/applications/plugins/text_viewer/textviewerflipper.PNG differ