Просмотр исходного кода

Add nfc_playlist from https://github.com/acegoal07/FlipperZero_NFC_Playlist

git-subtree-dir: nfc_playlist
git-subtree-mainline: fc8831a879b8bfc49c0bc206c33f7ae61df4f906
git-subtree-split: 530fc9ff24ce63d5204d6cc98232f435c0ba4761
Willy-JL 1 год назад
Родитель
Сommit
e7d36eec06

+ 4 - 0
nfc_playlist/.github/FUNDING.yml

@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+github: "acegoal07"
+custom: "https://acegoal07.dev/donate"

+ 21 - 0
nfc_playlist/.github/ISSUE_TEMPLATE/bug_report.md

@@ -0,0 +1,21 @@
+---
+name: Bug report
+about: A bug report
+title: "[BUG]"
+labels: bug
+assignees: acegoal07
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - Firmware [e.g. Xtreme]
+ - Version [e.g. 1.1]
+
+**Additional context**
+Add any other context about the problem here.

+ 11 - 0
nfc_playlist/.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,11 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: "[FEATURE}"
+labels: enhancement
+assignees: acegoal07
+
+---
+
+**Describe the feature**
+A indepth description of what you want

+ 1 - 0
nfc_playlist/.gitignore

@@ -0,0 +1 @@
+notes.txt

+ 1 - 0
nfc_playlist/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/acegoal07/FlipperZero_NFC_Playlist main /

+ 37 - 0
nfc_playlist/README.md

