Преглед на файлове

Add hex_viewer from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: hex_viewer
git-subtree-mainline: f82ff3c1a3344c5c662a6f5fd23b739a3eb66f26
git-subtree-split: 536e07fddaaa7a99505ab8f5ad472e2b2111ef78
Willy-JL преди 1 година
родител
ревизия
ae3f0e3668
променени са 31 файла, в които са добавени 1492 реда и са изтрити 0 реда
  1. 52 0
      hex_viewer/.gitignore
  2. 1 0
      hex_viewer/.gitsubtree
  3. 21 0
      hex_viewer/LICENSE
  4. 16 0
      hex_viewer/README.md
  5. 17 0
      hex_viewer/application.fam
  6. 62 0
      hex_viewer/helpers/hex_viewer_custom_event.h
  7. 36 0
      hex_viewer/helpers/hex_viewer_haptic.c
  8. 8 0
      hex_viewer/helpers/hex_viewer_haptic.h
  9. 39 0
      hex_viewer/helpers/hex_viewer_led.c
  10. 6 0
      hex_viewer/helpers/hex_viewer_led.h
  11. 27 0
      hex_viewer/helpers/hex_viewer_speaker.c
  12. 4 0
      hex_viewer/helpers/hex_viewer_speaker.h
  13. 173 0
      hex_viewer/helpers/hex_viewer_storage.c
  14. 23 0
      hex_viewer/helpers/hex_viewer_storage.h
  15. 143 0
      hex_viewer/hex_viewer.c
  16. 92 0
      hex_viewer/hex_viewer.h
  17. BIN
      hex_viewer/icons/hex_10px.bmp
  18. BIN
      hex_viewer/icons/hex_10px.png
  19. BIN
      hex_viewer/img/1.png
  20. BIN
      hex_viewer/img/2.png
  21. 30 0
      hex_viewer/scenes/hex_viewer_scene.c
  22. 29 0
      hex_viewer/scenes/hex_viewer_scene.h
  23. 6 0
      hex_viewer/scenes/hex_viewer_scene_config.h
  24. 42 0
      hex_viewer/scenes/hex_viewer_scene_info.c
  25. 83 0
      hex_viewer/scenes/hex_viewer_scene_menu.c
  26. 42 0
      hex_viewer/scenes/hex_viewer_scene_open.c
  27. 61 0
      hex_viewer/scenes/hex_viewer_scene_scroll.c
  28. 147 0
      hex_viewer/scenes/hex_viewer_scene_settings.c
  29. 65 0
      hex_viewer/scenes/hex_viewer_scene_startscreen.c
  30. 248 0
      hex_viewer/views/hex_viewer_startscreen.c
  31. 19 0
      hex_viewer/views/hex_viewer_startscreen.h

+ 52 - 0
hex_viewer/.gitignore

@@ -0,0 +1,52 @@
+# Prerequisites
+*.d
+
+# Object files
+*.o
+*.ko
+*.obj
+*.elf
+
+# Linker output
+*.ilk
+*.map
+*.exp
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
+# Debug files
+*.dSYM/
+*.su
+*.idb
+*.pdb
+
+# Kernel Module Compile Results
+*.mod*
+*.cmd
+.tmp_versions/
+modules.order
+Module.symvers
+Mkfile.old
+dkms.conf

+ 1 - 0
hex_viewer/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev base_pack/hex_viewer

+ 21 - 0
hex_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.

+ 16 - 0
hex_viewer/README.md

@@ -0,0 +1,16 @@
+# #️⃣ Hex Viewer
+
+Hex Viewer application for Flipper Zero!  
+The app allows you to view various files as HEX
+
+**Some facts**:
+- Written with pure C in a very simple and effective manner
+- Tested on files up to 16Mb
+- Very effective: calls `canvas_draw_str` 8 times during repaint and that's almost it
+- Can also view text representation of bytes (makes it kinda poor man's text viewer)
+- Has "Scroll to ..." feature which allows you to jump to any percent of file
+
+Feel free to send PRs! 
+
+App URL: https://lab.flipper.net/apps/hex_viewer
+Catalog's manifest: [flipper-application-catalog/applications/Tools/hex_viewer/manifest.yml](https://github.com/flipperdevices/flipper-application-catalog/blob/main/applications/Tools/hex_viewer/manifest.yml)

+ 17 - 0
hex_viewer/application.fam

@@ -0,0 +1,17 @@
+App(
+    appid="hex_viewer",
+    name="HEX Viewer",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="hex_viewer_app",
+    requires=[
+        "gui",
+        "dialogs",
+    ],
+    stack_size=2 * 1024,
+    fap_icon="icons/hex_10px.bmp",
+    fap_icon_assets="icons",
+    fap_category="Tools",
+    fap_author="@QtRoS",
+    fap_version="2.0",
+    fap_description="App allows to view various files as HEX",
+)

+ 62 - 0
hex_viewer/helpers/hex_viewer_custom_event.h

@@ -0,0 +1,62 @@
+#pragma once
+
+typedef enum {
+    HexViewerCustomEventStartscreenUp,
+    HexViewerCustomEventStartscreenDown,
+    HexViewerCustomEventStartscreenLeft,
+    HexViewerCustomEventStartscreenRight,
+    HexViewerCustomEventStartscreenOk,
+    HexViewerCustomEventStartscreenBack,
+    HexViewerCustomEventScene1Up,
+    HexViewerCustomEventScene1Down,
+    HexViewerCustomEventScene1Left,
+    HexViewerCustomEventScene1Right,
+    HexViewerCustomEventScene1Ok,
+    HexViewerCustomEventScene1Back,
+    HexViewerCustomEventScene2Up,
+    HexViewerCustomEventScene2Down,
+    HexViewerCustomEventScene2Left,
+    HexViewerCustomEventScene2Right,
+    HexViewerCustomEventScene2Ok,
+    HexViewerCustomEventScene2Back,
+} HexViewerCustomEvent;
+
+enum HexViewerCustomEventType {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    HexViewerCustomEventMenuVoid,
+    HexViewerCustomEventMenuSelected,
+    HexViewerCustomEventMenuPercentEntered,
+};
+
+#pragma pack(push, 1)
+typedef union {
+    uint32_t packed_value;
+    struct {
+        uint16_t type;
+        int16_t value;
+    } content;
+} HexViewerCustomEventMenu;
+#pragma pack(pop)
+
+static inline uint32_t hex_viewer_custom_menu_event_pack(uint16_t type, int16_t value) {
+    HexViewerCustomEventMenu event = {.content = {.type = type, .value = value}};
+    return event.packed_value;
+}
+static inline void
+    hex_viewer_custom_menu_event_unpack(uint32_t packed_value, uint16_t* type, int16_t* value) {
+    HexViewerCustomEventMenu event = {.packed_value = packed_value};
+    if(type) *type = event.content.type;
+    if(value) *value = event.content.value;
+}
+
+static inline uint16_t hex_viewer_custom_menu_event_get_type(uint32_t packed_value) {
+    uint16_t type;
+    hex_viewer_custom_menu_event_unpack(packed_value, &type, NULL);
+    return type;
+}
+
+static inline int16_t hex_viewer_custom_menu_event_get_value(uint32_t packed_value) {
+    int16_t value;
+    hex_viewer_custom_menu_event_unpack(packed_value, NULL, &value);
+    return value;
+}

+ 36 - 0
hex_viewer/helpers/hex_viewer_haptic.c

@@ -0,0 +1,36 @@
+#include "hex_viewer_haptic.h"
+#include "../hex_viewer.h"
+
+
+void hex_viewer_play_happy_bump(void* context) {
+    HexViewer* app = context;
+    if (app->haptic != 1) {
+        return;
+    }
+    notification_message(app->notification, &sequence_set_vibro_on);
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 20);
+    notification_message(app->notification, &sequence_reset_vibro);
+}
+
+void hex_viewer_play_bad_bump(void* context) {
+    HexViewer* app = context;
+    if (app->haptic != 1) {
+        return;
+    }
+    notification_message(app->notification, &sequence_set_vibro_on);
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 100);
+    notification_message(app->notification, &sequence_reset_vibro);
+}
+
+void hex_viewer_play_long_bump(void* context) {
+    HexViewer* app = context;
+    if (app->haptic != 1) {
+        return;
+    }
+    for (int i = 0; i < 4; i++) {
+        notification_message(app->notification, &sequence_set_vibro_on);
+        furi_thread_flags_wait(0, FuriFlagWaitAny, 50);
+        notification_message(app->notification, &sequence_reset_vibro);
+        furi_thread_flags_wait(0, FuriFlagWaitAny, 100);
+    }
+}

