Add better controls for queue manipulation

custom
jacqueline 2 years ago
parent 2eb7eaa2a6
commit 782e8dc8c2
  1. 33
      src/playlist/include/source.hpp
  2. 163
      src/playlist/source.cpp
  3. 15
      src/ui/encoder_input.cpp
  4. 5
      src/ui/include/modal_add_to_queue.hpp
  5. 11
      src/ui/include/screen_track_browser.hpp
  6. 2
      src/ui/include/ui_events.hpp
  7. 75
      src/ui/modal_add_to_queue.cpp
  8. 134
      src/ui/screen_track_browser.cpp
  9. 35
      src/ui/ui_fsm.cpp

@ -73,6 +73,11 @@ class IResetableSource : public ISource {
virtual auto Reset() -> void = 0; 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 { class IndexRecordSource : public IResetableSource {
public: public:
IndexRecordSource(std::weak_ptr<database::Database> db, IndexRecordSource(std::weak_ptr<database::Database> db,
@ -102,4 +107,32 @@ class IndexRecordSource : public IResetableSource {
ssize_t current_item_; 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 } // namespace playlist

@ -22,6 +22,17 @@
namespace playlist { 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( IndexRecordSource::IndexRecordSource(
std::weak_ptr<database::Database> db, std::weak_ptr<database::Database> db,
std::shared_ptr<database::Result<database::IndexRecord>> initial) std::shared_ptr<database::Result<database::IndexRecord>> initial)
@ -142,4 +153,156 @@ auto IndexRecordSource::Reset() -> void {
current_item_ = initial_item_; 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 } // namespace playlist

@ -9,7 +9,10 @@
#include <sys/_stdint.h> #include <sys/_stdint.h>
#include <memory> #include <memory>
#include "lvgl.h"
#include "audio_events.hpp" #include "audio_events.hpp"
#include "core/lv_event.h"
#include "core/lv_group.h" #include "core/lv_group.h"
#include "esp_timer.h" #include "esp_timer.h"
#include "event_queue.hpp" #include "event_queue.hpp"
@ -20,6 +23,8 @@
#include "touchwheel.hpp" #include "touchwheel.hpp"
#include "ui_events.hpp" #include "ui_events.hpp"
static constexpr char kTag[] = "input";
constexpr int kDPadAngleThreshold = 20; constexpr int kDPadAngleThreshold = 20;
constexpr int kLongPressDelayMs = 500; constexpr int kLongPressDelayMs = 500;
constexpr int kRepeatDelayMs = 250; constexpr int kRepeatDelayMs = 250;
@ -58,6 +63,11 @@ auto EncoderInput::Read(lv_indev_data_t* data) -> void {
return; return;
} }
lv_obj_t* active_object = nullptr;
if (registration_ && registration_->group) {
active_object = lv_group_get_focused(registration_->group);
}
raw_wheel_.Update(); raw_wheel_.Update();
relative_wheel_->Update(); relative_wheel_->Update();
// GPIOs updating is handled by system_fsm. // 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; data->state = LV_INDEV_STATE_PRESSED;
break; break;
case Trigger::kLongPress: case Trigger::kLongPress:
// TODO: ??? if (active_object) {
data->state = LV_INDEV_STATE_PRESSED; lv_event_send(active_object, LV_EVENT_LONG_PRESSED, NULL);
}
break; break;
} }

@ -24,11 +24,12 @@ class AddToQueue : public Modal {
public: public:
AddToQueue(Screen*, AddToQueue(Screen*,
audio::TrackQueue&, audio::TrackQueue&,
std::shared_ptr<playlist::IndexRecordSource>); std::shared_ptr<playlist::IResetableSource>,
bool all_tracks_only = false);
private: private:
audio::TrackQueue& queue_; audio::TrackQueue& queue_;
std::shared_ptr<playlist::IndexRecordSource> item_; std::shared_ptr<playlist::IResetableSource> item_;
lv_obj_t* container_; lv_obj_t* container_;
lv_obj_t* selected_track_btn_; lv_obj_t* selected_track_btn_;

@ -16,6 +16,7 @@
#include "database.hpp" #include "database.hpp"
#include "model_top_bar.hpp" #include "model_top_bar.hpp"
#include "screen.hpp" #include "screen.hpp"
#include "track_queue.hpp"
namespace ui { namespace ui {
namespace screens { namespace screens {
@ -23,9 +24,10 @@ namespace screens {
class TrackBrowser : public Screen { class TrackBrowser : public Screen {
public: public:
TrackBrowser( TrackBrowser(
models::TopBar&, models::TopBar& top_bar,
audio::TrackQueue& queue,
std::weak_ptr<database::Database> db, 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); std::future<database::Result<database::IndexRecord>*>&& initial_page);
~TrackBrowser() {} ~TrackBrowser() {}
@ -49,11 +51,16 @@ class TrackBrowser : public Screen {
auto GetNumRecords() -> std::size_t; auto GetNumRecords() -> std::size_t;
auto GetItemIndex(lv_obj_t* obj) -> std::optional<std::size_t>; auto GetItemIndex(lv_obj_t* obj) -> std::optional<std::size_t>;
audio::TrackQueue& queue_;
std::weak_ptr<database::Database> db_; std::weak_ptr<database::Database> db_;
lv_obj_t* back_button_; lv_obj_t* back_button_;
lv_obj_t* play_button_;
lv_obj_t* enqueue_button_;
lv_obj_t* list_; lv_obj_t* list_;
lv_obj_t* loading_indicator_; lv_obj_t* loading_indicator_;
std::pmr::vector<std::pmr::string> breadcrumbs_;
std::optional<Position> loading_pos_; std::optional<Position> loading_pos_;
std::optional<std::future<database::Result<database::IndexRecord>*>> std::optional<std::future<database::Result<database::IndexRecord>*>>
loading_page_; loading_page_;

@ -27,6 +27,8 @@ struct OnSystemError : tinyfsm::Event {};
namespace internal { namespace internal {
struct RecordSelected : tinyfsm::Event { 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>> initial_page;
std::shared_ptr<database::Result<database::IndexRecord>> page; std::shared_ptr<database::Result<database::IndexRecord>> page;
std::size_t record; std::size_t record;

@ -37,50 +37,55 @@ namespace modals {
AddToQueue::AddToQueue(Screen* host, AddToQueue::AddToQueue(Screen* host,
audio::TrackQueue& queue, 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) { : Modal(host), queue_(queue), item_(item), all_tracks_(0) {
lv_obj_set_layout(root_, LV_LAYOUT_FLEX); lv_obj_set_layout(root_, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(root_, LV_FLEX_FLOW_COLUMN); 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_obj_set_flex_align(root_, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_CENTER); LV_FLEX_ALIGN_CENTER);
lv_obj_t* button_container = lv_obj_create(root_); if (all_tracks_only) {
lv_obj_set_size(button_container, lv_pct(100), LV_SIZE_CONTENT); all_tracks_ = true;
lv_obj_set_layout(button_container, LV_LAYOUT_FLEX); } else {
lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW); lv_obj_t* button_container = lv_obj_create(root_);
lv_obj_set_flex_align(button_container, LV_FLEX_ALIGN_SPACE_EVENLY, lv_obj_set_size(button_container, lv_pct(100), LV_SIZE_CONTENT);
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_layout(button_container, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW);
selected_track_btn_ = lv_btn_create(button_container); lv_obj_set_flex_align(button_container, LV_FLEX_ALIGN_SPACE_EVENLY,
lv_obj_t* label = lv_label_create(selected_track_btn_); LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
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);
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_add_state(selected_track_btn_, LV_STATE_CHECKED);
lv_obj_clear_state(all_tracks_btn_, LV_STATE_CHECKED); themes::Theme::instance()->ApplyStyle(selected_track_btn_,
all_tracks_ = false; themes::Style::kTab);
});
all_tracks_btn_ = lv_btn_create(button_container); lv_bind(selected_track_btn_, LV_EVENT_CLICKED, [this](lv_obj_t*) {
label = lv_label_create(all_tracks_btn_); lv_obj_add_state(selected_track_btn_, LV_STATE_CHECKED);
lv_label_set_text(label, "All tracks"); lv_obj_clear_state(all_tracks_btn_, LV_STATE_CHECKED);
lv_group_add_obj(group_, all_tracks_btn_); all_tracks_ = false;
themes::Theme::instance()->ApplyStyle(all_tracks_btn_, themes::Style::kTab); });
lv_bind(all_tracks_btn_, LV_EVENT_CLICKED, [this](lv_obj_t*) { all_tracks_btn_ = lv_btn_create(button_container);
lv_obj_clear_state(selected_track_btn_, LV_STATE_CHECKED); label = lv_label_create(all_tracks_btn_);
lv_obj_add_state(all_tracks_btn_, LV_STATE_CHECKED); lv_label_set_text(label, "From here");
all_tracks_ = true; 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_bind(all_tracks_btn_, LV_EVENT_CLICKED, [this](lv_obj_t*) {
lv_obj_set_size(spacer, 1, 4); 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_size(button_container, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_set_layout(button_container, LV_LAYOUT_FLEX); lv_obj_set_layout(button_container, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW); 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_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t* btn = lv_btn_create(button_container); 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_label_set_text(label, "Play now");
lv_group_add_obj(group_, btn); lv_group_add_obj(group_, btn);
@ -112,7 +117,7 @@ AddToQueue::AddToQueue(Screen* host,
label = lv_label_create(root_); label = lv_label_create(root_);
lv_label_set_text(label, "Enqueue"); 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); lv_obj_set_size(spacer, 1, 4);
button_container = lv_obj_create(root_); 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); lv_obj_set_size(spacer, 1, 4);
button_container = lv_obj_create(root_); button_container = lv_obj_create(root_);

@ -16,6 +16,7 @@
#include "font/lv_symbol_def.h" #include "font/lv_symbol_def.h"
#include "lvgl.h" #include "lvgl.h"
#include "misc/lv_anim.h" #include "misc/lv_anim.h"
#include "misc/lv_color.h"
#include "model_top_bar.hpp" #include "model_top_bar.hpp"
#include "screen_menu.hpp" #include "screen_menu.hpp"
@ -30,6 +31,9 @@
#include "hal/lv_hal_disp.h" #include "hal/lv_hal_disp.h"
#include "misc/lv_area.h" #include "misc/lv_area.h"
#include "screen_track_browser.hpp" #include "screen_track_browser.hpp"
#include "source.hpp"
#include "themes.hpp"
#include "track_queue.hpp"
#include "ui_events.hpp" #include "ui_events.hpp"
#include "ui_fsm.hpp" #include "ui_fsm.hpp"
#include "widget_top_bar.hpp" #include "widget_top_bar.hpp"
@ -61,12 +65,17 @@ static void item_select_cb(lv_event_t* ev) {
TrackBrowser::TrackBrowser( TrackBrowser::TrackBrowser(
models::TopBar& top_bar_model, models::TopBar& top_bar_model,
audio::TrackQueue& queue,
std::weak_ptr<database::Database> db, 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) std::future<database::Result<database::IndexRecord>*>&& initial_page)
: db_(db), : queue_(queue),
db_(db),
play_button_(nullptr),
enqueue_button_(nullptr),
list_(nullptr), list_(nullptr),
loading_indicator_(nullptr), loading_indicator_(nullptr),
breadcrumbs_(crumbs),
loading_pos_(END), loading_pos_(END),
loading_page_(move(initial_page)), loading_page_(move(initial_page)),
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_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, 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{ widgets::TopBar::Configuration config{
.show_back_button = true, .show_back_button = true,
.title = title, .title = breadcrumbs_[0],
}; };
auto top_bar = CreateTopBar(content_, config, top_bar_model); auto top_bar = CreateTopBar(content_, config, top_bar_model);
back_button_ = top_bar->button(); back_button_ = top_bar->button();
list_ = lv_list_create(content_); lv_obj_t* scrollable = lv_obj_create(content_);
lv_obj_set_width(list_, lv_pct(100)); lv_obj_set_width(scrollable, lv_pct(100));
lv_obj_set_flex_grow(list_, 1); lv_obj_set_flex_grow(scrollable, 1);
lv_obj_center(list_); 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 { auto TrackBrowser::Tick() -> void {
@ -138,7 +236,12 @@ auto TrackBrowser::OnItemClicked(lv_event_t* ev) -> void {
for (const auto& page : current_pages_) { for (const auto& page : current_pages_) {
for (std::size_t i = 0; i < page->values().size(); i++) { for (std::size_t i = 0; i < page->values().size(); i++) {
if (index == 0) { if (index == 0) {
auto text = page->values()[i]->text();
auto crumbs = breadcrumbs_;
crumbs.push_back(text.value());
events::Ui().Dispatch(internal::RecordSelected{ events::Ui().Dispatch(internal::RecordSelected{
.show_menu = ev->code == LV_EVENT_LONG_PRESSED,
.new_crumbs = crumbs,
.initial_page = initial_page_, .initial_page = initial_page_,
.page = page, .page = page,
.record = i, .record = i,
@ -181,6 +284,7 @@ auto TrackBrowser::AddResults(
lv_obj_t* item = lv_list_add_btn(list_, NULL, text->c_str()); 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_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_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); lv_obj_add_event_cb(item, item_select_cb, LV_EVENT_FOCUSED, this);
if (pos == START) { if (pos == START) {
@ -217,6 +321,12 @@ auto TrackBrowser::AddResults(
lv_group_remove_all_objs(group_); lv_group_remove_all_objs(group_);
lv_group_add_obj(group_, back_button_); 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_); int num_children = lv_obj_get_child_cnt(list_);
for (int i = 0; i < num_children; i++) { for (int i = 0; i < num_children; i++) {
lv_group_add_obj(group_, lv_obj_get_child(list_, i)); lv_group_add_obj(group_, lv_obj_get_child(list_, i));

@ -11,6 +11,7 @@
#include "audio_fsm.hpp" #include "audio_fsm.hpp"
#include "battery.hpp" #include "battery.hpp"
#include "core/lv_obj.h" #include "core/lv_obj.h"
#include "database.hpp"
#include "misc/lv_gc.h" #include "misc/lv_gc.h"
#include "audio_events.hpp" #include "audio_events.hpp"
@ -112,8 +113,12 @@ void UiState::react(const audio::PlaybackUpdate& ev) {
void UiState::react(const audio::QueueUpdate&) { void UiState::react(const audio::QueueUpdate&) {
auto& queue = sServices->track_queue(); auto& queue = sServices->track_queue();
bool had_queue = sPlaybackModel.current_track.get().has_value();
sPlaybackModel.current_track.set(queue.GetCurrent()); sPlaybackModel.current_track.set(queue.GetCurrent());
sPlaybackModel.upcoming_tracks.set(queue.GetUpcoming(10)); sPlaybackModel.upcoming_tracks.set(queue.GetUpcoming(10));
if (!had_queue) {
transit<states::Playing>();
}
} }
void UiState::react(const internal::ControlSchemeChanged&) { void UiState::react(const internal::ControlSchemeChanged&) {
@ -285,14 +290,20 @@ void Browse::react(const internal::RecordSelected& ev) {
return; return;
} }
auto& queue = sServices->track_queue();
auto record = ev.page->values().at(ev.record); auto record = ev.page->values().at(ev.record);
if (record->track()) { if (record->track()) {
ESP_LOGI(kTag, "selected track '%s'", record->text()->c_str()); ESP_LOGI(kTag, "selected track '%s'", record->text()->c_str());
auto& queue = sServices->track_queue();
auto source = std::make_shared<playlist::IndexRecordSource>( auto source = std::make_shared<playlist::IndexRecordSource>(
sServices->database(), ev.initial_page, 0, ev.page, ev.record); sServices->database(), ev.initial_page, 0, ev.page, ev.record);
sCurrentModal.reset( if (ev.show_menu) {
new modals::AddToQueue(sCurrentScreen.get(), queue, source)); sCurrentModal.reset(
new modals::AddToQueue(sCurrentScreen.get(), queue, source));
} else {
queue.Clear();
queue.AddNext(source);
transit<Playing>();
}
} else { } else {
ESP_LOGI(kTag, "selected record '%s'", record->text()->c_str()); ESP_LOGI(kTag, "selected record '%s'", record->text()->c_str());
auto cont = record->Expand(kRecordsPerPage); auto cont = record->Expand(kRecordsPerPage);
@ -300,9 +311,17 @@ void Browse::react(const internal::RecordSelected& ev) {
return; return;
} }
auto query = db->GetPage<database::IndexRecord>(&cont.value()); auto query = db->GetPage<database::IndexRecord>(&cont.value());
std::pmr::string title = record->text().value_or("TODO"); if (ev.show_menu) {
PushScreen(std::make_shared<screens::TrackBrowser>( std::shared_ptr<database::Result<database::IndexRecord>> res{query.get()};
sTopBarModel, sServices->database(), title, std::move(query))); 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()); ESP_LOGI(kTag, "selected index %s", ev.index.name.c_str());
auto query = db->GetTracksByIndex(ev.index, kRecordsPerPage); auto query = db->GetTracksByIndex(ev.index, kRecordsPerPage);
std::pmr::vector<std::pmr::string> crumbs = {ev.index.name};
PushScreen(std::make_shared<screens::TrackBrowser>( 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) { void Browse::react(const internal::BackPressed& ev) {

Loading…
Cancel
Save