@@ -0,0 +1,37 @@
+# FlipperZero_NFC_Playlist:
+The idea behind this app is to allow for you to test multiple copies of NFC's at once as a bulk test
+## How it works:
+When starting the app you are greeted by a select file option where you choose the playlist you wanna run.
+
+All the playlists should be placed in ext/apps_data/nfc_playlist and an example of how the data in the file should look can be found below.
+```txt
+/ext/nfc/link.nfc
+/ext/nfc/link2.nfc
+```
+An example file can be found in the repository
+## How to build
+This app was design, built and tested using the <a href="https://github.com/Next-Flip/Momentum-Firmware">Momentum</a> so keep that in mind when building the FAP for yourself
+## Supported Firmwares
+As i know these firmwares are supported and working if you know any more please let me know
+- <a href="https://github.com/Flipper-XFW/Xtreme-Firmware">Xtreme</a>
+- <a href="https://github.com/Next-Flip/Momentum-Firmware">Momentum</a>
+- <a href="https://github.com/RogueMaster/flipperzero-firmware-wPlugins">RogueMaster</a>
+## Settings:
+- Emulate time (How long the NFC card will be emulated for)
+- Delay time (How long the gap between the cards will be)
+- LED indicator (Whether or not the LED's will be on)
+- Skip errors (Makes it so you can make the emulation screen hide errors and skip delays between errors and emulation)
+- Reset settings (Puts all the settings back to the defaults)
+## Playlist editor:
+- Create PLaylist (Creates a new playlist with the given name)
+- Delete playlist (Deletes the selected playlist)
+- Rename playlist (Renames the selected playlist to the new name provided)
+- View playlist content (Allows you to view the contents of the playlist)
+- Add NFC Item (Adds the selected nfc item to the currently selected playlist)
+## Development plans/ideas:
+Things i would like to add:
+- Ability to remove cards from the playlist
+
+These features are not guaranteed to be added but are being looked at as features to add
+
+Any feedback is welcome and would be very much appreciated

+ 18 - 0
nfc_playlist/application.fam

@@ -0,0 +1,18 @@
+App(
+    appid="nfc_playlist",
+    name="NFC Playlist",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="nfc_playlist_main",
+    requires=["gui", "nfc"],
+    stack_size=4 * 1024,
+    fap_category="NFC",
+    fap_author="@acegoal07",
+    fap_weburl="https://github.com/acegoal07/FlipperZero_NFC_Playlist/tree/main",
+    fap_version="1.8",
+    fap_icon="assets/icon.png",
+    fap_private_libs=[
+        Lib(
+            name="worker",
+        ),
+    ],
+)

BIN
nfc_playlist/assets/icon.png


+ 73 - 0
nfc_playlist/lib/worker/nfc_playlist_worker.c

@@ -0,0 +1,73 @@
+#include "nfc_playlist_worker.h"
+
+NfcPlaylistWorker* nfc_playlist_worker_alloc() {
+   NfcPlaylistWorker* nfc_playlist_worker = malloc(sizeof(NfcPlaylistWorker));
+   nfc_playlist_worker->thread = furi_thread_alloc_ex("NfcPlaylistWorker", 8192, nfc_playlist_worker_task, nfc_playlist_worker);
+   nfc_playlist_worker->state = NfcPlaylistWorkerState_Stopped;
+   nfc_playlist_worker->nfc = nfc_alloc();
+   nfc_playlist_worker->nfc_device = nfc_device_alloc();
+   return nfc_playlist_worker;
+}
+
+void nfc_playlist_worker_free(NfcPlaylistWorker* nfc_playlist_worker) {
+   furi_assert(nfc_playlist_worker);
+   furi_thread_free(nfc_playlist_worker->thread);
+   nfc_free(nfc_playlist_worker->nfc);
+   nfc_device_free(nfc_playlist_worker->nfc_device);
+   free(nfc_playlist_worker);
+}
+
+void nfc_playlist_worker_stop(NfcPlaylistWorker* nfc_playlist_worker) {
+   furi_assert(nfc_playlist_worker);
+   if (nfc_playlist_worker->state != NfcPlaylistWorkerState_Stopped) {
+      nfc_playlist_worker->state = NfcPlaylistWorkerState_Stopped;
+      furi_thread_join(nfc_playlist_worker->thread);
+   }
+}
+
+void nfc_playlist_worker_start(NfcPlaylistWorker* nfc_playlist_worker) {
+   furi_assert(nfc_playlist_worker);
+   nfc_playlist_worker->state = NfcPlaylistWorkerState_Emulating;
+   furi_thread_start(nfc_playlist_worker->thread);
+}
+
+int32_t nfc_playlist_worker_task(void* context) {
+   NfcPlaylistWorker* nfc_playlist_worker = context;
+
+   if (nfc_playlist_worker->state == NfcPlaylistWorkerState_Emulating) {
+
+      nfc_playlist_worker->nfc_listener =
+         nfc_listener_alloc(nfc_playlist_worker->nfc,
+            nfc_playlist_worker->nfc_protocol,
+            nfc_device_get_data(nfc_playlist_worker->nfc_device, nfc_playlist_worker->nfc_protocol)
+         );
+      nfc_listener_start(nfc_playlist_worker->nfc_listener, NULL, NULL);
+
+      while(nfc_playlist_worker->state == NfcPlaylistWorkerState_Emulating) {
+         furi_delay_ms(50);
+      }
+
+      nfc_listener_stop(nfc_playlist_worker->nfc_listener);
+      nfc_listener_free(nfc_playlist_worker->nfc_listener);
+   }
+
+   nfc_playlist_worker->state = NfcPlaylistWorkerState_Stopped;
+
+   return 0;
+}
+
+bool nfc_playlist_worker_is_emulating(NfcPlaylistWorker* nfc_playlist_worker) {
+   furi_assert(nfc_playlist_worker);
+   return nfc_playlist_worker->state == NfcPlaylistWorkerState_Emulating;
+}
+
+void nfc_playlist_worker_set_nfc_data(NfcPlaylistWorker* nfc_playlist_worker, char* file_path) {
+   furi_assert(nfc_playlist_worker);
+   nfc_device_load(nfc_playlist_worker->nfc_device, file_path);
+   nfc_playlist_worker->nfc_protocol = nfc_device_get_protocol(nfc_playlist_worker->nfc_device);
+}
+
+void nfc_playlist_worker_clear_nfc_data(NfcPlaylistWorker* nfc_playlist_worker) {
+   furi_assert(nfc_playlist_worker);
+   nfc_device_clear(nfc_playlist_worker->nfc_device);
+}

+ 31 - 0
nfc_playlist/lib/worker/nfc_playlist_worker.h

@@ -0,0 +1,31 @@
+#pragma once
+#include <furi.h>
+#include <furi_hal.h>
+#include <nfc/nfc.h>
+#include <nfc/nfc_device.h>
+#include <nfc/nfc_listener.h>
+
+typedef enum NfcPlaylistWorkerState {
+   NfcPlaylistWorkerState_Emulating,
+   NfcPlaylistWorkerState_Stopped
+} NfcPlaylistWorkerState;
+
+typedef struct NfcPlaylistWorker {
+   FuriThread* thread;
+   NfcPlaylistWorkerState state;
+   NfcListener* nfc_listener;
+   NfcDevice* nfc_device;
+   NfcProtocol nfc_protocol;
+   Nfc* nfc;
+} NfcPlaylistWorker;
+
+NfcPlaylistWorker* nfc_playlist_worker_alloc();
+void nfc_playlist_worker_free(NfcPlaylistWorker* nfc_playlist_worker);
+void nfc_playlist_worker_stop(NfcPlaylistWorker* nfc_playlist_worker);
+void nfc_playlist_worker_start(NfcPlaylistWorker* nfc_playlist_worker);
+
+int32_t nfc_playlist_worker_task(void* context);
+
+bool nfc_playlist_worker_is_emulating(NfcPlaylistWorker* nfc_playlist_worker);
+void nfc_playlist_worker_set_nfc_data(NfcPlaylistWorker* nfc_playlist_worker, char* file_path);
+void nfc_playlist_worker_clear_nfc_data(NfcPlaylistWorker* nfc_playlist_worker);

+ 150 - 0
nfc_playlist/nfc_playlist.c

@@ -0,0 +1,150 @@
+#include "nfc_playlist.h"
+
+static bool nfc_playlist_custom_callback(void* context, uint32_t custom_event) {
+   furi_assert(context);
+   NfcPlaylist* nfc_playlist = context;
+   return scene_manager_handle_custom_event(nfc_playlist->scene_manager, custom_event);
+}
+
+static bool nfc_playlist_back_event_callback(void* context) {
+   furi_assert(context);
+   NfcPlaylist* nfc_playlist = context;
+   return scene_manager_handle_back_event(nfc_playlist->scene_manager);
+}
+
+static NfcPlaylist* nfc_playlist_alloc() {
+   NfcPlaylist* nfc_playlist = malloc(sizeof(NfcPlaylist));
+   furi_assert(nfc_playlist);
+   nfc_playlist->scene_manager = scene_manager_alloc(&nfc_playlist_scene_handlers, nfc_playlist);
+   nfc_playlist->view_dispatcher = view_dispatcher_alloc();
+   view_dispatcher_enable_queue(nfc_playlist->view_dispatcher);
+   nfc_playlist->variable_item_list = variable_item_list_alloc();
+   nfc_playlist->submenu = submenu_alloc();
+   nfc_playlist->widget= widget_alloc();
+
+   nfc_playlist->settings.file_path = furi_string_alloc();
+   nfc_playlist->file_browser_output = furi_string_alloc();
+   nfc_playlist->settings.playlist_selected = false;
+   nfc_playlist->settings.emulate_timeout = default_emulate_timeout;
+   nfc_playlist->settings.emulate_delay = default_emulate_delay;
+   nfc_playlist->settings.emulate_led_indicator = default_emulate_led_indicator;
+   nfc_playlist->settings.skip_error = default_skip_error;
+
+   nfc_playlist->notification = furi_record_open(RECORD_NOTIFICATION);
+   nfc_playlist->file_browser = file_browser_alloc(nfc_playlist->file_browser_output);
+
+   nfc_playlist->text_input = text_input_alloc();
+   nfc_playlist->popup = popup_alloc();
+
+   view_dispatcher_set_event_callback_context(nfc_playlist->view_dispatcher, nfc_playlist);
+   view_dispatcher_set_custom_event_callback(nfc_playlist->view_dispatcher, nfc_playlist_custom_callback);
+   view_dispatcher_set_navigation_event_callback(nfc_playlist->view_dispatcher, nfc_playlist_back_event_callback);
+
+   view_dispatcher_add_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Submenu, submenu_get_view(nfc_playlist->submenu));
+   view_dispatcher_add_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Popup, popup_get_view(nfc_playlist->popup));
+   view_dispatcher_add_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Widget, widget_get_view(nfc_playlist->widget));
+   view_dispatcher_add_view(nfc_playlist->view_dispatcher, NfcPlaylistView_VariableItemList, variable_item_list_get_view(nfc_playlist->variable_item_list));
+   view_dispatcher_add_view(nfc_playlist->view_dispatcher, NfcPlaylistView_FileBrowser, file_browser_get_view(nfc_playlist->file_browser));
+   view_dispatcher_add_view(nfc_playlist->view_dispatcher, NfcPlaylistView_TextInput, text_input_get_view(nfc_playlist->text_input));
+
+   Storage* storage = furi_record_open(RECORD_STORAGE);
+   if (!storage_common_exists(storage, PLAYLIST_DIR)) {
+      storage_common_mkdir(storage, PLAYLIST_DIR);
+   }
+   furi_record_close(RECORD_STORAGE);
+
+   return nfc_playlist;
+}
+
+static void nfc_playlist_free(NfcPlaylist* nfc_playlist) {
+   furi_assert(nfc_playlist);
+
+   view_dispatcher_remove_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Submenu);
+   view_dispatcher_remove_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Popup);
+   view_dispatcher_remove_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Widget);
+   view_dispatcher_remove_view(nfc_playlist->view_dispatcher, NfcPlaylistView_VariableItemList);
+   view_dispatcher_remove_view(nfc_playlist->view_dispatcher, NfcPlaylistView_FileBrowser);
+   view_dispatcher_remove_view(nfc_playlist->view_dispatcher, NfcPlaylistView_TextInput);
+
+   scene_manager_free(nfc_playlist->scene_manager);
+   view_dispatcher_free(nfc_playlist->view_dispatcher);
+   variable_item_list_free(nfc_playlist->variable_item_list);
+   submenu_free(nfc_playlist->submenu);
+   widget_free(nfc_playlist->widget);
+
+   furi_record_close(RECORD_NOTIFICATION);
+   file_browser_free(nfc_playlist->file_browser);
+   text_input_free(nfc_playlist->text_input);
+   popup_free(nfc_playlist->popup);
+
+   furi_string_free(nfc_playlist->settings.file_path);
+   furi_string_free(nfc_playlist->file_browser_output);
+   free(nfc_playlist);
+}
+
+void nfc_playlist_set_log_level() {
+#ifdef FURI_DEBUG
+   furi_log_set_level(FuriLogLevelTrace);
+#else
+   furi_log_set_level(FuriLogLevelInfo);
+#endif
+}
+
+int32_t nfc_playlist_main(void* p) {
+   UNUSED(p);
+
+   NfcPlaylist* nfc_playlist = nfc_playlist_alloc();
+
+   nfc_playlist_set_log_level();
+
+   Gui* gui = furi_record_open(RECORD_GUI);
+   view_dispatcher_attach_to_gui(nfc_playlist->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+   scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_MainMenu);
+   view_dispatcher_run(nfc_playlist->view_dispatcher);
+
+   furi_record_close(RECORD_GUI);
+   nfc_playlist_free(nfc_playlist);
+
+   return 0;
+}
+
+NotificationMessage blink_message_normal = {
+   .type = NotificationMessageTypeLedBlinkStart,
+   .data.led_blink.color = LightBlue | LightGreen,
+   .data.led_blink.on_time = 10,
+   .data.led_blink.period = 100
+};
+const NotificationSequence blink_sequence_normal = {
+   &blink_message_normal,
+   &message_do_not_reset,
+   NULL
+};
+
+NotificationMessage blink_message_error = {
+   .type = NotificationMessageTypeLedBlinkStart,
+   .data.led_blink.color = LightRed,
+   .data.led_blink.on_time = 10,
+   .data.led_blink.period = 100
+};
+
+const NotificationSequence blink_sequence_error = {
+   &blink_message_error,
+   &message_do_not_reset,
+   NULL
+};
+
+void start_blink(NfcPlaylist* nfc_playlist, int state) {
+   if (nfc_playlist->settings.emulate_led_indicator) {
+      if (state == NfcPlaylistLedState_Normal) {
+         notification_message_block(nfc_playlist->notification, &blink_sequence_normal);
+      } else if (state == NfcPlaylistLedState_Error) {
+         notification_message_block(nfc_playlist->notification, &blink_sequence_error);
+      }
+   }
+}
+
+void stop_blink(NfcPlaylist* nfc_playlist) {
+   if (nfc_playlist->settings.emulate_led_indicator) {
+      notification_message_block(nfc_playlist->notification, &sequence_blink_stop);
+   }
+}