+ 8 - 0
hex_viewer/helpers/hex_viewer_haptic.h

@@ -0,0 +1,8 @@
+#include <notification/notification_messages.h>
+
+void hex_viewer_play_happy_bump(void* context);
+
+void hex_viewer_play_bad_bump(void* context);
+
+void hex_viewer_play_long_bump(void* context);
+

+ 39 - 0
hex_viewer/helpers/hex_viewer_led.c

@@ -0,0 +1,39 @@
+#include "hex_viewer_led.h"
+#include "../hex_viewer.h"
+
+
+
+void hex_viewer_led_set_rgb(void* context, int red, int green, int blue) {
+    HexViewer* app = context;
+    if (app->led != 1) {
+        return;
+    }
+    NotificationMessage notification_led_message_1;
+    notification_led_message_1.type = NotificationMessageTypeLedRed;
+    NotificationMessage notification_led_message_2;
+    notification_led_message_2.type = NotificationMessageTypeLedGreen;
+    NotificationMessage notification_led_message_3;
+    notification_led_message_3.type = NotificationMessageTypeLedBlue;
+
+    notification_led_message_1.data.led.value = red;
+    notification_led_message_2.data.led.value = green;
+    notification_led_message_3.data.led.value = blue;
+    const NotificationSequence notification_sequence = {
+        &notification_led_message_1,
+        &notification_led_message_2,
+        &notification_led_message_3,
+        &message_do_not_reset,
+        NULL,
+    };
+    notification_message(app->notification, &notification_sequence);
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 10); //Delay, prevent removal from RAM before LED value set    
+}
+
+void hex_viewer_led_reset(void* context) {
+    HexViewer* app = context;
+    notification_message(app->notification, &sequence_reset_red);
+    notification_message(app->notification, &sequence_reset_green);
+    notification_message(app->notification, &sequence_reset_blue);
+    
+    furi_thread_flags_wait(0, FuriFlagWaitAny, 300); //Delay, prevent removal from RAM before LED value set    
+}

+ 6 - 0
hex_viewer/helpers/hex_viewer_led.h

@@ -0,0 +1,6 @@
+
+
+void hex_viewer_led_set_rgb(void* context, int red, int green, int blue);
+
+void hex_viewer_led_reset(void* context);
+

+ 27 - 0
hex_viewer/helpers/hex_viewer_speaker.c

@@ -0,0 +1,27 @@
+#include "hex_viewer_speaker.h"
+#include "../hex_viewer.h"
+
+#define NOTE_INPUT 587.33f
+
+void hex_viewer_play_input_sound(void* context) {
+    HexViewer* app = context;
+    if (app->speaker != 1) {
+        return;
+    }
+    float volume = 1.0f;
+    if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
+        furi_hal_speaker_start(NOTE_INPUT, volume);
+    }
+    
+}
+
+void hex_viewer_stop_all_sound(void* context) {
+    HexViewer* app = context;
+    if (app->speaker != 1) {
+        return;
+    }
+    if(furi_hal_speaker_is_mine()) {
+        furi_hal_speaker_stop();
+        furi_hal_speaker_release();
+    }
+}

+ 4 - 0
hex_viewer/helpers/hex_viewer_speaker.h

@@ -0,0 +1,4 @@
+#define NOTE_INPUT 587.33f
+
+void hex_viewer_play_input_sound(void* context);
+void hex_viewer_stop_all_sound(void* context);

+ 173 - 0
hex_viewer/helpers/hex_viewer_storage.c

