From 73f7f847182641a15f1313e3857072541b6f4bf3 Mon Sep 17 00:00:00 2001 From: sommermorgentraum <24917424+zxkmm@users.noreply.github.com> Date: Wed, 19 Feb 2025 05:05:40 +0800 Subject: [PATCH] Playlist editor (#2506) * make both exist * format * fix focusing issue * add example hopper payload * fix compiler err * clean up * correct linker script addr * lint * PoC * unknown: write_line issue * clean up * merge * fix read line * remove debug code * fix english * support new file * support enter delay * fix crash * remove debug code * some final tune --- firmware/application/external/external.cmake | 7 +- firmware/application/external/external.ld | 7 + .../external/playlist_editor/main.cpp | 84 ++++ .../playlist_editor/ui_playlist_editor.cpp | 367 ++++++++++++++++++ .../playlist_editor/ui_playlist_editor.hpp | 159 ++++++++ firmware/application/file.cpp | 4 + 6 files changed, 627 insertions(+), 1 deletion(-) create mode 100644 firmware/application/external/playlist_editor/main.cpp create mode 100644 firmware/application/external/playlist_editor/ui_playlist_editor.cpp create mode 100644 firmware/application/external/playlist_editor/ui_playlist_editor.hpp diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 83700b7fc..f5c1425b7 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -166,6 +166,10 @@ set(EXTCPPSRC # wipe sdcard external/sd_wipe/main.cpp external/sd_wipe/ui_sd_wipe.cpp + + # playlist editor + external/playlist_editor/main.cpp + external/playlist_editor/ui_playlist_editor.cpp ) set(EXTAPPLIST @@ -209,4 +213,5 @@ set(EXTAPPLIST antenna_length view_wav sd_wipe -) \ No newline at end of file + playlist_editor +) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 37f4162e7..74d487a57 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -63,6 +63,7 @@ MEMORY ram_external_app_antenna_length(rwx) : org = 0xADD60000, len = 32k ram_external_app_view_wav(rwx) : org = 0xADD70000, len = 32k ram_external_app_sd_wipe(rwx) : org = 0xADD80000, len = 32k + ram_external_app_playlist_editor(rwx) : org = 0xADD90000, len = 32k } SECTIONS @@ -307,4 +308,10 @@ SECTIONS KEEP(*(.external_app.app_sd_wipe.application_information)); *(*ui*external_app*sd_wipe*); } > ram_external_app_sd_wipe + + .external_app_playlist_editor : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_playlist_editor.application_information)); + *(*ui*external_app*playlist_editor*); + } > ram_external_app_playlist_editor } diff --git a/firmware/application/external/playlist_editor/main.cpp b/firmware/application/external/playlist_editor/main.cpp new file mode 100644 index 000000000..3ffe23c5b --- /dev/null +++ b/firmware/application/external/playlist_editor/main.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_playlist_editor.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::playlist_editor { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::playlist_editor + +extern "C" { + +__attribute__((section(".external_app.app_playlist_editor.application_information"), used)) application_information_t _application_information_playlist_editor = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::playlist_editor::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "PlaylistEdit", + /*.bitmap_data = */ { + 0x03, + 0x00, + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x00, + 0x0F, + 0x00, + 0x00, + 0x00, + 0x03, + 0x01, + 0x80, + 0x01, + 0xC3, + 0x00, + 0xE0, + 0xFF, + 0xEF, + 0xFF, + 0xC0, + 0x00, + 0x83, + 0x01, + 0x00, + 0x01, + 0x03, + 0x00, + 0x00, + 0x00, + + }, + /*.icon_color = */ ui::Color::cyan().v, + /*.menu_location = */ app_location_t::UTILITIES, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_none */ {0, 0, 0, 0}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} \ No newline at end of file diff --git a/firmware/application/external/playlist_editor/ui_playlist_editor.cpp b/firmware/application/external/playlist_editor/ui_playlist_editor.cpp new file mode 100644 index 000000000..02f6921f4 --- /dev/null +++ b/firmware/application/external/playlist_editor/ui_playlist_editor.cpp @@ -0,0 +1,367 @@ +/* + * copyleft Elliot Alderson from F society + * copyleft Darlene Alderson from F society + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_playlist_editor.hpp" +#include "ui_navigation.hpp" +#include "ui_external_items_menu_loader.hpp" + +#include "file.hpp" +#include "ui_fileman.hpp" +#include "file_path.hpp" +#include "string_format.hpp" + +namespace fs = std::filesystem; + +#include "string_format.hpp" + +#include "file_reader.hpp" + +using namespace portapack; + +namespace ui::external_app::playlist_editor { + +/*********menu**********/ +PlaylistEditorView::PlaylistEditorView(NavigationView& nav) + : nav_{nav} { + portapack::async_tx_enabled = true; + add_children({&labels, + &button_new, + &text_current_ppl_file, + &menu_view, + &text_hint, + &button_open_playlist, + &button_edit, + &button_insert, + &button_save_playlist}); + + menu_view.set_parent_rect({0, 2 * 8, screen_width, 24 * 8}); + + menu_view.on_highlight = [this]() { + text_hint.set("Edit:" + + playlist[menu_view.highlighted_index()].substr(playlist[menu_view.highlighted_index()].find_last_of('/') + 1, + playlist[menu_view.highlighted_index()].find(',') - + playlist[menu_view.highlighted_index()].find_last_of('/') - 1)); + }; + + button_new.on_select = [this](Button&) { + if (on_create_ppl()) { + swap_opened_file_or_new_button(DisplayFilenameOrNewButton::DISPLAY_FILENAME); + refresh_interface(); + } + }; + + button_open_playlist.on_select = [this](Button&) { + open_file(); + }; + + button_edit.on_select = [this](Button&) { + on_edit_item(); + }; + + button_insert.on_select = [this](Button&) { + on_insert_item(); + }; + + button_save_playlist.on_select = [this](Button&) { + save_ppl(); + }; + + swap_opened_file_or_new_button(DisplayFilenameOrNewButton::DISPLAY_NEW_BUTTON); +} + +void PlaylistEditorView::focus() { + menu_view.focus(); +} + +void PlaylistEditorView::open_file() { + auto open_view = nav_.push(".PPL"); + open_view->push_dir(playlist_dir); + open_view->on_changed = [this](fs::path new_file_path) { + current_ppl_path = new_file_path; + on_file_changed(new_file_path); + }; +} + +void PlaylistEditorView::swap_opened_file_or_new_button(DisplayFilenameOrNewButton d) { + if (d == DisplayFilenameOrNewButton::DISPLAY_NEW_BUTTON) { + button_new.hidden(false); + text_current_ppl_file.hidden(true); + } else { + button_new.hidden(true); + text_current_ppl_file.hidden(false); + text_current_ppl_file.set(current_ppl_name_buffer); + } + refresh_interface(); +} + +/* +NB: same name would became as "open file" +*/ +bool PlaylistEditorView::on_create_ppl() { + bool success = false; + text_prompt( + nav_, + current_ppl_name_buffer, + 100, + [&](std::string& s) { + current_ppl_name_buffer = s; + + success = true; + current_ppl_name_buffer += ".PPL"; + current_ppl_path = playlist_dir / std::filesystem::path(current_ppl_name_buffer); + + File f; + f.open(current_ppl_path, true, true); // prob safer here as standalone obj as read only and then open again in process func + f.close(); + on_file_changed(current_ppl_path); + }); + + return success; +} + +void PlaylistEditorView::on_file_changed(const fs::path& new_file_path) { + File playlist_file; + auto error = playlist_file.open(new_file_path.string()); + + if (error) return; + + menu_view.clear(); + auto reader = FileLineReader(playlist_file); + + for (const auto& line : reader) { + playlist.push_back(line); + } + + for (auto& line : playlist) { + // remove empty lines + if (line == "\n" || line == "\r\n" || line == "\r") { + playlist.erase(std::remove(playlist.begin(), playlist.end(), line), playlist.end()); + } + + // remove line end \n etc + if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) { + line = line.substr(0, line.length() - 1); + } + } + text_hint.set("Highlight an entry"); + + text_current_ppl_file.set(new_file_path.string()); + + ever_opened = true; + + swap_opened_file_or_new_button(DisplayFilenameOrNewButton::DISPLAY_FILENAME); + + refresh_menu_view(); +} + +void PlaylistEditorView::refresh_menu_view() { + menu_view.clear(); + + for (const auto& line : playlist) { + if (line.length() == 0 || line[0] == '#') { + menu_view.add_item({line, + ui::Color::grey(), + &bitmap_icon_notepad, + [this](KeyEvent) { + button_insert.focus(); + }}); + } else { + const auto filename = line.substr(line.find_last_of('/') + 1, line.find(',') - line.find_last_of('/') - 1); + menu_view.add_item({filename, + ui::Color::white(), + &bitmap_icon_cwgen, + [this](KeyEvent) { + button_edit.focus(); + }}); + } + } +} + +void PlaylistEditorView::on_edit_item() { + if (!ever_opened || playlist.empty()) { + nav_.display_modal("Err", "No entry"); + return; + } + auto edit_view = nav_.push( + playlist[menu_view.highlighted_index()]); + + edit_view->set_on_delete([this]() { + playlist.erase(playlist.begin() + menu_view.highlighted_index()); + refresh_interface(); + }); + + edit_view->on_save = [this](std::string new_item) { + playlist[menu_view.highlighted_index()] = new_item; + refresh_interface(); + }; +} + +void PlaylistEditorView::on_insert_item() { + // if (current_ppl_path.empty() || current_ppl_path.string().find_first_not_of(" \t\n\r") == std::string::npos) { + if (!ever_opened) { // TODO: this is a workaround because the above line is not working and I took one hour and didn't find the issue + nav_.display_modal("Err", "No playlist file loaded"); + return; + } + + auto edit_view = nav_.push( + ""); + + edit_view->on_save = [&](std::string new_item) { + if (playlist.empty()) { + playlist.push_back(new_item); + } else { + playlist.insert(playlist.begin() + menu_view.highlighted_index() + 1, new_item); + } + refresh_interface(); + }; +} + +void PlaylistEditorView::refresh_interface() { + const auto previous_index = menu_view.highlighted_index(); + refresh_menu_view(); + set_dirty(); + menu_view.set_highlighted(previous_index); +} + +void PlaylistEditorView::save_ppl() { + if (current_ppl_path.empty()) { + nav_.display_modal("Err", "No playlist file loaded"); + return; + } else if (playlist.empty()) { + nav_.display_modal("Err", "List is empty"); + return; + } + + File playlist_file; + auto error = playlist_file.open(current_ppl_path.string(), false, false); + + if (error) { + nav_.display_modal("Err", "open err"); + return; + } + + // clear file + playlist_file.seek(0); + playlist_file.truncate(); + + // write new data + for (const auto& entry : playlist) { + playlist_file.write_line(entry); + } + + nav_.display_modal("Save", "Saved playlist\n" + current_ppl_path.string()); +} + +/*********edit**********/ + +PlaylistItemEditView::PlaylistItemEditView( + NavigationView& nav, + std::string item) + : nav_{nav}, + original_item_{item} { + add_children({&labels, + &field_path, + &field_delay, + &button_browse, + &button_input_delay, + &button_delete, + &button_save}); + + button_browse.on_select = [this, &nav](Button&) { + auto open_view = nav.push(".C16"); + open_view->push_dir(captures_dir); + open_view->on_changed = [this](fs::path path) { + field_path.set_text(path.string()); + path_ = path.string(); + }; + field_delay.on_change = [&](auto) { + delay_ = field_delay.value(); + }; + }; + + button_input_delay.on_select = [this](Button&) { + delay_str = to_string_dec_uint(delay_); + if (delay_str == "0") { + delay_str = ""; + } + text_prompt( + nav_, + delay_str, + 100, + [&](std::string& s) { + delay_ = atoi(s.c_str()); + field_delay.set_value(delay_); + refresh_ui(); + }); + }; + + button_delete.on_select = [this](Button&) { + if (on_delete) on_delete(); + nav_.pop(); + }; + + button_save.on_select = [&](Button&) { + if (path_.empty()) { + nav_.display_modal("Err", "Select a file\n or press back to cancel"); + return; + } + if (on_save) on_save(build_item()); + nav_.pop(); + }; + + if (!on_delete) { + button_delete.hidden(true); + } + + parse_item(item); + refresh_ui(); +} + +void PlaylistItemEditView::focus() { + button_save.focus(); +} + +void PlaylistItemEditView::refresh_ui() { + field_path.set_text(path_); + field_delay.set_value(delay_); +} + +void PlaylistItemEditView::parse_item(std::string item) { + // Parse format: path,delay + if (item.empty()) { + return; + } + auto parts = split_string(item, ','); + if (parts.size() >= 1) { + path_ = std::string{parts[0]}; + } + if (parts.size() >= 2) { + delay_ = atoi(std::string{parts[1]}.c_str()); + } +} + +std::string PlaylistItemEditView::build_item() const { + const auto v = path_ + "," + to_string_dec_uint(field_delay.value()); + return path_ + "," + to_string_dec_uint(field_delay.value()); +} + +} // namespace ui::external_app::playlist_editor \ No newline at end of file diff --git a/firmware/application/external/playlist_editor/ui_playlist_editor.hpp b/firmware/application/external/playlist_editor/ui_playlist_editor.hpp new file mode 100644 index 000000000..124f8fe6c --- /dev/null +++ b/firmware/application/external/playlist_editor/ui_playlist_editor.hpp @@ -0,0 +1,159 @@ +/* + * copyleft Elliot Alderson from F society + * copyleft Darlene Alderson from F society + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_PLAYLIST_EDITOR_H__ +#define __UI_PLAYLIST_EDITOR_H__ + +#include "ui_navigation.hpp" +namespace fs = std::filesystem; + +namespace ui::external_app::playlist_editor { + +enum DisplayFilenameOrNewButton { + DISPLAY_FILENAME, + DISPLAY_NEW_BUTTON +}; + +class PlaylistEditorView : public View { + public: + PlaylistEditorView(NavigationView& nav); + std::string title() const override { return "PPL Edit"; }; + + private: + NavigationView& nav_; + + void focus() override; + + std::vector playlist = {}; + fs::path current_ppl_path = ""; + std::string current_ppl_name_buffer = ""; // this is because text_prompt needs it. TODO: this is so annoying, shoudl refactor that func + bool ever_opened = false; + + Labels labels{ + {{0 * 8, 0 * 16}, "PPL file:", Theme::getInstance()->fg_light->foreground}}; + + Button button_new{ + {(sizeof("PPL file:") + 1) * 8, 0 * 16, 8 * 5, 16}, + "New"}; + + Text text_current_ppl_file{ + {sizeof("PPL file:") * 8, 0 * 16, screen_width - sizeof("PPL file:") * 8, 16}, + ""}; + + MenuView menu_view{}; + + Text text_hint{ + {0, 27 * 8, screen_width, 16}, + "Open a PPL file"}; + + Text text_ppl_name{ + {0, 27 * 8, screen_width, 16}, + "Highlight an app"}; + + Button button_open_playlist{ + {0, 29 * 8, screen_width / 2 - 1, 32}, + "Open PPL"}; + + Button button_edit{ + {screen_width / 2 + 2, 29 * 8, screen_width / 2 - 2, 32}, + "Edit Item"}; + + Button button_insert{ + {0, screen_height - 32 - 16, screen_width / 2 - 1, 32}, + "Ins. After"}; + + Button button_save_playlist{ + {screen_width / 2 + 2, screen_height - 32 - 16, screen_width / 2 - 2, 32}, + "Save PPL"}; + + void open_file(); + void swap_opened_file_or_new_button(DisplayFilenameOrNewButton d); + void on_file_changed(const fs::path& path); + bool on_create_ppl(); + void refresh_interface(); + void on_edit_item(); + void on_insert_item(); + void refresh_menu_view(); + void save_ppl(); +}; + +class PlaylistItemEditView : public View { + public: + std::function on_save{}; + std::function on_delete{}; + + PlaylistItemEditView(NavigationView& nav, std::string item); + + std::string title() const override { return "Edit Item"; }; + void focus() override; + + void set_on_delete(std::function callback) { + on_delete = callback; + button_delete.hidden(false); + } + + private: + NavigationView& nav_; + std::string original_item_ = ""; + std::string path_ = ""; + uint32_t delay_{0}; + std::string delay_str{""}; // needed by text_prompt + + void refresh_ui(); + void parse_item(std::string item); + std::string build_item() const; + + Labels labels{ + {{0 * 8, 1 * 16}, "Path:", Theme::getInstance()->fg_light->foreground}, + {{2 * 8, 5 * 16}, "Delay(ms):", Theme::getInstance()->fg_light->foreground}}; + + TextField field_path{ + {0, 2 * 16, screen_width, 16}, + "empty"}; + + NumberField field_delay{ + {11 * 8, 5 * 16}, + 5, + {0, 99999}, + 10, + ' '}; + + Button button_browse{ + {2 * 8, 8 * 16, 8 * 8, 3 * 16}, + "Browse"}; + + Button button_input_delay{ + {12 * 8, 8 * 16, sizeof("Input Delay") * 8, 3 * 16}, + "Input Delay"}; + + Button button_delete{ + {1, 17 * 16, screen_width / 2 - 4, 2 * 16}, + "Delete"}; + + Button button_save{ + {1 + screen_width / 2 + 1, 17 * 16, screen_width / 2 - 4, 2 * 16}, + "Save"}; +}; + +} // namespace ui::external_app::playlist_editor + +#endif // __UI_PLAYLIST_EDITOR_H__ \ No newline at end of file diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index dae33f01c..6da93056e 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -50,6 +50,10 @@ Optional File::open_fatfs(const std::filesystem::path& filename, BY } } +/* + * @param read_only: open in readonly mode + * @param create: create if it doesnt exist + */ Optional File::open(const std::filesystem::path& filename, bool read_only, bool create) { BYTE mode = read_only ? FA_READ : FA_READ | FA_WRITE; if (create)