+ 84 - 0
nfc_playlist/nfc_playlist.h

@@ -0,0 +1,84 @@
+#pragma once
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <string.h>
+
+#include <assets_icons.h>
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/file_browser.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/widget_elements/widget_element.h>
+
+#include <notification/notification_messages.h>
+
+#include <storage/storage.h>
+
+#include <toolbox/stream/stream.h>
+#include <toolbox/stream/file_stream.h>
+
+#include "lib/worker/nfc_playlist_worker.h"
+
+#include "scenes/nfc_playlist_scene.h"
+
+typedef enum {
+   NfcPlaylistView_Submenu,
+   NfcPlaylistView_Popup,
+   NfcPlaylistView_Widget,
+   NfcPlaylistView_VariableItemList,
+   NfcPlaylistView_FileBrowser,
+   NfcPlaylistView_TextInput
+} NfcPlaylistViews;
+
+typedef struct {
+   FuriString* file_path;
+   bool playlist_selected;
+   uint8_t emulate_timeout;
+   uint8_t emulate_delay;
+   bool emulate_led_indicator;
+   bool skip_error;
+} NfcPlaylistSettings;
+
+typedef struct {
+   SceneManager* scene_manager;
+   ViewDispatcher* view_dispatcher;
+   Submenu* submenu;
+   Popup* popup;
+   Widget* widget;
+   VariableItemList* variable_item_list;
+   FileBrowser* file_browser;
+   FuriString* file_browser_output;
+   TextInput* text_input;
+   char* text_input_output;
+   NotificationApp* notification;
+   FuriThread* thread;
+   FuriString* temp_furi_string;
+   NfcPlaylistWorker* nfc_playlist_worker;
+   NfcPlaylistSettings settings;
+} NfcPlaylist;
+
+static const int options_emulate_timeout[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
+static const int default_emulate_timeout = 4;
+static const int options_emulate_delay[] = {0, 1, 2, 3, 4, 5, 6};
+static const int default_emulate_delay = 0;
+static const bool default_emulate_led_indicator = true;
+static const bool default_skip_error = false;
+
+#define PLAYLIST_LOCATION "/ext/apps_data/nfc_playlist/"
+#define PLAYLIST_DIR "/ext/apps_data/nfc_playlist"
+#define PLAYLIST_VIEW_MAX_SIZE 1000
+
+typedef enum NfcPlaylistLedState {
+   NfcPlaylistLedState_Normal,
+   NfcPlaylistLedState_Error
+} NfcPlaylistLedState;
+
+void start_blink(NfcPlaylist* nfc_playlist, int state);
+void stop_blink(NfcPlaylist* nfc_playlist);

+ 3 - 0
nfc_playlist/playlist.txt

@@ -0,0 +1,3 @@
+/ext/nfc/RickRoll.nfc
+/ext/nfc/Website.nfc
+/ext/nfc/Phone_number.nfc

+ 30 - 0
nfc_playlist/scenes/nfc_playlist_scene.c

@@ -0,0 +1,30 @@
+#include "nfc_playlist_scene.h"
+
+// Generate scene on_enter handlers definition
+#define ADD_SCENE(prefix, name, id) prefix##_##name##_scene_on_enter,
+void (*const nfc_playlist_on_enter_handlers[])(void*) = {
+#include "nfc_playlist_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_##name##_scene_on_event,
+bool (*const nfc_playlist_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "nfc_playlist_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_##name##_scene_on_exit,
+void (*const nfc_playlist_on_exit_handlers[])(void* context) = {
+#include "nfc_playlist_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers nfc_playlist_scene_handlers = {
+   .on_enter_handlers = nfc_playlist_on_enter_handlers,
+   .on_event_handlers = nfc_playlist_on_event_handlers,
+   .on_exit_handlers = nfc_playlist_on_exit_handlers,
+   .scene_num = NfcPlaylistScene_Count,
+};

+ 29 - 0
nfc_playlist/scenes/nfc_playlist_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) NfcPlaylistScene_##id,
+typedef enum {
+#include "nfc_playlist_scene_config.h"
+   NfcPlaylistScene_Count
+} NfcPlaylistScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers nfc_playlist_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_##name##_scene_on_enter(void*);
+#include "nfc_playlist_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+   bool prefix##_##name##_scene_on_event(void* context, SceneManagerEvent event);
+#include "nfc_playlist_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_##name##_scene_on_exit(void* context);
+#include "nfc_playlist_scene_config.h"
+#undef ADD_SCENE

+ 10 - 0
nfc_playlist/scenes/nfc_playlist_scene_config.h

@@ -0,0 +1,10 @@
+ADD_SCENE(nfc_playlist, confirm_delete, ConfirmDelete)
+ADD_SCENE(nfc_playlist, emulation, Emulation)
+ADD_SCENE(nfc_playlist, file_edit, FileEdit)
+ADD_SCENE(nfc_playlist, file_rename, FileRename)
+ADD_SCENE(nfc_playlist, main_menu, MainMenu)
+ADD_SCENE(nfc_playlist, name_new_file, NameNewFile)
+ADD_SCENE(nfc_playlist, nfc_select, NfcSelect)
+ADD_SCENE(nfc_playlist, playlist_select, PlaylistSelect)
+ADD_SCENE(nfc_playlist, settings, Settings)
+ADD_SCENE(nfc_playlist, view_playlist_content, ViewPlaylistContent)

+ 51 - 0
nfc_playlist/scenes/nfc_playlist_scene_confirm_delete.c

@@ -0,0 +1,51 @@
+#include "../nfc_playlist.h"
+
+void nfc_playlist_confirm_delete_menu_callback(GuiButtonType result, InputType type, void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   if(type == InputTypeShort) {
+      view_dispatcher_send_custom_event(nfc_playlist->view_dispatcher, result);
+   }
+}
+
+void nfc_playlist_confirm_delete_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+
+   FuriString* temp_str = furi_string_alloc();
+   char* file_path = (char*)furi_string_get_cstr(nfc_playlist->settings.file_path);
+   furi_string_printf(temp_str, "\e#Delete %s?\e#", strchr(file_path, '/') != NULL ? &strrchr(file_path, '/')[1] : file_path);
+   furi_string_replace(temp_str, ".txt", "");
+
+   widget_add_text_box_element(nfc_playlist->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, furi_string_get_cstr(temp_str), false);
+   widget_add_button_element(nfc_playlist->widget, GuiButtonTypeLeft, "Cancel", nfc_playlist_confirm_delete_menu_callback, nfc_playlist);
+   widget_add_button_element(nfc_playlist->widget, GuiButtonTypeRight, "Delete", nfc_playlist_confirm_delete_menu_callback, nfc_playlist);
+
+   furi_string_free(temp_str);
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Widget);
+}
+
+bool nfc_playlist_confirm_delete_scene_on_event(void* context, SceneManagerEvent event) {
+   NfcPlaylist* nfc_playlist = context;
+   bool consumed = false;
+   if(event.type == SceneManagerEventTypeCustom) {
+      switch(event.event) {
+         case GuiButtonTypeRight:
+            Storage* storage = furi_record_open(RECORD_STORAGE);
+            storage_simply_remove(storage, furi_string_get_cstr(nfc_playlist->settings.file_path));
+            nfc_playlist->settings.playlist_selected = false;
+            furi_string_reset(nfc_playlist->settings.file_path);
+            furi_record_close(RECORD_STORAGE);
+            consumed = true;
+            break;
+         default:
+            break;
+      }
+      scene_manager_previous_scene(nfc_playlist->scene_manager);
+   }
+   return consumed;
+}
+
+void nfc_playlist_confirm_delete_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   widget_reset(nfc_playlist->widget);
+}