@@ -0,0 +1,173 @@
+#include "hex_viewer_storage.h"
+
+static Storage* hex_viewer_open_storage() {
+    return furi_record_open(RECORD_STORAGE);
+}
+
+static void hex_viewer_close_storage() {
+    furi_record_close(RECORD_STORAGE);
+}
+
+static void hex_viewer_close_config_file(FlipperFormat* file) {
+    if(file == NULL) return;
+    flipper_format_file_close(file);
+    flipper_format_free(file);
+}
+
+void hex_viewer_save_settings(void* context) {
+    HexViewer* app = context;
+    if(app->save_settings == 0) {
+        return;
+    }
+
+    FURI_LOG_D(TAG, "Saving Settings");
+    Storage* storage = hex_viewer_open_storage();
+    FlipperFormat* fff_file = flipper_format_file_alloc(storage);
+
+    // Overwrite wont work, so delete first
+    if(storage_file_exists(storage, HEX_VIEWER_SETTINGS_SAVE_PATH)) {
+        storage_simply_remove(storage, HEX_VIEWER_SETTINGS_SAVE_PATH);
+    }
+
+    // Open File, create if not exists
+    if(!storage_common_stat(storage, HEX_VIEWER_SETTINGS_SAVE_PATH, NULL) == FSE_OK) {
+        FURI_LOG_D(
+            TAG, "Config file %s is not found. Will create new.", HEX_VIEWER_SETTINGS_SAVE_PATH);
+        if(storage_common_stat(storage, CONFIG_FILE_DIRECTORY_PATH, NULL) == FSE_NOT_EXIST) {
+            FURI_LOG_D(
+                TAG, "Directory %s doesn't exist. Will create new.", CONFIG_FILE_DIRECTORY_PATH);
+            if(!storage_simply_mkdir(storage, CONFIG_FILE_DIRECTORY_PATH)) {
+                FURI_LOG_E(TAG, "Error creating directory %s", CONFIG_FILE_DIRECTORY_PATH);
+            }
+        }
+    }
+
+    if(!flipper_format_file_open_new(fff_file, HEX_VIEWER_SETTINGS_SAVE_PATH)) {
+        //totp_close_config_file(fff_file);
+        FURI_LOG_E(TAG, "Error creating new file %s", HEX_VIEWER_SETTINGS_SAVE_PATH);
+        hex_viewer_close_storage();
+        return;
+    }
+
+    // Store Settings
+    flipper_format_write_header_cstr(
+        fff_file, HEX_VIEWER_SETTINGS_HEADER, HEX_VIEWER_SETTINGS_FILE_VERSION);
+    flipper_format_write_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_HAPTIC, &app->haptic, 1);
+    flipper_format_write_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_SPEAKER, &app->speaker, 1);
+    flipper_format_write_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_LED, &app->led, 1);
+    flipper_format_write_uint32(
+        fff_file, HEX_VIEWER_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1);
+
+    if(!flipper_format_rewind(fff_file)) {
+        hex_viewer_close_config_file(fff_file);
+        FURI_LOG_E(TAG, "Rewind error");
+        hex_viewer_close_storage();
+        return;
+    }
+
+    hex_viewer_close_config_file(fff_file);
+    hex_viewer_close_storage();
+}
+
+void hex_viewer_read_settings(void* context) {
+    HexViewer* app = context;
+    Storage* storage = hex_viewer_open_storage();
+    FlipperFormat* fff_file = flipper_format_file_alloc(storage);
+
+    if(storage_common_stat(storage, HEX_VIEWER_SETTINGS_SAVE_PATH, NULL) != FSE_OK) {
+        hex_viewer_close_config_file(fff_file);
+        hex_viewer_close_storage();
+        return;
+    }
+    uint32_t file_version;
+    FuriString* temp_str = furi_string_alloc();
+
+    if(!flipper_format_file_open_existing(fff_file, HEX_VIEWER_SETTINGS_SAVE_PATH)) {
+        FURI_LOG_E(TAG, "Cannot open file %s", HEX_VIEWER_SETTINGS_SAVE_PATH);
+        hex_viewer_close_config_file(fff_file);
+        hex_viewer_close_storage();
+        return;
+    }
+
+    if(!flipper_format_read_header(fff_file, temp_str, &file_version)) {
+        FURI_LOG_E(TAG, "Missing Header Data");
+        hex_viewer_close_config_file(fff_file);
+        hex_viewer_close_storage();
+        furi_string_free(temp_str);
+        return;
+    }
+    furi_string_free(temp_str);
+
+    if(file_version < HEX_VIEWER_SETTINGS_FILE_VERSION) {
+        FURI_LOG_I(TAG, "old config version, will be removed.");
+        hex_viewer_close_config_file(fff_file);
+        hex_viewer_close_storage();
+        return;
+    }
+
+    flipper_format_read_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_HAPTIC, &app->haptic, 1);
+    flipper_format_read_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_SPEAKER, &app->speaker, 1);
+    flipper_format_read_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_LED, &app->led, 1);
+    flipper_format_read_uint32(
+        fff_file, HEX_VIEWER_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1);
+
+    flipper_format_rewind(fff_file);
+
+    hex_viewer_close_config_file(fff_file);
+    hex_viewer_close_storage();
+}
+
+bool hex_viewer_open_file(void* context, const char* file_path) {
+    HexViewer* hex_viewer = context;
+    furi_assert(hex_viewer);
+    furi_assert(file_path);
+
+    // TODO Separate function?
+    if(hex_viewer->model->stream) {
+        buffered_file_stream_close(hex_viewer->model->stream);
+        stream_free(hex_viewer->model->stream);
+        hex_viewer->model->file_offset = 0;
+    }
+
+    hex_viewer->model->stream = buffered_file_stream_alloc(hex_viewer->storage);
+    bool isOk = true;
+
+    do {
+        if(!buffered_file_stream_open(
+               hex_viewer->model->stream, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) {
+            FURI_LOG_E(TAG, "Unable to open stream: %s", file_path);
+            isOk = false;
+            break;
+        };
+
+        hex_viewer->model->file_size = stream_size(hex_viewer->model->stream);
+    } while(false);
+
+    return isOk;
+}
+
+bool hex_viewer_read_file(void* context) {
+    HexViewer* hex_viewer = context;
+    furi_assert(hex_viewer);
+    furi_assert(hex_viewer->model->stream);
+    furi_assert(hex_viewer->model->file_offset % HEX_VIEWER_BYTES_PER_LINE == 0);
+
+    memset(hex_viewer->model->file_bytes, 0x0, HEX_VIEWER_BUF_SIZE);
+    bool isOk = true;
+
+    do {
+        uint32_t offset = hex_viewer->model->file_offset;
+        if(!stream_seek(hex_viewer->model->stream, offset, true)) {
+            FURI_LOG_E(TAG, "Unable to seek stream");
+            isOk = false;
+            break;
+        }
+
+        hex_viewer->model->file_read_bytes = stream_read(
+            hex_viewer->model->stream,
+            (uint8_t*)hex_viewer->model->file_bytes,
+            HEX_VIEWER_BUF_SIZE);
+    } while(false);
+
+    return isOk;
+}

+ 23 - 0
hex_viewer/helpers/hex_viewer_storage.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <stdlib.h>
+#include <string.h>
+#include <storage/storage.h>
+#include <flipper_format/flipper_format_i.h>
+#include "../hex_viewer.h"
+
+#define HEX_VIEWER_SETTINGS_FILE_VERSION 1
+#define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/hex_viewer")
+#define HEX_VIEWER_SETTINGS_SAVE_PATH CONFIG_FILE_DIRECTORY_PATH "/hex_viewer.conf"
+#define HEX_VIEWER_SETTINGS_SAVE_PATH_TMP HEX_VIEWER_SETTINGS_SAVE_PATH ".tmp"
+#define HEX_VIEWER_SETTINGS_HEADER "HexViewer Config File"
+#define HEX_VIEWER_SETTINGS_KEY_HAPTIC "Haptic"
+#define HEX_VIEWER_SETTINGS_KEY_LED "Led"
+#define HEX_VIEWER_SETTINGS_KEY_SPEAKER "Speaker"
+#define HEX_VIEWER_SETTINGS_KEY_SAVE_SETTINGS "SaveSettings"
+
+void hex_viewer_save_settings(void* context);
+void hex_viewer_read_settings(void* context);
+
+bool hex_viewer_open_file(void* context, const char* file_path);
+bool hex_viewer_read_file(void* context);

+ 143 - 0
hex_viewer/hex_viewer.c

@@ -0,0 +1,143 @@
+#include "hex_viewer.h"
+
+bool hex_viewer_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    HexViewer* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+void hex_viewer_tick_event_callback(void* context) {
+    furi_assert(context);
+    HexViewer* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+//leave app if back button pressed
+bool hex_viewer_navigation_event_callback(void* context) {
+    furi_assert(context);
+    HexViewer* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+HexViewer* hex_viewer_app_alloc() {
+    HexViewer* app = malloc(sizeof(HexViewer));
+
+    app->model = malloc(sizeof(HexViewerModel));
+    memset(app->model, 0, sizeof(HexViewerModel));
+
+    app->gui = furi_record_open(RECORD_GUI);
+    app->storage = furi_record_open(RECORD_STORAGE);
+    app->notification = furi_record_open(RECORD_NOTIFICATION);
+
+    //Turn backlight on, believe me this makes testing your app easier
+    notification_message(app->notification, &sequence_display_backlight_on);
+
+    //Scene additions
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    app->scene_manager = scene_manager_alloc(&hex_viewer_scene_handlers, app);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, hex_viewer_navigation_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, hex_viewer_tick_event_callback, 100);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, hex_viewer_custom_event_callback);
+
+    app->submenu = submenu_alloc();
+    app->text_input = text_input_alloc();
+
+    // Set defaults, in case no config loaded
+    app->haptic = 1;
+    app->speaker = 1;
+    app->led = 1;
+    app->save_settings = 1;
+
+    // Used for File Browser
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+    app->file_path = furi_string_alloc();
+
+    // Load configs
+    hex_viewer_read_settings(app);
+
+    view_dispatcher_add_view(
+        app->view_dispatcher, HexViewerViewIdMenu, submenu_get_view(app->submenu));
+
+    app->hex_viewer_startscreen = hex_viewer_startscreen_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        HexViewerViewIdStartscreen,
+        hex_viewer_startscreen_get_view(app->hex_viewer_startscreen));
+
+    view_dispatcher_add_view(
+        app->view_dispatcher, HexViewerViewIdScroll, text_input_get_view(app->text_input));
+
+    app->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        HexViewerViewIdSettings,
+        variable_item_list_get_view(app->variable_item_list));
+
+    //End Scene Additions
+
+    return app;
+}
+
+void hex_viewer_app_free(HexViewer* app) {
+    furi_assert(app);
+
+    if(app->model->stream) {
+        buffered_file_stream_close(app->model->stream);
+        stream_free(app->model->stream);
+    }
+
+    // Scene manager
+    scene_manager_free(app->scene_manager);
+
+    // View Dispatcher
+    view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdMenu);
+    view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdStartscreen);
+    view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdScroll);
+    view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdSettings);
+
+    submenu_free(app->submenu);
+    text_input_free(app->text_input);
+
+    view_dispatcher_free(app->view_dispatcher);
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_GUI);
+
+    app->storage = NULL;
+    app->gui = NULL;
+    app->notification = NULL;
+
+    // Close File Browser
+    furi_record_close(RECORD_DIALOGS);
+    furi_string_free(app->file_path);
+
+    free(app->model);
+
+    //Remove whatever is left
+    free(app);
+}
+
+int32_t hex_viewer_app(void* p) {
+    UNUSED(p);
+    HexViewer* app = hex_viewer_app_alloc();
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    scene_manager_next_scene(app->scene_manager, HexViewerSceneStartscreen);
+
+    furi_hal_power_suppress_charge_enter();
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    hex_viewer_save_settings(app);
+
+    furi_hal_power_suppress_charge_exit();
+    hex_viewer_app_free(app);
+
+    return 0;
+}

