summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2023-10-18 14:35:28 +1100
committerjacqueline <me@jacqueline.id.au>2023-10-18 14:35:28 +1100
commit782e8dc8c25402171fc4724075b998eae4fa2c76 (patch)
treef710f8fe8e84f5a201410520f3f88364a19f76d8 /src
parent2eb7eaa2a6a5d9ccfe7a0535858778dfb85997cb (diff)
downloadtangara-fw-782e8dc8c25402171fc4724075b998eae4fa2c76.tar.gz
Add better controls for queue manipulation
Diffstat (limited to 'src')
-rw-r--r--src/playlist/include/source.hpp33
-rw-r--r--src/playlist/source.cpp163
-rw-r--r--src/ui/encoder_input.cpp15
-rw-r--r--src/ui/include/modal_add_to_queue.hpp5
-rw-r--r--src/ui/include/screen_track_browser.hpp11
-rw-r--r--src/ui/include/ui_events.hpp2
-rw-r--r--src/ui/modal_add_to_queue.cpp75
-rw-r--r--src/ui/screen_track_browser.cpp134
-rw-r--r--src/ui/ui_fsm.cpp35
9 files changed, 413 insertions, 60 deletions
diff --git a/src/playlist/include/source.hpp b/src/playlist/include/source.hpp
index 069c1e93..aa15e7df 100644
--- a/src/playlist/include/source.hpp
+++ b/src/playlist/include/source.hpp
@@ -73,6 +73,11 @@ class IResetableSource : public ISource {
virtual auto Reset() -> void = 0;
};
+auto CreateSourceFromResults(
+ std::weak_ptr<database::Database>,
+ std::shared_ptr<database::Result<database::IndexRecord>>)
+ -> std::shared_ptr<IResetableSource>;
+
class IndexRecordSource : public IResetableSource {
public:
IndexRecordSource(std::weak_ptr<database::Database> db,
@@ -102,4 +107,32 @@ class IndexRecordSource : public IResetableSource {
ssize_t current_item_;
};
+class NestedSource : public IResetableSource {
+ public:
+ NestedSource(std::weak_ptr<database::Database> db,
+ std::shared_ptr<database::Result<database::IndexRecord>>);
+
+ auto Current() -> std::optional<database::TrackId> override;
+ auto Advance() -> std::optional<database::TrackId> override;
+ auto Peek(std::size_t n, std::vector<database::TrackId>*)
+ -> std::size_t override;
+
+ auto Previous() -> std::optional<database::TrackId> override;
+ auto Reset() -> void override;
+
+ private:
+ auto CreateChild(std::shared_ptr<database::IndexRecord> page)
+ -> std::shared_ptr<IResetableSource>;
+
+ std::weak_ptr<database::Database> db_;
+
+ std::shared_ptr<database::Result<database::IndexRecord>> initial_page_;
+ ssize_t initial_item_;
+
+ std::shared_ptr<database::Result<database::IndexRecord>> current_page_;
+ ssize_t current_item_;
+
+ std::shared_ptr<IResetableSource> current_child_;
+};
+
} // namespace playlist
diff --git a/src/playlist/source.cpp b/src/playlist/source.cpp
index 18a7887b..d51d97ab 100644
--- a/src/playlist/source.cpp
+++ b/src/playlist/source.cpp
@@ -22,6 +22,17 @@
namespace playlist {
+auto CreateSourceFromResults(
+ std::weak_ptr<database::Database> db,
+ std::shared_ptr<database::Result<database::IndexRecord>> results)
+ -> std::shared_ptr<IResetableSource> {
+ if (results->values()[0]->track()) {
+ return std::make_shared<IndexRecordSource>(db, results);
+ } else {
+ return std::make_shared<NestedSource>(db, results);
+ }
+}
+
IndexRecordSource::IndexRecordSource(
std::weak_ptr<database::Database> db,
std::shared_ptr<database::Result<database::IndexRecord>> initial)
@@ -142,4 +153,156 @@ auto IndexRecordSource::Reset() -> void {
current_item_ = initial_item_;
}
+NestedSource::NestedSource(
+ std::weak_ptr<database::Database> db,
+ std::shared_ptr<database::Result<database::IndexRecord>> initial)
+ : db_(db),
+ initial_page_(initial),
+ initial_item_(0),
+ current_page_(initial_page_),
+ current_item_(initial_item_),
+ current_child_(CreateChild(initial->values()[0])) {}
+
+auto NestedSource::Current() -> std::optional<database::TrackId> {
+ if (current_child_) {
+ return current_child_->Current();
+ }
+ return {};
+}
+
+auto NestedSource::Advance() -> std::optional<database::TrackId> {
+ if (!current_child_) {
+ return {};
+ }
+
+ auto child_next = current_child_->Advance();
+ if (child_next) {
+ return child_next;
+ }
+ // Our current child has run out of tracks. Move on to the next child.
+ current_item_++;
+ current_child_.reset();
+
+ if (current_item_ >= current_page_->values().size()) {
+ // We're even out of items in this page!
+ auto next_page = current_page_->next_page();
+ if (!next_page) {
+ current_item_--;
+ return {};
+ }
+
+ auto db = db_.lock();
+ if (!db) {
+ return {};
+ }
+
+ current_page_.reset(db->GetPage<database::IndexRecord>(&*next_page).get());
+ current_item_ = 0;
+ }
+ current_child_ = CreateChild(current_page_->values()[current_item_]);
+
+ return Current();
+}
+
+auto NestedSource::Previous() -> std::optional<database::TrackId> {
+ if (current_page_ == initial_page_ && current_item_ <= initial_item_) {
+ return {};
+ }
+
+ current_item_--;
+ current_child_.reset();
+
+ if (current_item_ < 0) {
+ auto prev_page = current_page_->prev_page();
+ if (!prev_page) {
+ return {};
+ }
+
+ auto db = db_.lock();
+ if (!db) {
+ return {};
+ }
+
+ current_page_.reset(db->GetPage<database::IndexRecord>(&*prev_page).get());
+ current_item_ = current_page_->values().size() - 1;
+ }
+ current_child_ = CreateChild(current_page_->values()[current_item_]);
+
+ return Current();
+}
+
+auto NestedSource::Peek(std::size_t n, std::vector<database::TrackId>* out)
+ -> std::size_t {
+ if (current_page_->values().size() <= current_item_) {
+ return {};
+ }
+
+ auto db = db_.lock();
+ if (!db) {
+ return 0;
+ }
+
+ std::size_t items_added = 0;
+
+ std::shared_ptr<database::Result<database::IndexRecord>> working_page =
+ current_page_;
+ std::size_t working_item = current_item_;
+ std::shared_ptr<IResetableSource> working_child = current_child_;
+
+ while (working_child) {
+ auto res = working_child->Peek(n, out);
+ n -= res;
+ items_added += res;
+
+ if (n == 0) {
+ break;
+ } else {
+ working_item++;
+ if (working_item < working_page->values().size()) {
+ working_child = CreateChild(working_page->values()[working_item]);
+ } else {
+ auto next_page = current_page_->next_page();
+ if (!next_page) {
+ break;
+ }
+ working_page.reset(
+ db->GetPage<database::IndexRecord>(&*next_page).get());
+ working_item = 0;
+ working_child = CreateChild(working_page->values()[0]);
+ }
+ }
+ }
+
+ return items_added;
+}
+
+auto NestedSource::Reset() -> void {
+ current_page_ = initial_page_;
+ current_item_ = initial_item_;
+ current_child_ = CreateChild(initial_page_->values()[initial_item_]);
+}
+
+auto NestedSource::CreateChild(std::shared_ptr<database::IndexRecord> record)
+ -> std::shared_ptr<IResetableSource> {
+ auto cont = record->Expand(10);
+ if (!cont) {
+ return {};
+ }
+ auto db = db_.lock();
+ if (!db) {
+ return {};
+ }
+ std::shared_ptr<database::Result<database::IndexRecord>> next_level{
+ db->GetPage<database::IndexRecord>(&*cont).get()};
+ if (!next_level) {
+ return {};
+ }
+ auto next_level_record = next_level->values()[0];
+ if (next_level_record->track()) {
+ return std::make_shared<IndexRecordSource>(db_, next_level);
+ } else {
+ return std::make_shared<NestedSource>(db_, next_level);
+ }
+}
+
} // namespace playlist
diff --git a/src/ui/encoder_input.cpp b/src/ui/encoder_input.cpp
index 0345665b..f6a981a7 100644
--- a/src/ui/encoder_input.cpp
+++ b/src/ui/encoder_input.cpp
@@ -9,7 +9,10 @@
#include <sys/_stdint.h>
#include <memory>
+#include "lvgl.h"
+
#include "audio_events.hpp"
+#include "core/lv_event.h"
#include "core/lv_group.h"
#include "esp_timer.h"
#include "event_queue.hpp"
@@ -20,6 +23,8 @@
#include "touchwheel.hpp"
#include "ui_events.hpp"
+static constexpr char kTag[] = "input";
+
constexpr int kDPadAngleThreshold = 20;
constexpr int kLongPressDelayMs = 500;
constexpr int kRepeatDelayMs = 250;
@@ -58,6 +63,11 @@ auto EncoderInput::Read(lv_indev_data_t* data) -> void {
return;
}
+ lv_obj_t* active_object = nullptr;
+ if (registration_ && registration_->group) {
+ active_object = lv_group_get_focused(registration_->group);
+ }
+
raw_wheel_.Update();
relative_wheel_->Update();
// GPIOs updating is handled by system_fsm.
@@ -226,8 +236,9 @@ auto EncoderInput::Read(lv_indev_data_t* data) -> void {
data->state = LV_INDEV_STATE_PRESSED;
break;
case Trigger::kLongPress:
- // TODO: ???
- data->state = LV_INDEV_STATE_PRESSED;
+ if (active_object) {
+ lv_event_send(active_object, LV_EVENT_LONG_PRESSED, NULL);
+ }
break;
}
diff --git a/src/ui/include/modal_add_to_queue.hpp b/src/ui/include/modal_add_to_queue.hpp
index 79f804a4..e6417cd4 100644
--- a/src/ui/include/modal_add_to_queue.hpp
+++ b/src/ui/include/modal_add_to_queue.hpp
@@ -24,11 +24,12 @@ class AddToQueue : public Modal {
public:
AddToQueue(Screen*,
audio::TrackQueue&,
- std::shared_ptr<playlist::IndexRecordSource>);
+ std::shared_ptr<playlist::IResetableSource>,
+ bool all_tracks_only = false);
private:
audio::TrackQueue& queue_;
- std::shared_ptr<playlist::IndexRecordSource> item_;
+ std::shared_ptr<playlist::IResetableSource> item_;
lv_obj_t* container_;
lv_obj_t* selected_track_btn_;
diff --git a/src/ui/include/screen_track_browser.hpp b/src/ui/include/screen_track_browser.hpp
index 719306f0..0b2d6fc3 100644
--- a/src/ui/include/screen_track_browser.hpp
+++ b/src/ui/include/screen_track_browser.hpp
@@ -16,6 +16,7 @@
#include "database.hpp"
#include "model_top_bar.hpp"
#include "screen.hpp"
+#include "track_queue.hpp"
namespace ui {
namespace screens {
@@ -23,9 +24,10 @@ namespace screens {
class TrackBrowser : public Screen {
public:
TrackBrowser(
- models::TopBar&,
+ models::TopBar& top_bar,
+ audio::TrackQueue& queue,
std::weak_ptr<database::Database> db,
- const std::pmr::string& title,
+ const std::pmr::vector<std::pmr::string>& breadcrumbs,
std::future<database::Result<database::IndexRecord>*>&& initial_page);
~TrackBrowser() {}
@@ -49,11 +51,16 @@ class TrackBrowser : public Screen {
auto GetNumRecords() -> std::size_t;
auto GetItemIndex(lv_obj_t* obj) -> std::optional<std::size_t>;
+ audio::TrackQueue& queue_;
std::weak_ptr<database::Database> db_;
lv_obj_t* back_button_;
+ lv_obj_t* play_button_;
+ lv_obj_t* enqueue_button_;
lv_obj_t* list_;
lv_obj_t* loading_indicator_;
+ std::pmr::vector<std::pmr::string> breadcrumbs_;
+
std::optional<Position> loading_pos_;
std::optional<std::future<database::Result<database::IndexRecord>*>>
loading_page_;
diff --git a/src/ui/include/ui_events.hpp b/src/ui/include/ui_events.hpp
index 4549a51d..2bee6222 100644
--- a/src/ui/include/ui_events.hpp
+++ b/src/ui/include/ui_events.hpp
@@ -27,6 +27,8 @@ struct OnSystemError : tinyfsm::Event {};
namespace internal {
struct RecordSelected : tinyfsm::Event {
+ bool show_menu;
+ std::pmr::vector<std::pmr::string> new_crumbs;
std::shared_ptr<database::Result<database::IndexRecord>> initial_page;
std::shared_ptr<database::Result<database::IndexRecord>> page;
std::size_t record;
diff --git a/src/ui/modal_add_to_queue.cpp b/src/ui/modal_add_to_queue.cpp
index cc3a8d51..e102fae8 100644
--- a/src/ui/modal_add_to_queue.cpp
+++ b/src/ui/modal_add_to_queue.cpp
@@ -37,50 +37,55 @@ namespace modals {
AddToQueue::AddToQueue(Screen* host,
audio::TrackQueue& queue,
- std::shared_ptr<playlist::IndexRecordSource> item)
+ std::shared_ptr<playlist::IResetableSource> item,
+ bool all_tracks_only)
: Modal(host), queue_(queue), item_(item), all_tracks_(0) {
lv_obj_set_layout(root_, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(root_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(root_, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_CENTER);
- lv_obj_t* button_container = lv_obj_create(root_);
- lv_obj_set_size(button_container, lv_pct(100), LV_SIZE_CONTENT);
- lv_obj_set_layout(button_container, LV_LAYOUT_FLEX);
- lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW);
- lv_obj_set_flex_align(button_container, LV_FLEX_ALIGN_SPACE_EVENLY,
- LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
-
- selected_track_btn_ = lv_btn_create(button_container);
- lv_obj_t* label = lv_label_create(selected_track_btn_);
- lv_label_set_text(label, "Selected");
- lv_group_add_obj(group_, selected_track_btn_);
- lv_obj_add_state(selected_track_btn_, LV_STATE_CHECKED);
- themes::Theme::instance()->ApplyStyle(selected_track_btn_,
- themes::Style::kTab);
+ if (all_tracks_only) {
+ all_tracks_ = true;
+ } else {
+ lv_obj_t* button_container = lv_obj_create(root_);
+ lv_obj_set_size(button_container, lv_pct(100), LV_SIZE_CONTENT);
+ lv_obj_set_layout(button_container, LV_LAYOUT_FLEX);
+ lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW);
+ lv_obj_set_flex_align(button_container, LV_FLEX_ALIGN_SPACE_EVENLY,
+ LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
- lv_bind(selected_track_btn_, LV_EVENT_CLICKED, [this](lv_obj_t*) {
+ selected_track_btn_ = lv_btn_create(button_container);
+ lv_obj_t* label = lv_label_create(selected_track_btn_);
+ lv_label_set_text(label, "Selected");
+ lv_group_add_obj(group_, selected_track_btn_);
lv_obj_add_state(selected_track_btn_, LV_STATE_CHECKED);
- lv_obj_clear_state(all_tracks_btn_, LV_STATE_CHECKED);
- all_tracks_ = false;
- });
+ themes::Theme::instance()->ApplyStyle(selected_track_btn_,
+ themes::Style::kTab);
- all_tracks_btn_ = lv_btn_create(button_container);
- label = lv_label_create(all_tracks_btn_);
- lv_label_set_text(label, "All tracks");
- lv_group_add_obj(group_, all_tracks_btn_);
- themes::Theme::instance()->ApplyStyle(all_tracks_btn_, themes::Style::kTab);
+ lv_bind(selected_track_btn_, LV_EVENT_CLICKED, [this](lv_obj_t*) {
+ lv_obj_add_state(selected_track_btn_, LV_STATE_CHECKED);
+ lv_obj_clear_state(all_tracks_btn_, LV_STATE_CHECKED);
+ all_tracks_ = false;
+ });
- lv_bind(all_tracks_btn_, LV_EVENT_CLICKED, [this](lv_obj_t*) {
- lv_obj_clear_state(selected_track_btn_, LV_STATE_CHECKED);
- lv_obj_add_state(all_tracks_btn_, LV_STATE_CHECKED);
- all_tracks_ = true;
- });
+ all_tracks_btn_ = lv_btn_create(button_container);
+ label = lv_label_create(all_tracks_btn_);
+ lv_label_set_text(label, "From here");
+ lv_group_add_obj(group_, all_tracks_btn_);
+ themes::Theme::instance()->ApplyStyle(all_tracks_btn_, themes::Style::kTab);
- lv_obj_t* spacer = lv_obj_create(root_);
- lv_obj_set_size(spacer, 1, 4);
+ lv_bind(all_tracks_btn_, LV_EVENT_CLICKED, [this](lv_obj_t*) {
+ lv_obj_clear_state(selected_track_btn_, LV_STATE_CHECKED);
+ lv_obj_add_state(all_tracks_btn_, LV_STATE_CHECKED);
+ all_tracks_ = true;
+ });
- button_container = lv_obj_create(root_);
+ lv_obj_t* spacer = lv_obj_create(root_);
+ lv_obj_set_size(spacer, 1, 4);
+ }
+
+ lv_obj_t* button_container = lv_obj_create(root_);
lv_obj_set_size(button_container, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_set_layout(button_container, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW);
@@ -88,7 +93,7 @@ AddToQueue::AddToQueue(Screen* host,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t* btn = lv_btn_create(button_container);
- label = lv_label_create(btn);
+ lv_obj_t* label = lv_label_create(btn);
lv_label_set_text(label, "Play now");
lv_group_add_obj(group_, btn);
@@ -112,7 +117,7 @@ AddToQueue::AddToQueue(Screen* host,
label = lv_label_create(root_);
lv_label_set_text(label, "Enqueue");
- spacer = lv_obj_create(root_);
+ lv_obj_t* spacer = lv_obj_create(root_);
lv_obj_set_size(spacer, 1, 4);
button_container = lv_obj_create(root_);
@@ -151,7 +156,7 @@ AddToQueue::AddToQueue(Screen* host,
});
}
- spacer = lv_obj_create(root_);
+ lv_obj_t* spacer = lv_obj_create(root_);
lv_obj_set_size(spacer, 1, 4);
button_container = lv_obj_create(root_);
diff --git a/src/ui/screen_track_browser.cpp b/src/ui/screen_track_browser.cpp
index 58cd2946..d0bb59e9 100644
--- a/src/ui/screen_track_browser.cpp
+++ b/src/ui/screen_track_browser.cpp
@@ -16,6 +16,7 @@
#include "font/lv_symbol_def.h"
#include "lvgl.h"
#include "misc/lv_anim.h"
+#include "misc/lv_color.h"
#include "model_top_bar.hpp"
#include "screen_menu.hpp"
@@ -30,6 +31,9 @@
#include "hal/lv_hal_disp.h"
#include "misc/lv_area.h"
#include "screen_track_browser.hpp"
+#include "source.hpp"
+#include "themes.hpp"
+#include "track_queue.hpp"
#include "ui_events.hpp"
#include "ui_fsm.hpp"
#include "widget_top_bar.hpp"
@@ -61,12 +65,17 @@ static void item_select_cb(lv_event_t* ev) {
TrackBrowser::TrackBrowser(
models::TopBar& top_bar_model,
+ audio::TrackQueue& queue,
std::weak_ptr<database::Database> db,
- const std::pmr::string& title,
+ const std::pmr::vector<std::pmr::string>& crumbs,
std::future<database::Result<database::IndexRecord>*>&& initial_page)
- : db_(db),
+ : queue_(queue),
+ db_(db),
+ play_button_(nullptr),
+ enqueue_button_(nullptr),
list_(nullptr),
loading_indicator_(nullptr),
+ breadcrumbs_(crumbs),
loading_pos_(END),
loading_page_(move(initial_page)),
initial_page_(),
@@ -76,22 +85,111 @@ TrackBrowser::TrackBrowser(
lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
- // The default scrollbar is deceptive because we load in items progressively.
- lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
- // Wrapping behaves in surprising ways, again due to progressing loading.
- lv_group_set_wrap(group_, false);
-
widgets::TopBar::Configuration config{
.show_back_button = true,
- .title = title,
+ .title = breadcrumbs_[0],
};
auto top_bar = CreateTopBar(content_, config, top_bar_model);
back_button_ = top_bar->button();
- list_ = lv_list_create(content_);
- lv_obj_set_width(list_, lv_pct(100));
- lv_obj_set_flex_grow(list_, 1);
- lv_obj_center(list_);
+ lv_obj_t* scrollable = lv_obj_create(content_);
+ lv_obj_set_width(scrollable, lv_pct(100));
+ lv_obj_set_flex_grow(scrollable, 1);
+ lv_obj_set_layout(scrollable, LV_LAYOUT_FLEX);
+ lv_obj_set_flex_flow(scrollable, LV_FLEX_FLOW_COLUMN);
+ lv_obj_set_flex_align(scrollable, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START,
+ LV_FLEX_ALIGN_START);
+
+ if (crumbs.size() > 1) {
+ lv_obj_t* header = lv_obj_create(scrollable);
+ lv_obj_set_size(header, lv_pct(100), LV_SIZE_CONTENT);
+ lv_obj_set_layout(header, LV_LAYOUT_FLEX);
+ lv_obj_set_flex_flow(header, LV_FLEX_FLOW_COLUMN);
+ lv_obj_set_flex_align(header, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START,
+ LV_FLEX_ALIGN_START);
+
+ lv_obj_set_style_pad_left(header, 4, LV_PART_MAIN);
+ lv_obj_set_style_pad_right(header, 4, LV_PART_MAIN);
+
+ lv_obj_t* spacer = lv_obj_create(header);
+ lv_obj_set_size(spacer, 1, 2);
+
+ for (size_t i = 1; i < crumbs.size(); i++) {
+ lv_obj_t* crumb = lv_label_create(header);
+ lv_label_set_text(crumb, crumbs[i].c_str());
+
+ spacer = lv_obj_create(header);
+ lv_obj_set_size(spacer, 1, 2);
+ }
+
+ spacer = lv_obj_create(header);
+ lv_obj_set_size(spacer, 1, 2);
+
+ lv_obj_t* buttons_container = lv_obj_create(header);
+ lv_obj_set_width(buttons_container, lv_pct(100));
+ lv_obj_set_height(buttons_container, LV_SIZE_CONTENT);
+ lv_obj_set_layout(buttons_container, LV_LAYOUT_FLEX);
+ lv_obj_set_flex_flow(buttons_container, LV_FLEX_FLOW_ROW);
+ lv_obj_set_flex_align(buttons_container, LV_FLEX_ALIGN_END,
+ LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
+
+ lv_obj_t* label;
+
+ play_button_ = lv_btn_create(buttons_container);
+ label = lv_label_create(play_button_);
+ lv_label_set_text(label, "Play all");
+ lv_group_add_obj(group_, play_button_);
+ themes::Theme::instance()->ApplyStyle(play_button_,
+ themes::Style::kButtonPrimary);
+
+ lv_bind(play_button_, LV_EVENT_CLICKED, [&](lv_obj_t*) {
+ if (!initial_page_) {
+ return;
+ }
+ queue_.Clear();
+ queue_.IncludeNext(playlist::CreateSourceFromResults(db_, initial_page_));
+ events::Ui().Dispatch(internal::ShowNowPlaying{});
+ });
+
+ if (queue_.GetCurrent()) {
+ spacer = lv_obj_create(buttons_container);
+ lv_obj_set_size(spacer, 4, 1);
+
+ enqueue_button_ = lv_btn_create(buttons_container);
+ label = lv_label_create(enqueue_button_);
+ lv_label_set_text(label, "Enqueue");
+ lv_group_add_obj(group_, enqueue_button_);
+ themes::Theme::instance()->ApplyStyle(enqueue_button_,
+ themes::Style::kButtonPrimary);
+
+ lv_bind(enqueue_button_, LV_EVENT_CLICKED, [&](lv_obj_t*) {
+ if (!initial_page_) {
+ return;
+ }
+ queue_.IncludeNext(
+ playlist::CreateSourceFromResults(db_, initial_page_));
+ });
+ }
+
+ lv_obj_set_style_border_width(header, 1, LV_PART_MAIN);
+ lv_obj_set_style_border_color(header, lv_color_black(), LV_PART_MAIN);
+ lv_obj_set_style_border_side(header, LV_BORDER_SIDE_BOTTOM, LV_PART_MAIN);
+
+ spacer = lv_obj_create(header);
+ lv_obj_set_size(spacer, 1, 4);
+
+ lv_obj_set_style_border_width(header, 1, LV_PART_MAIN);
+ lv_obj_set_style_border_color(
+ header, lv_palette_lighten(LV_PALETTE_GREY, 3), LV_PART_MAIN);
+ }
+
+ list_ = lv_list_create(scrollable);
+ lv_obj_set_size(list_, lv_pct(100), LV_SIZE_CONTENT);
+
+ // The default scrollbar is deceptive because we load in items progressively.
+ lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
+ // Wrapping behaves in surprising ways, again due to progressing loading.
+ lv_group_set_wrap(group_, false);
}
auto TrackBrowser::Tick() -> void {
@@ -138,7 +236,12 @@ auto TrackBrowser::OnItemClicked(lv_event_t* ev) -> void {
for (const auto& page : current_pages_) {
for (std::size_t i = 0; i < page->values().size(); i++) {
if (index == 0) {
+ auto text = page->values()[i]->text();
+ auto crumbs = breadcrumbs_;
+ crumbs.push_back(text.value());
events::Ui().Dispatch(internal::RecordSelected{
+ .show_menu = ev->code == LV_EVENT_LONG_PRESSED,
+ .new_crumbs = crumbs,
.initial_page = initial_page_,
.page = page,
.record = i,
@@ -181,6 +284,7 @@ auto TrackBrowser::AddResults(
lv_obj_t* item = lv_list_add_btn(list_, NULL, text->c_str());
lv_label_set_long_mode(lv_obj_get_child(item, -1), LV_LABEL_LONG_DOT);
lv_obj_add_event_cb(item, item_click_cb, LV_EVENT_CLICKED, this);
+ lv_obj_add_event_cb(item, item_click_cb, LV_EVENT_LONG_PRESSED, this);
lv_obj_add_event_cb(item, item_select_cb, LV_EVENT_FOCUSED, this);
if (pos == START) {
@@ -217,6 +321,12 @@ auto TrackBrowser::AddResults(
lv_group_remove_all_objs(group_);
lv_group_add_obj(group_, back_button_);
+ if (play_button_) {
+ lv_group_add_obj(group_, play_button_);
+ }
+ if (enqueue_button_) {
+ lv_group_add_obj(group_, enqueue_button_);
+ }
int num_children = lv_obj_get_child_cnt(list_);
for (int i = 0; i < num_children; i++) {
lv_group_add_obj(group_, lv_obj_get_child(list_, i));
diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp
index 145bcbcc..a33dd38e 100644
--- a/src/ui/ui_fsm.cpp
+++ b/src/ui/ui_fsm.cpp
@@ -11,6 +11,7 @@
#include "audio_fsm.hpp"
#include "battery.hpp"
#include "core/lv_obj.h"
+#include "database.hpp"
#include "misc/lv_gc.h"
#include "audio_events.hpp"
@@ -112,8 +113,12 @@ void UiState::react(const audio::PlaybackUpdate& ev) {
void UiState::react(const audio::QueueUpdate&) {
auto& queue = sServices->track_queue();
+ bool had_queue = sPlaybackModel.current_track.get().has_value();
sPlaybackModel.current_track.set(queue.GetCurrent());
sPlaybackModel.upcoming_tracks.set(queue.GetUpcoming(10));
+ if (!had_queue) {
+ transit<states::Playing>();
+ }
}
void UiState::react(const internal::ControlSchemeChanged&) {
@@ -285,14 +290,20 @@ void Browse::react(const internal::RecordSelected& ev) {
return;
}
+ auto& queue = sServices->track_queue();
auto record = ev.page->values().at(ev.record);
if (record->track()) {
ESP_LOGI(kTag, "selected track '%s'", record->text()->c_str());
- auto& queue = sServices->track_queue();
auto source = std::make_shared<playlist::IndexRecordSource>(
sServices->database(), ev.initial_page, 0, ev.page, ev.record);
- sCurrentModal.reset(
- new modals::AddToQueue(sCurrentScreen.get(), queue, source));
+ if (ev.show_menu) {
+ sCurrentModal.reset(
+ new modals::AddToQueue(sCurrentScreen.get(), queue, source));
+ } else {
+ queue.Clear();
+ queue.AddNext(source);
+ transit<Playing>();
+ }
} else {
ESP_LOGI(kTag, "selected record '%s'", record->text()->c_str());
auto cont = record->Expand(kRecordsPerPage);
@@ -300,9 +311,17 @@ void Browse::react(const internal::RecordSelected& ev) {
return;
}
auto query = db->GetPage<database::IndexRecord>(&cont.value());
- std::pmr::string title = record->text().value_or("TODO");
- PushScreen(std::make_shared<screens::TrackBrowser>(
- sTopBarModel, sServices->database(), title, std::move(query)));
+ if (ev.show_menu) {
+ std::shared_ptr<database::Result<database::IndexRecord>> res{query.get()};
+ auto source = playlist::CreateSourceFromResults(db, res);
+ sCurrentModal.reset(
+ new modals::AddToQueue(sCurrentScreen.get(), queue, source, true));
+ } else {
+ std::pmr::string title = record->text().value_or("");
+ PushScreen(std::make_shared<screens::TrackBrowser>(
+ sTopBarModel, sServices->track_queue(), sServices->database(),
+ ev.new_crumbs, std::move(query)));
+ }
}
}
@@ -314,8 +333,10 @@ void Browse::react(const internal::IndexSelected& ev) {
ESP_LOGI(kTag, "selected index %s", ev.index.name.c_str());
auto query = db->GetTracksByIndex(ev.index, kRecordsPerPage);
+ std::pmr::vector<std::pmr::string> crumbs = {ev.index.name};
PushScreen(std::make_shared<screens::TrackBrowser>(
- sTopBarModel, sServices->database(), ev.index.name, std::move(query)));
+ sTopBarModel, sServices->track_queue(), sServices->database(), crumbs,
+ std::move(query)));
}
void Browse::react(const internal::BackPressed& ev) {