+ 169 - 0
nfc_playlist/scenes/nfc_playlist_scene_emulation.c

@@ -0,0 +1,169 @@
+#include "../nfc_playlist.h"
+
+typedef enum NfcPlaylistEmulationState {
+   NfcPlaylistEmulationState_Emulating,
+   NfcPlaylistEmulationState_Stopped,
+   NfcPlaylistEmulationState_Canceled
+} NfcPlaylistEmulationState;
+
+NfcPlaylistEmulationState EmulationState = NfcPlaylistEmulationState_Stopped;
+
+int32_t nfc_playlist_emulation_task(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+
+   Storage* storage = furi_record_open(RECORD_STORAGE);
+   Stream* stream = file_stream_alloc(storage);
+   bool skip_delay = false;
+
+   popup_reset(nfc_playlist->popup);
+   popup_set_context(nfc_playlist->popup, nfc_playlist);
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Popup);
+
+   if (file_stream_open(stream, furi_string_get_cstr(nfc_playlist->settings.file_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+      EmulationState = NfcPlaylistEmulationState_Emulating;
+      int file_position = 0;
+
+      FuriString* line = furi_string_alloc();
+      FuriString* tmp_header_str = furi_string_alloc();
+      FuriString* tmp_counter_str = furi_string_alloc();
+
+      while(stream_read_line(stream, line) && EmulationState == NfcPlaylistEmulationState_Emulating) {
+
+         char* file_path = (char*)furi_string_get_cstr(line);
+
+         if (strlen(file_path) <= 1) {continue;}
+
+         if (nfc_playlist->settings.emulate_delay > 0 && file_position != 0 && !skip_delay) {
+            popup_set_header(nfc_playlist->popup, "Delaying", 64, 10, AlignCenter, AlignTop);
+            start_blink(nfc_playlist, NfcPlaylistLedState_Error);
+            int time_counter_delay_ms = (options_emulate_delay[nfc_playlist->settings.emulate_delay]*1000);
+            while(time_counter_delay_ms > 0 && EmulationState == NfcPlaylistEmulationState_Emulating) {
+               furi_string_printf(tmp_counter_str,  "%ds", (time_counter_delay_ms/1000));
+               popup_set_text(nfc_playlist->popup, furi_string_get_cstr(tmp_counter_str), 64, 50, AlignCenter, AlignTop);
+               furi_delay_ms(50);
+               time_counter_delay_ms -= 50;
+            };
+         } else if (nfc_playlist->settings.emulate_delay > 0) {
+            skip_delay = false;
+            file_position++;
+         }
+
+         if(EmulationState != NfcPlaylistEmulationState_Emulating) {break;}
+
+         char* file_name = strchr(file_path, '/') != NULL ? &strrchr(file_path, '/')[1] : file_path;
+         char const* file_ext = &strrchr(file_path, '.')[1];
+         int time_counter_ms = (options_emulate_timeout[nfc_playlist->settings.emulate_timeout]*1000);
+
+         if(!strcasestr(file_ext, "nfc")) {
+            if(nfc_playlist->settings.skip_error) {
+               skip_delay = true;
+               continue;
+            }
+            furi_string_printf(tmp_header_str, "ERROR invalid file:\n%s", file_name);
+            popup_set_header(nfc_playlist->popup, furi_string_get_cstr(tmp_header_str), 64, 10, AlignCenter, AlignTop);
+            start_blink(nfc_playlist, NfcPlaylistLedState_Error);
+            while(time_counter_ms > 0 && EmulationState == NfcPlaylistEmulationState_Emulating) {
+               furi_string_printf(tmp_counter_str, "%ds", (time_counter_ms/1000));
+               popup_set_text(nfc_playlist->popup, furi_string_get_cstr(tmp_counter_str), 64, 50, AlignCenter, AlignTop);
+               furi_delay_ms(50);
+               time_counter_ms -= 50;
+            };
+         } else if(!storage_file_exists(storage, file_path)) {
+            if(nfc_playlist->settings.skip_error) {
+               skip_delay = true;
+               continue;
+            }
+            furi_string_printf(tmp_header_str, "ERROR not found:\n%s", file_name);
+            popup_set_header(nfc_playlist->popup, furi_string_get_cstr(tmp_header_str), 64, 10, AlignCenter, AlignTop);
+            start_blink(nfc_playlist, NfcPlaylistLedState_Error);
+            while(time_counter_ms > 0 && EmulationState == NfcPlaylistEmulationState_Emulating) {
+               furi_string_printf(tmp_counter_str, "%ds", (time_counter_ms/1000));
+               popup_set_text(nfc_playlist->popup, furi_string_get_cstr(tmp_counter_str), 64, 50, AlignCenter, AlignTop);
+               furi_delay_ms(50);
+               time_counter_ms -= 50;
+            };
+         } else {
+            furi_string_printf(tmp_header_str, "Emulating:\n%s", file_name);
+            popup_set_header(nfc_playlist->popup, furi_string_get_cstr(tmp_header_str), 64, 10, AlignCenter, AlignTop);
+            nfc_playlist_worker_set_nfc_data(nfc_playlist->nfc_playlist_worker, file_path);
+            nfc_playlist_worker_start(nfc_playlist->nfc_playlist_worker);
+            start_blink(nfc_playlist, NfcPlaylistLedState_Normal);
+            while(nfc_playlist_worker_is_emulating(nfc_playlist->nfc_playlist_worker) && time_counter_ms > 0 && EmulationState == NfcPlaylistEmulationState_Emulating) {
+               furi_string_printf(tmp_counter_str, "%ds", (time_counter_ms/1000));
+               popup_set_text(nfc_playlist->popup, furi_string_get_cstr(tmp_counter_str), 64, 50, AlignCenter, AlignTop);
+               furi_delay_ms(50);
+               time_counter_ms -= 50;
+            };
+            nfc_playlist_worker_stop(nfc_playlist->nfc_playlist_worker);
+            nfc_playlist_worker_clear_nfc_data(nfc_playlist->nfc_playlist_worker);
+         }
+      }
+      popup_reset(nfc_playlist->popup);
+      popup_set_header(nfc_playlist->popup, EmulationState == NfcPlaylistEmulationState_Canceled ? "Emulation stopped" : "Emulation finished", 64, 10, AlignCenter, AlignTop);
+      popup_set_text(nfc_playlist->popup, "Press back", 64, 50, AlignCenter, AlignTop);
+      stop_blink(nfc_playlist);
+
+      EmulationState = NfcPlaylistEmulationState_Stopped;
+      furi_string_free(line);
+      furi_string_free(tmp_header_str);
+      furi_string_free(tmp_counter_str);
+   } else {
+      popup_set_header(nfc_playlist->popup, "Failed to open playlist", 64, 10, AlignCenter, AlignTop);
+      popup_set_text(nfc_playlist->popup, "Press back", 64, 50, AlignCenter, AlignTop);
+   }
+
+   file_stream_close(stream);
+   furi_record_close(RECORD_STORAGE);
+   stream_free(stream);
+
+   return 0;
+}
+
+void nfc_playlist_emulation_setup(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   nfc_playlist->thread = furi_thread_alloc_ex("NfcPlaylistEmulationWorker", 8192, nfc_playlist_emulation_task, nfc_playlist);
+   nfc_playlist->nfc_playlist_worker = nfc_playlist_worker_alloc();
+}
+
+void nfc_playlist_emulation_free(NfcPlaylist* nfc_playlist) {
+   furi_assert(nfc_playlist);
+   furi_thread_free(nfc_playlist->thread);
+   nfc_playlist_worker_free(nfc_playlist->nfc_playlist_worker);
+   nfc_playlist->thread = NULL;
+   nfc_playlist->nfc_playlist_worker = NULL;
+}
+
+void nfc_playlist_emulation_start(NfcPlaylist* nfc_playlist) {
+   furi_assert(nfc_playlist);
+   furi_thread_start(nfc_playlist->thread);
+}
+
+void nfc_playlist_emulation_stop(NfcPlaylist* nfc_playlist) {
+   furi_assert(nfc_playlist);
+   furi_thread_join(nfc_playlist->thread);
+}
+
+void nfc_playlist_emulation_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   nfc_playlist_emulation_setup(nfc_playlist);
+   nfc_playlist_emulation_start(nfc_playlist);
+}
+
+bool nfc_playlist_emulation_scene_on_event(void* context, SceneManagerEvent event) {
+   UNUSED(context);
+   bool consumed = false;
+   if(event.event == 0 && EmulationState == NfcPlaylistEmulationState_Emulating) {
+      EmulationState = NfcPlaylistEmulationState_Canceled;
+      consumed = true;
+   }
+   return consumed;
+}
+
+void nfc_playlist_emulation_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   EmulationState = NfcPlaylistEmulationState_Stopped;
+   nfc_playlist_emulation_stop(nfc_playlist);
+   nfc_playlist_emulation_free(nfc_playlist);
+   popup_reset(nfc_playlist->popup);
+}