+ 92 - 0
hex_viewer/hex_viewer.h

@@ -0,0 +1,92 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <stdlib.h>
+#include <hex_viewer_icons.h>
+#include <dialogs/dialogs.h>
+#include <notification/notification_messages.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/text_input.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/button_menu.h>
+#include <gui/modules/dialog_ex.h>
+#include "scenes/hex_viewer_scene.h"
+#include "views/hex_viewer_startscreen.h"
+#include "helpers/hex_viewer_storage.h"
+
+#include <storage/storage.h>
+#include <stream/stream.h>
+#include <stream/buffered_file_stream.h>
+#include <toolbox/stream/file_stream.h>
+
+#define TAG "HexViewer"
+
+#define HEX_VIEWER_APP_PATH_FOLDER "/any" // TODO ANY_PATH
+#define HEX_VIEWER_APP_EXTENSION "*"
+#define HEX_VIEWER_PERCENT_INPUT 16
+
+#define HEX_VIEWER_BYTES_PER_LINE 4u
+#define HEX_VIEWER_LINES_ON_SCREEN 4u
+#define HEX_VIEWER_BUF_SIZE (HEX_VIEWER_LINES_ON_SCREEN * HEX_VIEWER_BYTES_PER_LINE)
+
+typedef struct {
+    uint8_t file_bytes[HEX_VIEWER_LINES_ON_SCREEN][HEX_VIEWER_BYTES_PER_LINE];
+    uint32_t file_offset;
+    uint32_t file_read_bytes;
+    uint32_t file_size;
+
+    Stream* stream;
+} HexViewerModel;
+
+typedef struct {
+    HexViewerModel* model;
+
+    Gui* gui;
+    Storage* storage;
+    NotificationApp* notification;
+    ViewDispatcher* view_dispatcher;
+    Submenu* submenu;
+    TextInput* text_input;
+    SceneManager* scene_manager;
+    VariableItemList* variable_item_list;
+    HexViewerStartscreen* hex_viewer_startscreen;
+    DialogsApp* dialogs; // File Browser
+    FuriString* file_path; // File Browser
+    uint32_t haptic;
+    uint32_t speaker;
+    uint32_t led;
+    uint32_t save_settings;
+    char percent_buf[HEX_VIEWER_PERCENT_INPUT];
+} HexViewer;
+
+typedef enum {
+    HexViewerViewIdStartscreen,
+    HexViewerViewIdMenu,
+    HexViewerViewIdScroll,
+    HexViewerViewIdSettings,
+} HexViewerViewId;
+
+typedef enum {
+    HexViewerHapticOff,
+    HexViewerHapticOn,
+} HexViewerHapticState;
+
+typedef enum {
+    HexViewerSpeakerOff,
+    HexViewerSpeakerOn,
+} HexViewerSpeakerState;
+
+typedef enum {
+    HexViewerLedOff,
+    HexViewerLedOn,
+} HexViewerLedState;
+
+typedef enum {
+    HexViewerSettingsOff,
+    HexViewerSettingsOn,
+} HexViewerSettingsStoreState;

