From 4e985420d4494853e40c0d0bbd0a489f1c47923e Mon Sep 17 00:00:00 2001 From: Kyle Reed <3761006+kallanreed@users.noreply.github.com> Date: Wed, 28 Jun 2023 10:02:06 -0700 Subject: [PATCH] Replace Replay with Playlist (#1202) * Replace Replay with Playlist * Suggestions from test-drive. Launch from Fileman --- firmware/application/CMakeLists.txt | 2 +- firmware/application/apps/ui_fileman.cpp | 50 +++++++++-------------- firmware/application/apps/ui_playlist.cpp | 49 ++++++++++++++++++---- firmware/application/apps/ui_playlist.hpp | 27 ++++++------ firmware/application/file.cpp | 18 ++++++++ firmware/application/file.hpp | 3 ++ firmware/application/ui_navigation.cpp | 4 +- 7 files changed, 100 insertions(+), 53 deletions(-) diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 941ca62c..4c50ccfe 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -287,7 +287,7 @@ set(CPPSRC apps/ert_app.cpp apps/lge_app.cpp apps/pocsag_app.cpp - apps/replay_app.cpp + # apps/replay_app.cpp apps/ui_playlist.cpp apps/gps_sim_app.cpp apps/soundboard_app.cpp diff --git a/firmware/application/apps/ui_fileman.cpp b/firmware/application/apps/ui_fileman.cpp index c9c990be..8df0518e 100644 --- a/firmware/application/apps/ui_fileman.cpp +++ b/firmware/application/apps/ui_fileman.cpp @@ -26,6 +26,7 @@ #include #include "ui_fileman.hpp" +#include "ui_playlist.hpp" #include "ui_ss_viewer.hpp" #include "ui_text_editor.hpp" #include "string_format.hpp" @@ -35,6 +36,13 @@ using namespace portapack; namespace fs = std::filesystem; +namespace ui { +static const fs::path txt_ext{u".TXT"}; +static const fs::path ppl_ext{u".PPL"}; +static const fs::path c16_ext{u".C16"}; +static const fs::path png_ext{u".PNG"}; +} // namespace ui + namespace { using namespace ui; @@ -47,25 +55,6 @@ std::string truncate(const fs::path& path, size_t max_length) { return ::truncate(path.string(), max_length); } -// Case insensitive path equality on underlying "native" string. -bool iequal( - const fs::path& lhs, - const fs::path& rhs) { - const auto& lhs_str = lhs.native(); - const auto& rhs_str = rhs.native(); - - // NB: Not correct for Unicode/locales. - if (lhs_str.length() == rhs_str.length()) { - for (size_t i = 0; i < lhs_str.length(); ++i) - if (towupper(lhs_str[i]) != towupper(rhs_str[i])) - return false; - - return true; - } - - return false; -} - // Inserts the entry into the entry list sorted directories first then by file name. void insert_sorted(std::vector& entries, fileman_entry&& entry) { auto it = std::lower_bound( @@ -86,15 +75,12 @@ void insert_sorted(std::vector& entries, fileman_entry&& entry) { fs::path get_partner_file(fs::path path) { if (fs::is_directory(path)) return {}; - - const fs::path txt_path{u".TXT"}; - const fs::path c16_path{u".C16"}; auto ext = path.extension(); - if (iequal(ext, txt_path)) - ext = c16_path; - else if (iequal(ext, c16_path)) - ext = txt_path; + if (path_iequal(ext, txt_ext)) + ext = c16_ext; + else if (path_iequal(ext, c16_ext)) + ext = txt_ext; else return {}; @@ -163,7 +149,7 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) { continue; if (fs::is_regular_file(entry.status())) { - if (!filtering || iequal(entry.path().extension(), extension_filter)) + if (!filtering || path_iequal(entry.path().extension(), extension_filter)) insert_sorted(entry_list, {entry.path(), (uint32_t)entry.size(), false}); } else if (fs::is_directory(entry.status())) { insert_sorted(entry_list, {entry.path(), 0, true}); @@ -301,7 +287,7 @@ const FileManBaseView::file_assoc_t& FileManBaseView::get_assoc( size_t index = 0; for (; index < file_types.size() - 1; ++index) - if (iequal(ext, file_types[index].extension)) + if (path_iequal(ext, file_types[index].extension)) return file_types[index]; // Default to last entry in the list. @@ -507,10 +493,14 @@ bool FileManagerView::handle_file_open() { auto path = get_selected_full_path(); auto ext = path.extension(); - if (iequal(u".TXT", ext) || iequal(u".PPL", ext)) { + if (path_iequal(txt_ext, ext)) { nav_.push(path); return true; - } else if (iequal(u".PNG", ext)) { + } else if (path_iequal(c16_ext, ext) || path_iequal(ppl_ext, ext)) { + // TODO: Enough memory to push? + nav_.push(path); + return true; + } else if (path_iequal(png_ext, ext)) { nav_.push(path); return true; } diff --git a/firmware/application/apps/ui_playlist.cpp b/firmware/application/apps/ui_playlist.cpp index 466a067f..f74e18f0 100644 --- a/firmware/application/apps/ui_playlist.cpp +++ b/firmware/application/apps/ui_playlist.cpp @@ -42,10 +42,15 @@ namespace fs = std::filesystem; /* TODO * - Should frequency overrides be saved in the playlist? + * - Start playing from current track (vs start over)? */ namespace ui { +// TODO: consolidate extesions into a shared header? +static const fs::path c16_ext = u".C16"; +static const fs::path ppl_ext = u".PPL"; + void PlaylistView::load_file(const fs::path& playlist_path) { File playlist_file; auto error = playlist_file.open(playlist_path.string()); @@ -103,7 +108,6 @@ void PlaylistView::on_file_changed(const fs::path& new_file_path) { load_file(playlist_path_); update_ui(); - button_play.focus(); } void PlaylistView::open_file(bool prompt_save) { @@ -119,8 +123,10 @@ void PlaylistView::open_file(bool prompt_save) { } auto open_view = nav_.push(".PPL"); + open_view->push_dir(u"PLAYLIST"); open_view->on_changed = [this](fs::path new_file_path) { on_file_changed(new_file_path); + button_play.focus(); }; } @@ -156,10 +162,8 @@ void PlaylistView::save_file(bool show_dialogs) { } void PlaylistView::add_entry(fs::path&& path) { - if (playlist_path_.empty()) { + if (playlist_path_.empty()) playlist_path_ = next_filename_matching_pattern(u"/PLAYLIST/PLAY_????.PPL"); - button_play.focus(); - } auto entry = load_entry(std::move(path)); if (entry) { @@ -252,7 +256,7 @@ void PlaylistView::send_current_track() { return; } - // ReplayThread starts immediately on contruction so + // ReplayThread starts immediately on construction so // these need to be set before creating the ReplayThread. transmitter_model.set_target_frequency(current()->metadata.center_frequency); transmitter_model.set_sampling_rate(current()->metadata.sample_rate * 8); @@ -391,25 +395,40 @@ PlaylistView::PlaylistView( }; button_add.on_select = [this, &nav]() { + if (is_active()) + return; auto open_view = nav_.push(".C16"); + open_view->push_dir(u"CAPTURES"); open_view->on_changed = [this](fs::path path) { + // Set focus to play only on the first "add". + auto set_focus = playlist_path_.empty(); add_entry(std::move(path)); + if (set_focus) + button_play.focus(); }; }; button_delete.on_select = [this, &nav]() { + if (is_active()) + return; delete_entry(); }; button_open.on_select = [this, &nav]() { + if (is_active()) + stop(); open_file(); }; button_save.on_select = [this]() { + if (is_active()) + stop(); save_file(); }; button_prev.on_select = [this]() { + if (is_active()) + return; --current_index_; if (at_end()) current_index_ = playlist_db_.size() - 1; @@ -417,6 +436,8 @@ PlaylistView::PlaylistView( }; button_next.on_select = [this]() { + if (is_active()) + return; ++current_index_; if (at_end()) current_index_ = 0; @@ -426,6 +447,17 @@ PlaylistView::PlaylistView( update_ui(); } +PlaylistView::PlaylistView( + NavigationView& nav, + const fs::path& path) + : PlaylistView(nav) { + auto ext = path.extension(); + if (path_iequal(ext, ppl_ext)) + on_file_changed(path); + else if (path_iequal(ext, c16_ext)) + add_entry(fs::path{path}); +} + PlaylistView::~PlaylistView() { transmitter_model.disable(); baseband::shutdown(); @@ -446,8 +478,11 @@ void PlaylistView::on_hide() { View::on_hide(); } -void PlaylistView::focus() { - button_open.focus(); +void PlaylistView::on_show() { + if (playlist_path_.empty()) + button_add.focus(); + else + button_play.focus(); } } /* namespace ui */ diff --git a/firmware/application/apps/ui_playlist.hpp b/firmware/application/apps/ui_playlist.hpp index c965838d..87877762 100644 --- a/firmware/application/apps/ui_playlist.hpp +++ b/firmware/application/apps/ui_playlist.hpp @@ -45,13 +45,14 @@ namespace ui { class PlaylistView : public View { public: PlaylistView(NavigationView& nav); + PlaylistView(NavigationView& nav, const std::filesystem::path& path); ~PlaylistView(); void set_parent_rect(Rect new_parent_rect) override; void on_hide() override; - void focus() override; + void on_show() override; - std::string title() const override { return "Playlist"; }; + std::string title() const override { return "Replay"; }; private: NavigationView& nav_; @@ -146,41 +147,41 @@ class PlaylistView : public View { {0 * 8, 3 * 16, 30 * 8, 16}}; NewButton button_prev{ - {0 * 8, 4 * 16, 4 * 8, 2 * 16}, + {2 * 8, 4 * 16, 4 * 8, 2 * 16}, "", &bitmap_arrow_left, Color::dark_grey()}; - NewButton button_add{ + NewButton button_next{ {6 * 8, 4 * 16, 4 * 8, 2 * 16}, "", + &bitmap_arrow_right, + Color::dark_grey()}; + + NewButton button_add{ + {11 * 8, 4 * 16, 4 * 8, 2 * 16}, + "", &bitmap_icon_new_file, Color::orange()}; NewButton button_delete{ - {10 * 8, 4 * 16, 4 * 8, 2 * 16}, + {15 * 8, 4 * 16, 4 * 8, 2 * 16}, "", &bitmap_icon_delete, Color::orange()}; NewButton button_open{ - {16 * 8, 4 * 16, 4 * 8, 2 * 16}, + {20 * 8, 4 * 16, 4 * 8, 2 * 16}, "", &bitmap_icon_load, Color::dark_blue()}; NewButton button_save{ - {20 * 8, 4 * 16, 4 * 8, 2 * 16}, + {24 * 8, 4 * 16, 4 * 8, 2 * 16}, "", &bitmap_icon_save, Color::dark_blue()}; - NewButton button_next{ - {26 * 8, 4 * 16, 4 * 8, 2 * 16}, - "", - &bitmap_arrow_right, - Color::dark_grey()}; - spectrum::WaterfallWidget waterfall{}; MessageHandlerRegistration message_handler_replay_thread_error{ diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index e6ff9913..4aaa2fd3 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -488,6 +488,24 @@ path operator/(const path& lhs, const path& rhs) { return result; } +bool path_iequal( + const path& lhs, + const path& rhs) { + const auto& lhs_str = lhs.native(); + const auto& rhs_str = rhs.native(); + + // NB: Not correct for Unicode/locales. + if (lhs_str.length() == rhs_str.length()) { + for (size_t i = 0; i < lhs_str.length(); ++i) + if (towupper(lhs_str[i]) != towupper(rhs_str[i])) + return false; + + return true; + } + + return false; +} + directory_iterator::directory_iterator( std::filesystem::path path, std::filesystem::path wild) diff --git a/firmware/application/file.hpp b/firmware/application/file.hpp index 4c33a439..2b057ecd 100644 --- a/firmware/application/file.hpp +++ b/firmware/application/file.hpp @@ -168,6 +168,9 @@ bool operator>(const path& lhs, const path& rhs); path operator+(const path& lhs, const path& rhs); path operator/(const path& lhs, const path& rhs); +/* Case insensitive path equality on underlying "native" string. */ +bool path_iequal(const path& lhs, const path& rhs); + using file_status = BYTE; static_assert(sizeof(path::value_type) == 2, "sizeof(std::filesystem::path::value_type) != 2"); diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index d8342bfb..47d35c87 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -584,7 +584,7 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) { {"SSTV", ui::Color::green(), &bitmap_icon_sstv, [&nav]() { nav.push(); }}, {"TEDI/LCR", ui::Color::yellow(), &bitmap_icon_lcr, [&nav]() { nav.push(); }}, {"TouchTune", ui::Color::green(), &bitmap_icon_touchtunes, [&nav]() { nav.push(); }}, - {"Playlist", ui::Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push(); }}, + //{"Playlist", ui::Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push(); }}, {"S.Painter", ui::Color::orange(), &bitmap_icon_paint, [&nav]() { nav.push(); }}, //{ "Remote", ui::Color::dark_grey(), &bitmap_icon_remote, [&nav](){ nav.push(); } }, }); @@ -630,7 +630,7 @@ SystemMenuView::SystemMenuView(NavigationView& nav) { {"Receive", Color::cyan(), &bitmap_icon_receivers, [&nav]() { nav.push(); }}, {"Transmit", Color::cyan(), &bitmap_icon_transmit, [&nav]() { nav.push(); }}, {"Capture", Color::red(), &bitmap_icon_capture, [&nav]() { nav.push(); }}, - {"Replay", Color::green(), &bitmap_icon_replay, [&nav]() { nav.push(); }}, + {"Replay", Color::green(), &bitmap_icon_replay, [&nav]() { nav.push(); }}, {"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push(); }}, {"Scanner", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push(); }}, {"Microphone", Color::green(), &bitmap_icon_microphone, [&nav]() { nav.push(); }},