+ 102 - 0
nfc_playlist/scenes/nfc_playlist_scene_file_edit.c

@@ -0,0 +1,102 @@
+#include "../nfc_playlist.h"
+
+typedef enum {
+   NfcPlaylistMenuSelection_CreatePlaylist,
+   NfcPlaylistMenuSelection_DeletePlaylist,
+   NfcPlaylistMenuSelection_RenamePlaylist,
+   NfcPlaylistMenuSelection_ViewPlaylistContent,
+   NfcPlaylistMenuSelection_AddNfcItem
+} NfcPlaylistFileEditMenuSelection;
+
+void nfc_playlist_file_edit_menu_callback(void* context, uint32_t index) {
+   NfcPlaylist* nfc_playlist = context;
+   scene_manager_handle_custom_event(nfc_playlist->scene_manager, index);
+}
+
+void nfc_playlist_file_edit_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+
+   submenu_set_header(nfc_playlist->submenu, "Edit Playlist");
+
+   submenu_add_item(
+      nfc_playlist->submenu,
+      "Create Playlist",
+      NfcPlaylistMenuSelection_CreatePlaylist,
+      nfc_playlist_file_edit_menu_callback,
+      nfc_playlist);
+
+   submenu_add_lockable_item(
+      nfc_playlist->submenu,
+      "Delete Playlist",
+      NfcPlaylistMenuSelection_DeletePlaylist,
+      nfc_playlist_file_edit_menu_callback,
+      nfc_playlist,
+      furi_string_empty(nfc_playlist->settings.file_path),
+      "No\nplaylist\nselected");
+
+   submenu_add_lockable_item(
+      nfc_playlist->submenu,
+      "Rename Playlist",
+      NfcPlaylistMenuSelection_RenamePlaylist,
+      nfc_playlist_file_edit_menu_callback,
+      nfc_playlist,
+      furi_string_empty(nfc_playlist->settings.file_path),
+      "No\nplaylist\nselected");
+
+   submenu_add_lockable_item(
+      nfc_playlist->submenu,
+      "View Playlist Content",
+      NfcPlaylistMenuSelection_ViewPlaylistContent,
+      nfc_playlist_file_edit_menu_callback,
+      nfc_playlist,
+      furi_string_empty(nfc_playlist->settings.file_path),
+      "No\nplaylist\nselected");
+
+   submenu_add_lockable_item(
+      nfc_playlist->submenu,
+      "Add NFC Item",
+      NfcPlaylistMenuSelection_AddNfcItem,
+      nfc_playlist_file_edit_menu_callback,
+      nfc_playlist,
+      furi_string_empty(nfc_playlist->settings.file_path),
+      "No\nplaylist\nselected");
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Submenu);
+}
+
+bool nfc_playlist_file_edit_scene_on_event(void* context, SceneManagerEvent event) {
+   NfcPlaylist* nfc_playlist = context;
+   bool consumed = false;
+   if(event.type == SceneManagerEventTypeCustom) {
+      switch(event.event) {
+         case NfcPlaylistMenuSelection_CreatePlaylist:
+            scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_NameNewFile);
+            consumed = true;
+            break;
+         case NfcPlaylistMenuSelection_DeletePlaylist:
+            scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_ConfirmDelete);
+            consumed = true;
+            break;
+         case NfcPlaylistMenuSelection_RenamePlaylist:
+            scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_FileRename);
+            consumed = true;
+            break;
+         case NfcPlaylistMenuSelection_ViewPlaylistContent:
+            scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_ViewPlaylistContent);
+            consumed = true;
+            break;
+         case NfcPlaylistMenuSelection_AddNfcItem:
+            scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_NfcSelect);
+            consumed = true;
+            break;
+         default:
+            break;
+      }
+   }
+   return consumed;
+}
+
+void nfc_playlist_file_edit_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   submenu_reset(nfc_playlist->submenu);
+}

+ 54 - 0
nfc_playlist/scenes/nfc_playlist_scene_file_rename.c