BIN
hex_viewer/icons/hex_10px.bmp


BIN
hex_viewer/icons/hex_10px.png


BIN
hex_viewer/img/1.png


BIN
hex_viewer/img/2.png


+ 30 - 0
hex_viewer/scenes/hex_viewer_scene.c

@@ -0,0 +1,30 @@
+#include "hex_viewer_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const hex_viewer_on_enter_handlers[])(void*) = {
+#include "hex_viewer_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const hex_viewer_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "hex_viewer_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const hex_viewer_on_exit_handlers[])(void* context) = {
+#include "hex_viewer_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers hex_viewer_scene_handlers = {
+    .on_enter_handlers = hex_viewer_on_enter_handlers,
+    .on_event_handlers = hex_viewer_on_event_handlers,
+    .on_exit_handlers = hex_viewer_on_exit_handlers,
+    .scene_num = HexViewerSceneNum,
+};

+ 29 - 0
hex_viewer/scenes/hex_viewer_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) HexViewerScene##id,
+typedef enum {
+#include "hex_viewer_scene_config.h"
+    HexViewerSceneNum,
+} HexViewerScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers hex_viewer_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "hex_viewer_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "hex_viewer_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "hex_viewer_scene_config.h"
+#undef ADD_SCENE

+ 6 - 0
hex_viewer/scenes/hex_viewer_scene_config.h

@@ -0,0 +1,6 @@
+ADD_SCENE(hex_viewer, startscreen, Startscreen)
+ADD_SCENE(hex_viewer, menu, Menu)
+ADD_SCENE(hex_viewer, scroll, Scroll)
+ADD_SCENE(hex_viewer, info, Info)
+ADD_SCENE(hex_viewer, open, Open)
+ADD_SCENE(hex_viewer, settings, Settings)

+ 42 - 0
hex_viewer/scenes/hex_viewer_scene_info.c

@@ -0,0 +1,42 @@
+#include "../hex_viewer.h"
+
+void hex_viewer_scene_info_on_enter(void* context) {
+    furi_assert(context);
+    HexViewer* app = context;
+
+    FuriString* buffer;
+    buffer = furi_string_alloc();
+    furi_string_printf(
+        buffer,
+        "File path: %s\nFile size: %lu (0x%lX)",
+        furi_string_get_cstr(app->file_path),
+        app->model->file_size,
+        app->model->file_size);
+
+    DialogMessage* message = dialog_message_alloc();
+    dialog_message_set_header(message, "Hex Viewer v2.0", 16, 2, AlignLeft, AlignTop);
+    dialog_message_set_icon(message, &I_hex_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(app->dialogs, message);
+
+    furi_string_free(buffer);
+    dialog_message_free(message);
+
+    scene_manager_search_and_switch_to_previous_scene(
+        app->scene_manager, HexViewerViewIdStartscreen);
+}
+
+bool hex_viewer_scene_info_on_event(void* context, SceneManagerEvent event) {
+    HexViewer* app = context;
+    UNUSED(app);
+    UNUSED(event);
+    bool consumed = true;
+
+    return consumed;
+}
+
+void hex_viewer_scene_info_on_exit(void* context) {
+    HexViewer* app = context;
+    UNUSED(app);
+}

+ 83 - 0
hex_viewer/scenes/hex_viewer_scene_menu.c

@@ -0,0 +1,83 @@
+#include "../hex_viewer.h"
+
+enum SubmenuIndex {
+    SubmenuIndexScroll = 10,
+    SubmenuIndexInfo,
+    SubmenuIndexOpen,
+    // SubmenuIndexSettings,
+};
+
+void hex_viewer_scene_menu_submenu_callback(void* context, uint32_t index) {
+    HexViewer* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void hex_viewer_scene_menu_on_enter(void* context) {
+    HexViewer* app = context;
+
+    submenu_set_header(app->submenu, "Select action");
+    submenu_add_item(
+        app->submenu,
+        "Open file ...",
+        SubmenuIndexOpen,
+        hex_viewer_scene_menu_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Scroll to ...",
+        SubmenuIndexScroll,
+        hex_viewer_scene_menu_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Show info ...",
+        SubmenuIndexInfo,
+        hex_viewer_scene_menu_submenu_callback,
+        app);
+    // submenu_add_item(app->submenu, "Settings", SubmenuIndexSettings, hex_viewer_scene_menu_submenu_callback, app);
+
+    submenu_set_selected_item(
+        app->submenu, scene_manager_get_scene_state(app->scene_manager, HexViewerSceneMenu));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerViewIdMenu);
+}
+
+bool hex_viewer_scene_menu_on_event(void* context, SceneManagerEvent event) {
+    HexViewer* app = context;
+
+    if(event.type == SceneManagerEventTypeBack) {
+        //exit app
+        // scene_manager_stop(app->scene_manager);
+        // view_dispatcher_stop(app->view_dispatcher);
+        scene_manager_previous_scene(app->scene_manager);
+        return true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexScroll) {
+            scene_manager_set_scene_state(
+                app->scene_manager, HexViewerSceneMenu, SubmenuIndexScroll);
+            scene_manager_next_scene(app->scene_manager, HexViewerSceneScroll);
+            return true;
+        } else if(event.event == SubmenuIndexInfo) {
+            scene_manager_set_scene_state(
+                app->scene_manager, HexViewerSceneMenu, SubmenuIndexInfo);
+            scene_manager_next_scene(app->scene_manager, HexViewerSceneInfo);
+            return true;
+        } else if(event.event == SubmenuIndexOpen) {
+            scene_manager_set_scene_state(
+                app->scene_manager, HexViewerSceneMenu, SubmenuIndexOpen);
+            scene_manager_next_scene(app->scene_manager, HexViewerSceneOpen);
+            // } else if (event.event == SubmenuIndexSettings) {
+            //     scene_manager_set_scene_state(
+            //         app->scene_manager, HexViewerSceneMenu, SubmenuIndexSettings);
+            //     scene_manager_next_scene(app->scene_manager, HexViewerSceneSettings);
+            //     return true;
+        }
+    }
+
+    return false;
+}
+
+void hex_viewer_scene_menu_on_exit(void* context) {
+    HexViewer* app = context;
+    submenu_reset(app->submenu);
+}

