parent
80170642ea
commit
191441ebe2
@ -0,0 +1,29 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <memory> |
||||
|
||||
#include "lvgl.h" |
||||
|
||||
#include "database.hpp" |
||||
#include "screen.hpp" |
||||
|
||||
namespace ui { |
||||
namespace screens { |
||||
|
||||
class Playing : public Screen { |
||||
public: |
||||
explicit Playing(database::Track t); |
||||
~Playing(); |
||||
|
||||
private: |
||||
database::Track track_; |
||||
}; |
||||
|
||||
} // namespace screens
|
||||
} // namespace ui
|
@ -0,0 +1,64 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <deque> |
||||
#include <memory> |
||||
#include <string> |
||||
#include <vector> |
||||
|
||||
#include "lvgl.h" |
||||
|
||||
#include "database.hpp" |
||||
#include "screen.hpp" |
||||
|
||||
namespace ui { |
||||
namespace screens { |
||||
|
||||
class TrackBrowser : public Screen { |
||||
public: |
||||
TrackBrowser( |
||||
std::weak_ptr<database::Database> db, |
||||
const std::string& title, |
||||
std::future<database::Result<database::IndexRecord>*>&& initial_page); |
||||
~TrackBrowser() {} |
||||
|
||||
auto Tick() -> void override; |
||||
|
||||
auto OnItemSelected(lv_event_t* ev) -> void; |
||||
auto OnItemClicked(lv_event_t* ev) -> void; |
||||
|
||||
private: |
||||
enum Position { |
||||
START = 0, |
||||
END = 1, |
||||
}; |
||||
auto AddLoadingIndictor(Position pos) -> void; |
||||
auto AddResults(Position pos, database::Result<database::IndexRecord>*) |
||||
-> void; |
||||
auto DropPage(Position pos) -> void; |
||||
auto FetchNewPage(Position pos) -> void; |
||||
|
||||
auto GetNumRecords() -> std::size_t; |
||||
auto GetItemIndex(lv_obj_t* obj) -> std::optional<std::size_t>; |
||||
auto GetRecordByIndex(std::size_t index) |
||||
-> std::optional<database::IndexRecord>; |
||||
|
||||
std::weak_ptr<database::Database> db_; |
||||
lv_obj_t* list_; |
||||
lv_obj_t* loading_indicator_; |
||||
|
||||
std::optional<Position> loading_pos_; |
||||
std::optional<std::future<database::Result<database::IndexRecord>*>> |
||||
loading_page_; |
||||
|
||||
std::deque<std::unique_ptr<database::Result<database::IndexRecord>>> |
||||
current_pages_; |
||||
}; |
||||
|
||||
} // namespace screens
|
||||
} // namespace ui
|
@ -0,0 +1,43 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#include "screen_playing.hpp" |
||||
|
||||
#include "esp_log.h" |
||||
#include "lvgl.h" |
||||
|
||||
#include "core/lv_group.h" |
||||
#include "core/lv_obj_pos.h" |
||||
#include "event_queue.hpp" |
||||
#include "extra/widgets/list/lv_list.h" |
||||
#include "extra/widgets/menu/lv_menu.h" |
||||
#include "extra/widgets/spinner/lv_spinner.h" |
||||
#include "hal/lv_hal_disp.h" |
||||
#include "index.hpp" |
||||
#include "misc/lv_area.h" |
||||
#include "track.hpp" |
||||
#include "ui_events.hpp" |
||||
#include "ui_fsm.hpp" |
||||
#include "widgets/lv_label.h" |
||||
|
||||
namespace ui { |
||||
namespace screens { |
||||
|
||||
Playing::Playing(database::Track track) : track_(track) { |
||||
lv_obj_t* container = lv_obj_create(root_); |
||||
lv_obj_set_align(container, LV_ALIGN_CENTER); |
||||
lv_obj_set_size(container, LV_SIZE_CONTENT, LV_SIZE_CONTENT); |
||||
|
||||
// bro idk
|
||||
lv_obj_t* label = lv_label_create(container); |
||||
lv_label_set_text_static(label, track.TitleOrFilename().c_str()); |
||||
lv_obj_set_align(label, LV_ALIGN_CENTER); |
||||
} |
||||
|
||||
Playing::~Playing() {} |
||||
|
||||
} // namespace screens
|
||||
} // namespace ui
|
@ -0,0 +1,266 @@ |
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||
* |
||||
* SPDX-License-Identifier: GPL-3.0-only |
||||
*/ |
||||
|
||||
#include <algorithm> |
||||
#include <memory> |
||||
|
||||
#include "database.hpp" |
||||
#include "event_queue.hpp" |
||||
#include "lvgl.h" |
||||
#include "screen_menu.hpp" |
||||
|
||||
#include "core/lv_event.h" |
||||
#include "esp_log.h" |
||||
|
||||
#include "core/lv_group.h" |
||||
#include "core/lv_obj_pos.h" |
||||
#include "extra/widgets/list/lv_list.h" |
||||
#include "extra/widgets/menu/lv_menu.h" |
||||
#include "extra/widgets/spinner/lv_spinner.h" |
||||
#include "hal/lv_hal_disp.h" |
||||
#include "misc/lv_area.h" |
||||
#include "screen_track_browser.hpp" |
||||
#include "ui_events.hpp" |
||||
#include "ui_fsm.hpp" |
||||
#include "widgets/lv_label.h" |
||||
|
||||
static constexpr char kTag[] = "browser"; |
||||
|
||||
static constexpr int kMaxPages = 3; |
||||
static constexpr int kPageBuffer = 5; |
||||
|
||||
namespace ui { |
||||
namespace screens { |
||||
|
||||
static void item_click_cb(lv_event_t* ev) { |
||||
if (ev->user_data == NULL) { |
||||
return; |
||||
} |
||||
TrackBrowser* instance = reinterpret_cast<TrackBrowser*>(ev->user_data); |
||||
instance->OnItemClicked(ev); |
||||
} |
||||
|
||||
static void item_select_cb(lv_event_t* ev) { |
||||
if (ev->user_data == NULL) { |
||||
return; |
||||
} |
||||
TrackBrowser* instance = reinterpret_cast<TrackBrowser*>(ev->user_data); |
||||
instance->OnItemSelected(ev); |
||||
} |
||||
|
||||
TrackBrowser::TrackBrowser( |
||||
std::weak_ptr<database::Database> db, |
||||
const std::string& title, |
||||
std::future<database::Result<database::IndexRecord>*>&& initial_page) |
||||
: db_(db), |
||||
list_(nullptr), |
||||
loading_indicator_(nullptr), |
||||
loading_pos_(END), |
||||
loading_page_(std::move(initial_page)), |
||||
current_pages_() { |
||||
lv_obj_t* title_obj = lv_label_create(root_); |
||||
lv_label_set_text(title_obj, title.c_str()); |
||||
|
||||
list_ = lv_list_create(root_); |
||||
lv_obj_set_size(list_, lv_disp_get_hor_res(NULL), lv_disp_get_ver_res(NULL)); |
||||
lv_obj_center(list_); |
||||
} |
||||
|
||||
auto TrackBrowser::Tick() -> void { |
||||
if (!loading_page_) { |
||||
return; |
||||
} |
||||
if (!loading_page_->valid()) { |
||||
// TODO(jacqueline): error case.
|
||||
return; |
||||
} |
||||
if (loading_page_->wait_for(std::chrono::seconds(0)) == |
||||
std::future_status::ready) { |
||||
auto result = loading_page_->get(); |
||||
AddResults(loading_pos_.value_or(END), result); |
||||
|
||||
loading_page_.reset(); |
||||
loading_pos_.reset(); |
||||
} |
||||
} |
||||
|
||||
auto TrackBrowser::OnItemSelected(lv_event_t* ev) -> void { |
||||
auto index = GetItemIndex(lv_event_get_target(ev)); |
||||
if (!index) { |
||||
return; |
||||
} |
||||
if (index < kPageBuffer) { |
||||
FetchNewPage(START); |
||||
return; |
||||
} |
||||
if (index > GetNumRecords() - kPageBuffer) { |
||||
FetchNewPage(END); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
auto TrackBrowser::OnItemClicked(lv_event_t* ev) -> void { |
||||
auto index = GetItemIndex(lv_event_get_target(ev)); |
||||
if (!index) { |
||||
return; |
||||
} |
||||
auto record = GetRecordByIndex(*index); |
||||
if (!record) { |
||||
return; |
||||
} |
||||
ESP_LOGI(kTag, "clicked item %u (%s)", *index, |
||||
record->text().value_or("[nil]").c_str()); |
||||
events::Dispatch<internal::RecordSelected, UiState>( |
||||
internal::RecordSelected{.record = *record}); |
||||
} |
||||
|
||||
auto TrackBrowser::AddLoadingIndictor(Position pos) -> void { |
||||
if (loading_indicator_) { |
||||
return; |
||||
} |
||||
loading_indicator_ = lv_list_add_text(list_, "Loading..."); |
||||
if (pos == START) { |
||||
lv_obj_move_to_index(loading_indicator_, 0); |
||||
} |
||||
} |
||||
|
||||
auto TrackBrowser::AddResults(Position pos, |
||||
database::Result<database::IndexRecord>* results) |
||||
-> void { |
||||
if (loading_indicator_ != nullptr) { |
||||
lv_obj_del(loading_indicator_); |
||||
loading_indicator_ = nullptr; |
||||
} |
||||
|
||||
auto fn = [&](const database::IndexRecord& record) { |
||||
auto text = record.text(); |
||||
if (!text) { |
||||
// TODO(jacqueline): Display category-specific text.
|
||||
text = "[ no data ]"; |
||||
} |
||||
lv_obj_t* item = lv_list_add_btn(list_, NULL, text->c_str()); |
||||
lv_obj_add_event_cb(item, item_click_cb, LV_EVENT_CLICKED, this); |
||||
lv_obj_add_event_cb(item, item_select_cb, LV_EVENT_FOCUSED, this); |
||||
lv_group_add_obj(group_, item); |
||||
if (pos == START) { |
||||
lv_obj_move_to_index(item, 0); |
||||
} |
||||
}; |
||||
|
||||
switch (pos) { |
||||
case START: |
||||
std::for_each(results->values().rbegin(), results->values().rend(), fn); |
||||
current_pages_.emplace_front(results); |
||||
break; |
||||
case END: |
||||
std::for_each(results->values().begin(), results->values().end(), fn); |
||||
current_pages_.emplace_back(results); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
auto TrackBrowser::DropPage(Position pos) -> void { |
||||
if (pos == START) { |
||||
for (int i = 0; i < current_pages_.front()->values().size(); i++) { |
||||
lv_obj_t* item = lv_obj_get_child(list_, 0); |
||||
if (item == NULL) { |
||||
continue; |
||||
} |
||||
lv_obj_del(item); |
||||
} |
||||
current_pages_.pop_front(); |
||||
} else if (pos == END) { |
||||
for (int i = 0; i < current_pages_.back()->values().size(); i++) { |
||||
lv_obj_t* item = lv_obj_get_child(list_, lv_obj_get_child_cnt(list_) - 1); |
||||
if (item == NULL) { |
||||
continue; |
||||
} |
||||
lv_group_remove_obj(item); |
||||
lv_obj_del(item); |
||||
} |
||||
current_pages_.pop_back(); |
||||
} |
||||
} |
||||
|
||||
auto TrackBrowser::FetchNewPage(Position pos) -> void { |
||||
if (loading_page_) { |
||||
return; |
||||
} |
||||
auto db = db_.lock(); |
||||
if (!db) { |
||||
return; |
||||
} |
||||
|
||||
// If we already have a complete set of pages, drop the page that's furthest
|
||||
// away.
|
||||
if (current_pages_.size() >= kMaxPages) { |
||||
switch (pos) { |
||||
case START: |
||||
DropPage(END); |
||||
break; |
||||
case END: |
||||
DropPage(START); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
std::optional<database::Continuation<database::IndexRecord>> cont; |
||||
switch (pos) { |
||||
case START: |
||||
cont = current_pages_.front()->prev_page(); |
||||
break; |
||||
case END: |
||||
cont = current_pages_.back()->next_page(); |
||||
break; |
||||
} |
||||
if (!cont) { |
||||
return; |
||||
} |
||||
|
||||
loading_pos_ = pos; |
||||
loading_page_ = db->GetPage(&cont.value()); |
||||
} |
||||
|
||||
auto TrackBrowser::GetNumRecords() -> std::size_t { |
||||
return lv_obj_get_child_cnt(list_) - (loading_indicator_ != nullptr ? 1 : 0); |
||||
} |
||||
|
||||
auto TrackBrowser::GetItemIndex(lv_obj_t* obj) -> std::optional<std::size_t> { |
||||
std::size_t child_count = lv_obj_get_child_cnt(list_); |
||||
std::size_t index = 0; |
||||
for (int i = 0; i < child_count; i++) { |
||||
lv_obj_t* child = lv_obj_get_child(list_, i); |
||||
if (child == loading_indicator_) { |
||||
continue; |
||||
} |
||||
if (child == obj) { |
||||
return index; |
||||
} |
||||
index++; |
||||
} |
||||
return {}; |
||||
} |
||||
|
||||
auto TrackBrowser::GetRecordByIndex(std::size_t index) |
||||
-> std::optional<database::IndexRecord> { |
||||
std::size_t current_index = 0; |
||||
for (const auto& page : current_pages_) { |
||||
if (index > current_index + page->values().size()) { |
||||
current_index += page->values().size(); |
||||
continue; |
||||
} |
||||
if (index < current_index) { |
||||
// uhhh
|
||||
break; |
||||
} |
||||
std::size_t index_in_page = index - current_index; |
||||
return page->values().at(index_in_page); |
||||
} |
||||
return {}; |
||||
} |
||||
|
||||
} // namespace screens
|
||||
} // namespace ui
|
Loading…
Reference in new issue