@@ -0,0 +1,54 @@
+#include "../nfc_playlist.h"
+
+void nfc_playlist_file_rename_menu_callback(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   Storage* storage = furi_record_open(RECORD_STORAGE);
+
+   char const* old_file_path = (char*)furi_string_get_cstr(nfc_playlist->settings.file_path);
+   char const* old_file_name = strchr(old_file_path, '/') != NULL ? &strrchr(old_file_path, '/')[1] : old_file_path;
+
+   FuriString* tmp_old_file_path = furi_string_alloc_set_str(old_file_path);
+   furi_string_replace(tmp_old_file_path, old_file_name, "");
+
+   FuriString* tmp_new_file_path = furi_string_alloc();
+   furi_string_printf(tmp_new_file_path, "%s%s.txt", furi_string_get_cstr(tmp_old_file_path), nfc_playlist->text_input_output);
+
+   if(!storage_file_exists(storage, furi_string_get_cstr(tmp_new_file_path))) {
+      storage_common_rename_safe(storage, furi_string_get_cstr(nfc_playlist->settings.file_path), furi_string_get_cstr(tmp_new_file_path));
+      nfc_playlist->settings.file_path = furi_string_alloc_set_str(furi_string_get_cstr(tmp_new_file_path));
+   }
+   furi_record_close(RECORD_STORAGE);
+   furi_string_free(tmp_new_file_path);
+   furi_string_free(tmp_old_file_path);
+
+   scene_manager_previous_scene(nfc_playlist->scene_manager);
+}
+
+void nfc_playlist_file_rename_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+
+   char const* tmp_file_path = (char*)furi_string_get_cstr(nfc_playlist->settings.file_path);
+   char const* tmp_file_name = strchr(tmp_file_path, '/') != NULL ? &strrchr(tmp_file_path, '/')[1] : tmp_file_path;
+
+   FuriString* tmp_file_name_furi = furi_string_alloc_set_str(tmp_file_name);
+   furi_string_replace(tmp_file_name_furi, ".txt", "");
+
+   nfc_playlist->text_input_output = (char*)furi_string_get_cstr(tmp_file_name_furi);
+   text_input_set_header_text(nfc_playlist->text_input, "Enter new file name");
+   text_input_set_minimum_length(nfc_playlist->text_input, 1);
+   text_input_set_result_callback(nfc_playlist->text_input, nfc_playlist_file_rename_menu_callback, nfc_playlist, nfc_playlist->text_input_output, 50, false);
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_TextInput);
+}
+
+bool nfc_playlist_file_rename_scene_on_event(void* context, SceneManagerEvent event) {
+   UNUSED(context);
+   UNUSED(event);
+   return false;
+}
+
+void nfc_playlist_file_rename_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   text_input_reset(nfc_playlist->text_input);
+   free(nfc_playlist->text_input_output);
+}

+ 99 - 0
nfc_playlist/scenes/nfc_playlist_scene_main_menu.c

@@ -0,0 +1,99 @@
+#include "../nfc_playlist.h"
+
+typedef enum {
+   NfcPlaylistEvent_ShowEmulation,
+   NfcPlaylistEvent_ShowPlaylistSelect,
+   NfcPlaylistEvent_ShowFileEdit,
+   NfcPlaylistEvent_ShowSettings
+} NfcPlaylistMainMenuEvent;
+
+typedef enum {
+   NfcPlaylistMenuSelection_Start,
+   NfcPlaylistMenuSelection_PlaylistSelect,
+   NfcPlaylistMenuSelection_FileEdit,
+   NfcPlaylistMenuSelection_Settings
+} NfcPlaylistMainMenuMenuSelection;
+
+void nfc_playlist_main_menu_menu_callback(void* context, uint32_t index) {
+   NfcPlaylist* nfc_playlist = context;
+   scene_manager_handle_custom_event(nfc_playlist->scene_manager, index);
+}
+
+void nfc_playlist_main_menu_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   if (!nfc_playlist->settings.playlist_selected) {
+      nfc_playlist->settings.playlist_selected = true;
+      scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_PlaylistSelect);
+      return;
+   }
+
+   FuriString* tmp_str = furi_string_alloc();
+   furi_string_printf(tmp_str, "NFC Playlist v%s", FAP_VERSION);
+   submenu_set_header(nfc_playlist->submenu, furi_string_get_cstr(tmp_str));
+   furi_string_free(tmp_str);
+
+   submenu_add_lockable_item(
+      nfc_playlist->submenu,
+      "Start",
+      NfcPlaylistMenuSelection_Start,
+      nfc_playlist_main_menu_menu_callback,
+      nfc_playlist,
+      furi_string_empty(nfc_playlist->settings.file_path),
+      "No\nplaylist\nselected");
+
+   submenu_add_item(
+      nfc_playlist->submenu,
+      "Select playlist",
+      NfcPlaylistMenuSelection_PlaylistSelect,
+      nfc_playlist_main_menu_menu_callback,
+      nfc_playlist);
+
+   submenu_add_item(
+      nfc_playlist->submenu,
+      "Edit playlist",
+      NfcPlaylistMenuSelection_FileEdit,
+      nfc_playlist_main_menu_menu_callback,
+      nfc_playlist);
+
+   submenu_add_item(
+      nfc_playlist->submenu,
+      "Settings",
+      NfcPlaylistMenuSelection_Settings,
+      nfc_playlist_main_menu_menu_callback,
+      nfc_playlist);
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Submenu);
+}
+
+bool nfc_playlist_main_menu_scene_on_event(void* context, SceneManagerEvent event) {
+   NfcPlaylist* nfc_playlist = context;
+   bool consumed = false;
+   if (event.type == SceneManagerEventTypeCustom) {
+      switch(event.event) {
+         case NfcPlaylistEvent_ShowEmulation:
+            scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_Emulation);
+            consumed = true;
+            break;
+         case NfcPlaylistEvent_ShowPlaylistSelect:
+            scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_PlaylistSelect);
+            consumed = true;
+            break;
+         case NfcPlaylistEvent_ShowFileEdit:
+            scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_FileEdit);
+            consumed = true;
+            break;
+         case NfcPlaylistEvent_ShowSettings:
+            scene_manager_next_scene(nfc_playlist->scene_manager, NfcPlaylistScene_Settings);
+            consumed = true;
+            break;
+         default:
+            break;
+      }
+   }
+   return consumed;
+}
+
+void nfc_playlist_main_menu_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   submenu_reset(nfc_playlist->submenu);
+}

+ 42 - 0
nfc_playlist/scenes/nfc_playlist_scene_name_new_file.c

@@ -0,0 +1,42 @@
+#include "../nfc_playlist.h"
+
+void nfc_playlist_name_new_file_menu_callback(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   Storage* storage = furi_record_open(RECORD_STORAGE);
+   FuriString* file_name = furi_string_alloc();
+
+   furi_string_printf(file_name, "/ext/apps_data/nfc_playlist/%s.txt", nfc_playlist->text_input_output);
+
+   File* file = storage_file_alloc(storage);
+   if (storage_file_open(file, furi_string_get_cstr(file_name), FSAM_READ_WRITE, FSOM_CREATE_NEW)) {
+      storage_file_close(file);
+      furi_string_swap(nfc_playlist->settings.file_path, file_name);
+   }
+
+   storage_file_free(file);
+   furi_string_free(file_name);
+   furi_record_close(RECORD_STORAGE);
+   scene_manager_previous_scene(nfc_playlist->scene_manager);
+}
+
+void nfc_playlist_name_new_file_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   nfc_playlist->text_input_output = (char*)malloc(50);
+   text_input_set_header_text(nfc_playlist->text_input, "Enter file name");
+   text_input_set_minimum_length(nfc_playlist->text_input, 1);
+   text_input_set_result_callback(nfc_playlist->text_input, nfc_playlist_name_new_file_menu_callback, nfc_playlist, nfc_playlist->text_input_output, 50, true);
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_TextInput);
+}
+
+bool nfc_playlist_name_new_file_scene_on_event(void* context, SceneManagerEvent event) {
+   UNUSED(context);
+   UNUSED(event);
+   return false;
+}
+
+void nfc_playlist_name_new_file_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   text_input_reset(nfc_playlist->text_input);
+   free(nfc_playlist->text_input_output);
+}