+ 42 - 0
hex_viewer/scenes/hex_viewer_scene_open.c

@@ -0,0 +1,42 @@
+#include "../hex_viewer.h"
+
+void hex_viewer_scene_open_on_enter(void* context) {
+    furi_assert(context);
+    HexViewer* app = context;
+
+    FuriString* initial_path;
+    initial_path = furi_string_alloc();
+    furi_string_set(initial_path, HEX_VIEWER_APP_PATH_FOLDER);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, HEX_VIEWER_APP_EXTENSION, &I_hex_10px);
+    browser_options.hide_ext = false;
+
+    bool success =
+        dialog_file_browser_show(app->dialogs, app->file_path, initial_path, &browser_options);
+    furi_string_free(initial_path);
+
+    if(success) {
+        success = hex_viewer_open_file(app, furi_string_get_cstr(app->file_path));
+        if(success) hex_viewer_read_file(app);
+    }
+
+    if(success) {
+        scene_manager_search_and_switch_to_previous_scene(
+            app->scene_manager, HexViewerViewIdStartscreen);
+    } else {
+        scene_manager_previous_scene(app->scene_manager);
+    }
+}
+
+bool hex_viewer_scene_open_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    bool consumed = true;
+
+    return consumed;
+}
+
+void hex_viewer_scene_open_on_exit(void* context) {
+    UNUSED(context);
+}

+ 61 - 0
hex_viewer/scenes/hex_viewer_scene_scroll.c

@@ -0,0 +1,61 @@
+#include "../hex_viewer.h"
+#include "../helpers/hex_viewer_custom_event.h"
+
+void hex_viewer_scene_scroll_callback(void* context) {
+    HexViewer* app = (HexViewer*)context;
+    view_dispatcher_send_custom_event(
+        app->view_dispatcher, HexViewerCustomEventMenuPercentEntered);
+}
+
+void hex_viewer_scene_scroll_on_enter(void* context) {
+    furi_assert(context);
+    HexViewer* app = context;
+
+    TextInput* text_input = app->text_input;
+
+    text_input_set_header_text(text_input, "Scroll to percentage (0..100)");
+    text_input_set_result_callback(
+        text_input,
+        hex_viewer_scene_scroll_callback,
+        app,
+        app->percent_buf,
+        HEX_VIEWER_PERCENT_INPUT,
+        false);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerSceneScroll);
+}
+
+bool hex_viewer_scene_scroll_on_event(void* context, SceneManagerEvent event) {
+    HexViewer* app = (HexViewer*)context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == HexViewerCustomEventMenuPercentEntered) {
+            int ipercent = atoi(app->percent_buf);
+            ipercent = MIN(ipercent, 100);
+            ipercent = MAX(ipercent, 0);
+            float percent = ipercent / 100.0;
+
+            uint32_t line_count = app->model->file_size / HEX_VIEWER_BYTES_PER_LINE;
+            if(app->model->file_size % HEX_VIEWER_BYTES_PER_LINE != 0) line_count += 1;
+            uint32_t scrollable_lines = line_count - HEX_VIEWER_LINES_ON_SCREEN;
+            uint32_t target_line = (uint32_t)(percent * scrollable_lines);
+
+            uint32_t new_file_offset = target_line * HEX_VIEWER_BYTES_PER_LINE;
+            if(app->model->file_size > new_file_offset) {
+                app->model->file_offset = new_file_offset;
+                if(!hex_viewer_read_file(app)) new_file_offset = new_file_offset; // TODO Do smth
+            }
+
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, HexViewerViewIdStartscreen);
+
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void hex_viewer_scene_scroll_on_exit(void* context) {
+    UNUSED(context);
+}

+ 147 - 0
hex_viewer/scenes/hex_viewer_scene_settings.c

@@ -0,0 +1,147 @@
+#include "../hex_viewer.h"
+#include <lib/toolbox/value_index.h>
+
+enum SettingsIndex {
+    SettingsIndexHaptic = 10,
+    SettingsIndexValue1,
+    SettingsIndexValue2,
+};
+
+const char* const haptic_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t haptic_value[2] = {
+    HexViewerHapticOff,
+    HexViewerHapticOn,
+};
+
+const char* const speaker_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t speaker_value[2] = {
+    HexViewerSpeakerOff,
+    HexViewerSpeakerOn,
+};
+
+const char* const led_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t led_value[2] = {
+    HexViewerLedOff,
+    HexViewerLedOn,
+};
+
+const char* const settings_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t settings_value[2] = {
+    HexViewerSettingsOff,
+    HexViewerSettingsOn,
+};
+
+
+static void hex_viewer_scene_settings_set_haptic(VariableItem* item) {
+    HexViewer* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, haptic_text[index]);
+    app->haptic = haptic_value[index];
+}
+
+static void hex_viewer_scene_settings_set_speaker(VariableItem* item) {
+    HexViewer* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, speaker_text[index]);
+    app->speaker = speaker_value[index];
+}
+
+static void hex_viewer_scene_settings_set_led(VariableItem* item) {
+    HexViewer* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, led_text[index]);
+    app->led = led_value[index];
+}
+
+static void hex_viewer_scene_settings_set_save_settings(VariableItem* item) {
+    HexViewer* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, settings_text[index]);
+    app->save_settings = settings_value[index];
+}
+
+void hex_viewer_scene_settings_submenu_callback(void* context, uint32_t index) {
+    HexViewer* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void hex_viewer_scene_settings_on_enter(void* context) {
+    HexViewer* app = context;
+    VariableItem* item;
+    uint8_t value_index;
+
+    // Vibro on/off
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "Vibro/Haptic:",
+        2,
+        hex_viewer_scene_settings_set_haptic,
+        app);
+    value_index = value_index_uint32(app->haptic, haptic_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, haptic_text[value_index]);
+
+    // Sound on/off
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "Sound:",
+        2,
+        hex_viewer_scene_settings_set_speaker,
+        app);
+    value_index = value_index_uint32(app->speaker, speaker_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, speaker_text[value_index]);
+
+    // LED Effects on/off
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "LED FX:",
+        2,
+        hex_viewer_scene_settings_set_led,
+        app);
+    value_index = value_index_uint32(app->led, led_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, led_text[value_index]);
+
+    // Save Settings to File
+    item = variable_item_list_add(
+        app->variable_item_list,
+        "Save Settings",
+        2,
+        hex_viewer_scene_settings_set_save_settings,
+        app);
+    value_index = value_index_uint32(app->save_settings, settings_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, settings_text[value_index]);
+    
+    view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerViewIdSettings);
+}
+
+bool hex_viewer_scene_settings_on_event(void* context, SceneManagerEvent event) {
+    HexViewer* app = context;
+    UNUSED(app);
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        
+    }
+    return consumed;
+}
+
+void hex_viewer_scene_settings_on_exit(void* context) {
+    HexViewer* app = context;
+    variable_item_list_set_selected_item(app->variable_item_list, 0);
+    variable_item_list_reset(app->variable_item_list);
+}

+ 65 - 0
hex_viewer/scenes/hex_viewer_scene_startscreen.c