+ 64 - 0
nfc_playlist/scenes/nfc_playlist_scene_nfc_select.c

@@ -0,0 +1,64 @@
+#include "../nfc_playlist.h"
+
+void nfc_playlist_nfc_select_menu_callback(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+
+   Storage* storage = furi_record_open(RECORD_STORAGE);
+   File* file = storage_file_alloc(storage);
+
+   if (storage_file_open(file, furi_string_get_cstr(nfc_playlist->settings.file_path), FSAM_READ_WRITE, FSOM_OPEN_EXISTING)) {
+      uint8_t buffer[PLAYLIST_VIEW_MAX_SIZE];
+      uint16_t read_count = storage_file_read(file, buffer, PLAYLIST_VIEW_MAX_SIZE);
+      FuriString* playlist_content = furi_string_alloc();
+
+      for(uint16_t i = 0; i < read_count; i++) {
+         furi_string_push_back(playlist_content, buffer[i]);
+      }
+
+      if (read_count > 0) {
+         furi_string_printf(playlist_content, "\n%s", furi_string_get_cstr(nfc_playlist->file_browser_output));
+      } else {
+         furi_string_printf(playlist_content, "%s", furi_string_get_cstr(nfc_playlist->file_browser_output));
+      }
+
+      storage_file_write(file, furi_string_get_cstr(playlist_content), sizeof(char) * furi_string_utf8_length(playlist_content));
+
+      furi_string_free(playlist_content);
+      storage_file_close(file);
+   }
+
+   storage_file_free(file);
+   furi_record_close(RECORD_STORAGE);
+   furi_string_reset(nfc_playlist->file_browser_output);
+
+   scene_manager_previous_scene(nfc_playlist->scene_manager);
+}
+
+void nfc_playlist_nfc_select_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   file_browser_configure(
+      nfc_playlist->file_browser,
+      ".nfc",
+      "/ext/nfc/",
+      true,
+      true,
+      &I_Nfc_10px,
+      true);
+   file_browser_set_callback(nfc_playlist->file_browser, nfc_playlist_nfc_select_menu_callback, nfc_playlist);
+   FuriString* tmp_str = furi_string_alloc_set_str("/ext/nfc/");
+   file_browser_start(nfc_playlist->file_browser, tmp_str);
+   furi_string_free(tmp_str);
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_FileBrowser);
+}
+
+bool nfc_playlist_nfc_select_scene_on_event(void* context, SceneManagerEvent event) {
+   UNUSED(event);
+   UNUSED(context);
+   return false;
+}
+
+void nfc_playlist_nfc_select_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   file_browser_stop(nfc_playlist->file_browser);
+}

+ 37 - 0
nfc_playlist/scenes/nfc_playlist_scene_playlist_select.c

@@ -0,0 +1,37 @@
+#include "../nfc_playlist.h"
+
+void nfc_playlist_playlist_select_menu_callback(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   furi_string_swap(nfc_playlist->settings.file_path, nfc_playlist->file_browser_output);
+   furi_string_reset(nfc_playlist->file_browser_output);
+   scene_manager_previous_scene(nfc_playlist->scene_manager);
+}
+
+void nfc_playlist_playlist_select_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   file_browser_configure(
+      nfc_playlist->file_browser,
+      ".txt",
+      PLAYLIST_LOCATION,
+      true,
+      true,
+      &I_unknown_10px,
+      true);
+   file_browser_set_callback(nfc_playlist->file_browser, nfc_playlist_playlist_select_menu_callback, nfc_playlist);
+   FuriString* tmp_str = furi_string_alloc_set_str(PLAYLIST_LOCATION);
+   file_browser_start(nfc_playlist->file_browser, tmp_str);
+   furi_string_free(tmp_str);
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_FileBrowser);
+}
+
+bool nfc_playlist_playlist_select_scene_on_event(void* context, SceneManagerEvent event) {
+   UNUSED(event);
+   UNUSED(context);
+   return false;
+}
+
+void nfc_playlist_playlist_select_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   file_browser_stop(nfc_playlist->file_browser);
+}

+ 145 - 0
nfc_playlist/scenes/nfc_playlist_scene_settings.c