@@ -0,0 +1,65 @@
+#include "../hex_viewer.h"
+#include "../helpers/hex_viewer_custom_event.h"
+#include "../views/hex_viewer_startscreen.h"
+
+void hex_viewer_scene_startscreen_callback(HexViewerCustomEvent event, void* context) {
+    furi_assert(context);
+    HexViewer* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void hex_viewer_scene_startscreen_on_enter(void* context) {
+    furi_assert(context);
+    HexViewer* app = context;
+    hex_viewer_startscreen_set_callback(
+        app->hex_viewer_startscreen, hex_viewer_scene_startscreen_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerViewIdStartscreen);
+}
+
+bool hex_viewer_scene_startscreen_on_event(void* context, SceneManagerEvent event) {
+    HexViewer* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case HexViewerCustomEventStartscreenLeft:
+            //app->model->mode = !app->model->mode;
+            consumed = true;
+            break;
+        case HexViewerCustomEventStartscreenRight:
+            consumed = true;
+            break;
+        case HexViewerCustomEventStartscreenUp:
+            consumed = true;
+            break;
+        case HexViewerCustomEventStartscreenDown:
+            consumed = true;
+            break;
+        case HexViewerCustomEventStartscreenOk:
+            if(!app->model->file_size)
+                scene_manager_next_scene(app->scene_manager, HexViewerSceneOpen);
+            else
+                scene_manager_next_scene(app->scene_manager, HexViewerSceneMenu);
+            consumed = true;
+            break;
+        case HexViewerCustomEventStartscreenBack: // TODO Delete
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, HexViewerSceneStartscreen)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void hex_viewer_scene_startscreen_on_exit(void* context) {
+    HexViewer* app = context;
+    UNUSED(app);
+}

+ 248 - 0
hex_viewer/views/hex_viewer_startscreen.c

@@ -0,0 +1,248 @@
+#include "../hex_viewer.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+
+struct HexViewerStartscreen {
+    View* view;
+    HexViewerStartscreenCallback callback;
+    void* context;
+};
+
+typedef struct {
+    uint8_t file_bytes[HEX_VIEWER_LINES_ON_SCREEN][HEX_VIEWER_BYTES_PER_LINE];
+    uint32_t file_offset;
+    uint32_t file_read_bytes;
+    uint32_t file_size;
+    bool mode;
+    uint32_t dbg;
+} HexViewerStartscreenModel;
+
+void hex_viewer_startscreen_set_callback(
+    HexViewerStartscreen* instance,
+    HexViewerStartscreenCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void hex_viewer_startscreen_draw(Canvas* canvas, HexViewerStartscreenModel* model) {
+    canvas_clear(canvas);
+
+    if(!model->file_size) {
+        canvas_set_color(canvas, ColorBlack);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignTop, "HexViewer v2.0");
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str_aligned(canvas, 64, 22, AlignCenter, AlignTop, "Basic hex viewer");
+        canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignTop, "for your Flipper");
+        elements_button_center(canvas, "Open");
+    } else {
+        canvas_set_color(canvas, ColorBlack);
+
+        elements_button_left(canvas, model->mode ? "Addr" : "Text");
+        //elements_button_right(canvas, "Info");
+        elements_button_center(canvas, "Menu");
+
+        int ROW_HEIGHT = 12;
+        int TOP_OFFSET = 10;
+        int LEFT_OFFSET = 3;
+
+        uint32_t line_count = model->file_size / HEX_VIEWER_BYTES_PER_LINE;
+        if(model->file_size % HEX_VIEWER_BYTES_PER_LINE != 0) line_count += 1;
+        uint32_t first_line_on_screen = model->file_offset / HEX_VIEWER_BYTES_PER_LINE;
+        if(line_count > HEX_VIEWER_LINES_ON_SCREEN) {
+            uint8_t width = canvas_width(canvas);
+            elements_scrollbar_pos(
+                canvas,
+                width,
+                0,
+                ROW_HEIGHT * HEX_VIEWER_LINES_ON_SCREEN,
+                first_line_on_screen,
+                line_count - (HEX_VIEWER_LINES_ON_SCREEN - 1));
+        }
+
+        char temp_buf[32];
+        uint32_t row_iters = model->file_read_bytes / HEX_VIEWER_BYTES_PER_LINE;
+        if(model->file_read_bytes % HEX_VIEWER_BYTES_PER_LINE != 0) row_iters += 1;
+
+        // For the rest of drawing.
+        canvas_set_font(canvas, FontKeyboard);
+
+        for(uint32_t i = 0; i < row_iters; ++i) {
+            uint32_t bytes_left_per_row = model->file_read_bytes - i * HEX_VIEWER_BYTES_PER_LINE;
+            bytes_left_per_row = MIN(bytes_left_per_row, HEX_VIEWER_BYTES_PER_LINE);
+
+            if(model->mode) {
+                memcpy(temp_buf, 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 = model->file_offset + i * HEX_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);
+            }
+
+            char* p = temp_buf;
+            for(uint32_t j = 0; j < bytes_left_per_row; ++j)
+                p += snprintf(p, 32, "%02X ", model->file_bytes[i][j]);
+
+            //canvas_set_font(canvas, FontKeyboard);
+            canvas_draw_str(canvas, LEFT_OFFSET + 41, TOP_OFFSET + i * ROW_HEIGHT, temp_buf);
+        }
+
+        // Poor man's debug
+        // snprintf(temp_buf, 32, "D %02lX", model->dbg);
+        // elements_button_right(canvas, temp_buf);
+    }
+}
+
+static void hex_viewer_startscreen_model_init(HexViewerStartscreenModel* const model) {
+    memset(model->file_bytes, 0, sizeof(model->file_bytes));
+    model->file_offset = 0;
+    model->file_read_bytes = 0;
+    model->file_size = 0;
+    model->mode = false;
+    model->dbg = 0;
+}
+
+static void
+    update_local_model_from_app(HexViewer* const app, HexViewerStartscreenModel* const model) {
+    memcpy(model->file_bytes, app->model->file_bytes, sizeof(model->file_bytes));
+    model->file_offset = app->model->file_offset;
+    model->file_read_bytes = app->model->file_read_bytes;
+    model->file_size = app->model->file_size;
+    //model->mode = app->model->mode;
+}
+
+bool hex_viewer_startscreen_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    HexViewerStartscreen* instance = context;
+    HexViewer* app = instance->context; // TO so good, but works
+
+    if(event->type == InputTypeRelease || event->type == InputTypeRepeat) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                HexViewerStartscreenModel * model,
+                {
+                    instance->callback(HexViewerCustomEventStartscreenBack, instance->context);
+                    update_local_model_from_app(instance->context, model);
+                },
+                true);
+            break;
+        case InputKeyLeft:
+            with_view_model(
+                instance->view,
+                HexViewerStartscreenModel * model,
+                { model->mode = !model->mode; },
+                true);
+            break;
+        case InputKeyRight:
+            with_view_model(
+                instance->view, HexViewerStartscreenModel * model, { model->dbg = 0; }, true);
+            break;
+        case InputKeyUp:
+            with_view_model(
+                instance->view,
+                HexViewerStartscreenModel * model,
+                {
+                    if(app->model->file_offset > 0) {
+                        app->model->file_offset -= HEX_VIEWER_BYTES_PER_LINE;
+                        if(!hex_viewer_read_file(app)) break; // TODO Do smth
+                    }
+
+                    update_local_model_from_app(instance->context, model);
+                },
+                true);
+            break;
+        case InputKeyDown:
+            with_view_model(
+                instance->view,
+                HexViewerStartscreenModel * model,
+                {
+                    uint32_t last_byte_on_screen =
+                        app->model->file_offset + app->model->file_read_bytes;
+                    if(app->model->file_size > last_byte_on_screen) {
+                        app->model->file_offset += HEX_VIEWER_BYTES_PER_LINE;
+                        if(!hex_viewer_read_file(app)) break; // TODO Do smth
+                    }
+
+                    update_local_model_from_app(instance->context, model);
+                },
+                true);
+            break;
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                HexViewerStartscreenModel * model,
+                {
+                    instance->callback(HexViewerCustomEventStartscreenOk, instance->context);
+                    update_local_model_from_app(instance->context, model);
+                },
+                true);
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+
+    return true;
+}
+
+void hex_viewer_startscreen_exit(void* context) {
+    furi_assert(context);
+}
+
+void hex_viewer_startscreen_enter(void* context) {
+    furi_assert(context);
+    HexViewerStartscreen* instance = (HexViewerStartscreen*)context;
+    with_view_model(
+        instance->view,
+        HexViewerStartscreenModel * model,
+        { update_local_model_from_app(instance->context, model); },
+        true);
+}
+
+HexViewerStartscreen* hex_viewer_startscreen_alloc() {
+    HexViewerStartscreen* instance = malloc(sizeof(HexViewerStartscreen));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(HexViewerStartscreenModel));
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)hex_viewer_startscreen_draw);
+    view_set_input_callback(instance->view, hex_viewer_startscreen_input);
+    view_set_enter_callback(instance->view, hex_viewer_startscreen_enter);
+    view_set_exit_callback(instance->view, hex_viewer_startscreen_exit);
+
+    with_view_model(
+        instance->view,
+        HexViewerStartscreenModel * model,
+        { hex_viewer_startscreen_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void hex_viewer_startscreen_free(HexViewerStartscreen* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, HexViewerStartscreenModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* hex_viewer_startscreen_get_view(HexViewerStartscreen* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 19 - 0
hex_viewer/views/hex_viewer_startscreen.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/hex_viewer_custom_event.h"
+
+typedef struct HexViewerStartscreen HexViewerStartscreen;
+
+typedef void (*HexViewerStartscreenCallback)(HexViewerCustomEvent event, void* context);
+
+void hex_viewer_startscreen_set_callback(
+    HexViewerStartscreen* hex_viewer_startscreen,
+    HexViewerStartscreenCallback callback,
+    void* context);
+
+View* hex_viewer_startscreen_get_view(HexViewerStartscreen* hex_viewer_static);
+
+HexViewerStartscreen* hex_viewer_startscreen_alloc();
+
+void hex_viewer_startscreen_free(HexViewerStartscreen* hex_viewer_static);