@@ -0,0 +1,145 @@
+#include "../nfc_playlist.h"
+
+typedef enum {
+   NfcPlaylistSettings_Timeout,
+   NfcPlaylistSettings_Delay,
+   NfcPlaylistSettings_LedIndicator,
+   NfcPlaylistSettings_SkipError,
+   NfcPlaylistSettings_Reset
+} NfcPlaylistSettingsMenuSelection;
+
+void nfc_playlist_settings_menu_callback(void* context, uint32_t index) {
+   NfcPlaylist* nfc_playlist = context;
+   scene_manager_handle_custom_event(nfc_playlist->scene_manager, index);
+}
+
+void nfc_playlist_settings_options_change_callback(VariableItem* item) {
+   NfcPlaylist* nfc_playlist = variable_item_get_context(item);
+
+   uint8_t current_option = variable_item_list_get_selected_item_index(nfc_playlist->variable_item_list);
+   uint8_t option_value_index = variable_item_get_current_value_index(item);
+   FuriString* tmp_str = furi_string_alloc();
+   switch(current_option) {
+      case NfcPlaylistSettings_Timeout:
+         nfc_playlist->settings.emulate_timeout = option_value_index;
+         furi_string_printf(tmp_str, "%ds", options_emulate_timeout[nfc_playlist->settings.emulate_timeout]);
+         variable_item_set_current_value_text(item, furi_string_get_cstr(tmp_str));
+         break;
+      case NfcPlaylistSettings_Delay:
+         nfc_playlist->settings.emulate_delay = option_value_index;
+         furi_string_printf(tmp_str, "%ds", options_emulate_delay[nfc_playlist->settings.emulate_delay]);
+         variable_item_set_current_value_text(item, furi_string_get_cstr(tmp_str));
+         break;
+      case NfcPlaylistSettings_LedIndicator:
+         nfc_playlist->settings.emulate_led_indicator = option_value_index;
+         variable_item_set_current_value_text(item, nfc_playlist->settings.emulate_led_indicator ? "ON" : "OFF");
+         break;
+      case NfcPlaylistSettings_SkipError:
+         nfc_playlist->settings.skip_error = option_value_index;
+         variable_item_set_current_value_text(item, nfc_playlist->settings.skip_error ? "ON" : "OFF");
+         break;
+      default:
+         break;
+   }
+   furi_string_free(tmp_str);
+}
+
+void nfc_playlist_settings_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   FuriString* tmp_str = furi_string_alloc();
+
+   variable_item_list_set_header(nfc_playlist->variable_item_list, "Settings");
+
+   VariableItem* emulation_timeout_setting = variable_item_list_add(
+      nfc_playlist->variable_item_list,
+      "Emulate time",
+      (sizeof(options_emulate_timeout)/sizeof(options_emulate_timeout[0])),
+      nfc_playlist_settings_options_change_callback,
+      nfc_playlist);
+   variable_item_set_current_value_index(emulation_timeout_setting, nfc_playlist->settings.emulate_timeout);
+   furi_string_printf(tmp_str, "%ds", options_emulate_timeout[nfc_playlist->settings.emulate_timeout]);
+   variable_item_set_current_value_text(emulation_timeout_setting, furi_string_get_cstr(tmp_str));
+
+   VariableItem* emulation_delay_setting = variable_item_list_add(
+      nfc_playlist->variable_item_list,
+      "Delay time",
+      (sizeof(options_emulate_delay)/sizeof(options_emulate_delay[0])),
+      nfc_playlist_settings_options_change_callback,
+      nfc_playlist);
+   variable_item_set_current_value_index(emulation_delay_setting, nfc_playlist->settings.emulate_delay);
+   furi_string_printf(tmp_str, "%ds", options_emulate_delay[nfc_playlist->settings.emulate_delay]);
+   variable_item_set_current_value_text(emulation_delay_setting, furi_string_get_cstr(tmp_str));
+
+   VariableItem* emulation_led_indicator_setting = variable_item_list_add(
+      nfc_playlist->variable_item_list,
+      "LED Indicator",
+      2,
+      nfc_playlist_settings_options_change_callback,
+      nfc_playlist);
+   variable_item_set_current_value_index(emulation_led_indicator_setting, nfc_playlist->settings.emulate_led_indicator);
+   variable_item_set_current_value_text(emulation_led_indicator_setting, nfc_playlist->settings.emulate_led_indicator ? "ON" : "OFF");
+
+   VariableItem* emulation_skip_error_setting = variable_item_list_add(
+      nfc_playlist->variable_item_list,
+      "Skip Error",
+      2,
+      nfc_playlist_settings_options_change_callback,
+      nfc_playlist);
+   variable_item_set_current_value_index(emulation_skip_error_setting, nfc_playlist->settings.skip_error);
+   variable_item_set_current_value_text(emulation_skip_error_setting, nfc_playlist->settings.skip_error ? "ON" : "OFF");
+
+   variable_item_list_add(nfc_playlist->variable_item_list, "Reset settings", 0, NULL, NULL);
+
+   VariableItem* credits = variable_item_list_add(nfc_playlist->variable_item_list, "acegoal07", 1, NULL, NULL);
+   variable_item_set_current_value_text(credits, "Credits");
+
+   variable_item_list_set_enter_callback(nfc_playlist->variable_item_list, nfc_playlist_settings_menu_callback, nfc_playlist);
+   furi_string_free(tmp_str);
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_VariableItemList);
+}
+
+bool nfc_playlist_settings_scene_on_event(void* context, SceneManagerEvent event) {
+   NfcPlaylist* nfc_playlist = context;
+   bool consumed = false;
+   if (event.type == SceneManagerEventTypeCustom) {
+      switch(event.event) {
+         case NfcPlaylistSettings_Reset:
+            FuriString* tmp_str = furi_string_alloc();
+
+            nfc_playlist->settings.emulate_timeout = default_emulate_timeout;
+            VariableItem* emulation_timeout_setting = variable_item_list_get(nfc_playlist->variable_item_list, NfcPlaylistSettings_Timeout);
+            variable_item_set_current_value_index(emulation_timeout_setting, nfc_playlist->settings.emulate_timeout);
+            furi_string_printf(tmp_str, "%ds", options_emulate_timeout[nfc_playlist->settings.emulate_timeout]);
+            variable_item_set_current_value_text(emulation_timeout_setting, furi_string_get_cstr(tmp_str));
+
+            nfc_playlist->settings.emulate_delay = default_emulate_delay;
+            VariableItem* emulation_delay_setting = variable_item_list_get(nfc_playlist->variable_item_list, NfcPlaylistSettings_Delay);
+            variable_item_set_current_value_index(emulation_delay_setting, nfc_playlist->settings.emulate_delay);
+            furi_string_printf(tmp_str, "%ds", options_emulate_delay[nfc_playlist->settings.emulate_delay]);
+            variable_item_set_current_value_text(emulation_delay_setting, furi_string_get_cstr(tmp_str));
+
+            nfc_playlist->settings.emulate_led_indicator = default_emulate_led_indicator;
+            VariableItem* emulation_led_indicator_setting = variable_item_list_get(nfc_playlist->variable_item_list, NfcPlaylistSettings_LedIndicator);
+            variable_item_set_current_value_index(emulation_led_indicator_setting, nfc_playlist->settings.emulate_led_indicator);
+            variable_item_set_current_value_text(emulation_led_indicator_setting, nfc_playlist->settings.emulate_led_indicator ? "ON" : "OFF");
+
+            nfc_playlist->settings.skip_error = default_skip_error;
+            VariableItem* emulation_skip_error_setting = variable_item_list_get(nfc_playlist->variable_item_list, NfcPlaylistSettings_SkipError);
+            variable_item_set_current_value_index(emulation_skip_error_setting, nfc_playlist->settings.skip_error);
+            variable_item_set_current_value_text(emulation_skip_error_setting, nfc_playlist->settings.skip_error ? "ON" : "OFF");
+
+            furi_string_free(tmp_str);
+            consumed = true;
+            break;
+         default:
+            break;
+      }
+   }
+   return consumed;
+}
+
+void nfc_playlist_settings_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   variable_item_list_reset(nfc_playlist->variable_item_list);
+}

+ 42 - 0
nfc_playlist/scenes/nfc_playlist_scene_view_playlist_content.c

@@ -0,0 +1,42 @@
+#include "../nfc_playlist.h"
+
+void nfc_playlist_view_playlist_content_scene_on_enter(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+
+   Storage* storage = furi_record_open(RECORD_STORAGE);
+   File* file = storage_file_alloc(storage);
+
+   if (storage_file_open(file, furi_string_get_cstr(nfc_playlist->settings.file_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+      uint8_t buffer[PLAYLIST_VIEW_MAX_SIZE];
+      uint16_t read_count = storage_file_read(file, buffer, PLAYLIST_VIEW_MAX_SIZE);
+      FuriString* playlist_content = furi_string_alloc();
+
+      for(uint16_t i = 0; i < read_count; i++) {
+         furi_string_push_back(playlist_content, buffer[i]);
+      }
+
+      widget_add_text_scroll_element(nfc_playlist->widget, 4, 4, 124, 60, furi_string_get_cstr(playlist_content));
+      widget_add_frame_element(nfc_playlist->widget, 0, 0, 128, 64, 0);
+
+      furi_string_free(playlist_content);
+      storage_file_close(file);
+   } else {
+      widget_add_text_box_element(nfc_playlist->widget, 0, 0, 128, 64, AlignCenter, AlignCenter, "\eFailed to open playlist\n\nPress back\e", false);
+   }
+
+   storage_file_free(file);
+   furi_record_close(RECORD_STORAGE);
+
+   view_dispatcher_switch_to_view(nfc_playlist->view_dispatcher, NfcPlaylistView_Widget);
+}
+
+bool nfc_playlist_view_playlist_content_scene_on_event(void* context, SceneManagerEvent event) {
+   UNUSED(context);
+   UNUSED(event);
+   return false;
+}
+
+void nfc_playlist_view_playlist_content_scene_on_exit(void* context) {
+   NfcPlaylist* nfc_playlist = context;
+   widget_reset(nfc_playlist->widget);
+}