parent
230721cd62
commit
7c6eb2997c
@ -0,0 +1,87 @@ |
|||||||
|
#include "luavgl.h" |
||||||
|
#include "private.h" |
||||||
|
#include <src/misc/lv_anim.h> |
||||||
|
#include <src/widgets/lv_bar.h> |
||||||
|
|
||||||
|
static int luavgl_bar_create(lua_State *L) |
||||||
|
{ |
||||||
|
return luavgl_obj_create_helper(L, lv_bar_create); |
||||||
|
} |
||||||
|
|
||||||
|
static void _lv_bar_set_range(void *obj, lua_State *L) |
||||||
|
{ |
||||||
|
int min=0, max=100; |
||||||
|
|
||||||
|
int type = lua_type(L, -1); |
||||||
|
if (type == LUA_TTABLE) { |
||||||
|
lua_getfield(L, -1, "min"); |
||||||
|
min = lua_tointeger(L, -1); |
||||||
|
lua_pop(L, 1); |
||||||
|
lua_getfield(L, -1, "max"); |
||||||
|
max = luavgl_tointeger(L, -1); |
||||||
|
lua_pop(L, 1); |
||||||
|
} |
||||||
|
|
||||||
|
lv_bar_set_range(obj, min, max); |
||||||
|
} |
||||||
|
|
||||||
|
static void _lv_bar_set_value(void *obj, int value) |
||||||
|
{ |
||||||
|
lv_bar_set_value(obj, value, LV_ANIM_OFF); |
||||||
|
} |
||||||
|
|
||||||
|
static const luavgl_value_setter_t bar_property_table[] = { |
||||||
|
{"range", SETTER_TYPE_STACK, {.setter_stack = _lv_bar_set_range}}, |
||||||
|
{"value", SETTER_TYPE_INT, {.setter = (setter_int_t)_lv_bar_set_value}}, |
||||||
|
}; |
||||||
|
|
||||||
|
LUALIB_API int luavgl_bar_set_property_kv(lua_State *L, void *data) |
||||||
|
{ |
||||||
|
lv_obj_t *obj = data; |
||||||
|
int ret = luavgl_set_property(L, obj, bar_property_table); |
||||||
|
|
||||||
|
if (ret == 0) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
/* a base obj property? */ |
||||||
|
ret = luavgl_obj_set_property_kv(L, obj); |
||||||
|
if (ret != 0) { |
||||||
|
debug("unkown property for bar.\n"); |
||||||
|
} |
||||||
|
|
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
static int luavgl_bar_set(lua_State *L) |
||||||
|
{ |
||||||
|
lv_obj_t *obj = luavgl_to_obj(L, 1); |
||||||
|
|
||||||
|
if (!lua_istable(L, -1)) { |
||||||
|
luaL_error(L, "expect a table on 2nd para."); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
luavgl_iterate(L, -1, luavgl_bar_set_property_kv, obj); |
||||||
|
|
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
static int luavgl_bar_tostring(lua_State *L) |
||||||
|
{ |
||||||
|
lv_obj_t *obj = luavgl_to_obj(L, 1); |
||||||
|
lua_pushfstring(L, "lv_bar:%p", obj); |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
static const luaL_Reg luavgl_bar_methods[] = { |
||||||
|
{"set", luavgl_bar_set }, |
||||||
|
{NULL, NULL }, |
||||||
|
}; |
||||||
|
|
||||||
|
static void luavgl_bar_init(lua_State *L) |
||||||
|
{ |
||||||
|
luavgl_obj_newmetatable(L, &lv_bar_class, "lv_bar", luavgl_bar_methods); |
||||||
|
lua_pushcfunction(L, luavgl_bar_tostring); |
||||||
|
lua_setfield(L, -2, "__tostring"); |
||||||
|
lua_pop(L, 1); |
||||||
|
} |
After Width: | Height: | Size: 621 B |
After Width: | Height: | Size: 581 B |
After Width: | Height: | Size: 617 B |
After Width: | Height: | Size: 626 B |
@ -0,0 +1,41 @@ |
|||||||
|
/*
|
||||||
|
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: GPL-3.0-only |
||||||
|
*/ |
||||||
|
|
||||||
|
#include "audio_source.hpp" |
||||||
|
#include "codec.hpp" |
||||||
|
#include "types.hpp" |
||||||
|
|
||||||
|
namespace audio { |
||||||
|
|
||||||
|
TaggedStream::TaggedStream(std::shared_ptr<database::TrackTags> t, |
||||||
|
std::unique_ptr<codecs::IStream> w) |
||||||
|
: codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)) {} |
||||||
|
|
||||||
|
auto TaggedStream::tags() -> std::shared_ptr<database::TrackTags> { |
||||||
|
return tags_; |
||||||
|
} |
||||||
|
|
||||||
|
auto TaggedStream::Read(cpp::span<std::byte> dest) -> ssize_t { |
||||||
|
return wrapped_->Read(dest); |
||||||
|
} |
||||||
|
|
||||||
|
auto TaggedStream::CanSeek() -> bool { |
||||||
|
return wrapped_->CanSeek(); |
||||||
|
} |
||||||
|
|
||||||
|
auto TaggedStream::SeekTo(int64_t destination, SeekFrom from) -> void { |
||||||
|
wrapped_->SeekTo(destination, from); |
||||||
|
} |
||||||
|
|
||||||
|
auto TaggedStream::CurrentPosition() -> int64_t { |
||||||
|
return wrapped_->CurrentPosition(); |
||||||
|
} |
||||||
|
|
||||||
|
auto TaggedStream::SetPreambleFinished() -> void { |
||||||
|
wrapped_->SetPreambleFinished(); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace audio
|
@ -1,41 +0,0 @@ |
|||||||
/*
|
|
||||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
|
||||||
* |
|
||||||
* SPDX-License-Identifier: GPL-3.0-only |
|
||||||
*/ |
|
||||||
|
|
||||||
#pragma once |
|
||||||
|
|
||||||
#include <memory> |
|
||||||
#include <vector> |
|
||||||
|
|
||||||
#include "database.hpp" |
|
||||||
#include "index.hpp" |
|
||||||
#include "lvgl.h" |
|
||||||
|
|
||||||
#include "modal.hpp" |
|
||||||
#include "source.hpp" |
|
||||||
#include "track_queue.hpp" |
|
||||||
|
|
||||||
namespace ui { |
|
||||||
namespace modals { |
|
||||||
|
|
||||||
class AddToQueue : public Modal { |
|
||||||
public: |
|
||||||
AddToQueue(Screen*, |
|
||||||
audio::TrackQueue&, |
|
||||||
std::shared_ptr<playlist::IResetableSource>, |
|
||||||
bool all_tracks_only = false); |
|
||||||
|
|
||||||
private: |
|
||||||
audio::TrackQueue& queue_; |
|
||||||
std::shared_ptr<playlist::IResetableSource> item_; |
|
||||||
lv_obj_t* container_; |
|
||||||
|
|
||||||
lv_obj_t* selected_track_btn_; |
|
||||||
lv_obj_t* all_tracks_btn_; |
|
||||||
bool all_tracks_; |
|
||||||
}; |
|
||||||
|
|
||||||
} // namespace modals
|
|
||||||
} // namespace ui
|
|
@ -1,63 +0,0 @@ |
|||||||
/*
|
|
||||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
|
||||||
* |
|
||||||
* SPDX-License-Identifier: GPL-3.0-only |
|
||||||
*/ |
|
||||||
|
|
||||||
#pragma once |
|
||||||
|
|
||||||
#include <memory> |
|
||||||
#include <vector> |
|
||||||
|
|
||||||
#include "lvgl.h" |
|
||||||
|
|
||||||
#include "screen.hpp" |
|
||||||
|
|
||||||
namespace ui { |
|
||||||
namespace screens { |
|
||||||
|
|
||||||
class Onboarding : public Screen { |
|
||||||
public: |
|
||||||
Onboarding(const std::pmr::string& title, bool show_prev, bool show_next); |
|
||||||
|
|
||||||
private: |
|
||||||
lv_obj_t* window_; |
|
||||||
lv_obj_t* title_; |
|
||||||
lv_obj_t* next_button_; |
|
||||||
lv_obj_t* prev_button_; |
|
||||||
|
|
||||||
protected: |
|
||||||
lv_obj_t* content_; |
|
||||||
}; |
|
||||||
|
|
||||||
namespace onboarding { |
|
||||||
|
|
||||||
class LinkToManual : public Onboarding { |
|
||||||
public: |
|
||||||
LinkToManual(); |
|
||||||
}; |
|
||||||
|
|
||||||
class Controls : public Onboarding { |
|
||||||
public: |
|
||||||
Controls(); |
|
||||||
}; |
|
||||||
|
|
||||||
class MissingSdCard : public Onboarding { |
|
||||||
public: |
|
||||||
MissingSdCard(); |
|
||||||
}; |
|
||||||
|
|
||||||
class FormatSdCard : public Onboarding { |
|
||||||
public: |
|
||||||
FormatSdCard(); |
|
||||||
}; |
|
||||||
|
|
||||||
class InitDatabase : public Onboarding { |
|
||||||
public: |
|
||||||
InitDatabase(); |
|
||||||
}; |
|
||||||
|
|
||||||
} // namespace onboarding
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
@ -1,73 +0,0 @@ |
|||||||
/*
|
|
||||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
|
||||||
* |
|
||||||
* SPDX-License-Identifier: GPL-3.0-only |
|
||||||
*/ |
|
||||||
|
|
||||||
#pragma once |
|
||||||
|
|
||||||
#include <stdint.h> |
|
||||||
#include <sys/_stdint.h> |
|
||||||
#include <memory> |
|
||||||
#include <vector> |
|
||||||
|
|
||||||
#include "bindey/property.h" |
|
||||||
#include "esp_log.h" |
|
||||||
#include "lvgl.h" |
|
||||||
|
|
||||||
#include "database.hpp" |
|
||||||
#include "future_fetcher.hpp" |
|
||||||
#include "model_playback.hpp" |
|
||||||
#include "model_top_bar.hpp" |
|
||||||
#include "screen.hpp" |
|
||||||
#include "track.hpp" |
|
||||||
#include "track_queue.hpp" |
|
||||||
|
|
||||||
namespace ui { |
|
||||||
namespace screens { |
|
||||||
|
|
||||||
/*
|
|
||||||
* The 'Now Playing' / 'Currently Playing' screen that contains information |
|
||||||
* about the current track, as well as playback controls. |
|
||||||
*/ |
|
||||||
class Playing : public Screen { |
|
||||||
public: |
|
||||||
explicit Playing(models::TopBar&, |
|
||||||
models::Playback& playback_model, |
|
||||||
std::weak_ptr<database::Database> db, |
|
||||||
audio::TrackQueue& queue); |
|
||||||
~Playing(); |
|
||||||
|
|
||||||
auto Tick() -> void override; |
|
||||||
|
|
||||||
auto OnFocusAboveFold() -> void; |
|
||||||
auto OnFocusBelowFold() -> void; |
|
||||||
|
|
||||||
Playing(const Playing&) = delete; |
|
||||||
Playing& operator=(const Playing&) = delete; |
|
||||||
|
|
||||||
private: |
|
||||||
auto control_button(lv_obj_t* parent, char* icon) -> lv_obj_t*; |
|
||||||
auto next_up_label(lv_obj_t* parent, const std::pmr::string& text) |
|
||||||
-> lv_obj_t*; |
|
||||||
|
|
||||||
std::weak_ptr<database::Database> db_; |
|
||||||
audio::TrackQueue& queue_; |
|
||||||
|
|
||||||
bindey::property<std::shared_ptr<database::Track>> current_track_; |
|
||||||
bindey::property<std::vector<std::shared_ptr<database::Track>>> next_tracks_; |
|
||||||
|
|
||||||
std::unique_ptr<database::FutureFetcher<std::shared_ptr<database::Track>>> |
|
||||||
new_track_; |
|
||||||
std::unique_ptr< |
|
||||||
database::FutureFetcher<std::vector<std::shared_ptr<database::Track>>>> |
|
||||||
new_next_tracks_; |
|
||||||
|
|
||||||
lv_obj_t* next_up_header_; |
|
||||||
lv_obj_t* next_up_label_; |
|
||||||
lv_obj_t* next_up_hint_; |
|
||||||
lv_obj_t* next_up_container_; |
|
||||||
}; |
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
@ -1,74 +0,0 @@ |
|||||||
/*
|
|
||||||
* 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 "model_top_bar.hpp" |
|
||||||
#include "screen.hpp" |
|
||||||
#include "track_queue.hpp" |
|
||||||
|
|
||||||
namespace ui { |
|
||||||
namespace screens { |
|
||||||
|
|
||||||
class TrackBrowser : public Screen { |
|
||||||
public: |
|
||||||
TrackBrowser( |
|
||||||
models::TopBar& top_bar, |
|
||||||
audio::TrackQueue& queue, |
|
||||||
std::weak_ptr<database::Database> db, |
|
||||||
const std::pmr::vector<std::pmr::string>& breadcrumbs, |
|
||||||
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, |
|
||||||
std::shared_ptr<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>; |
|
||||||
|
|
||||||
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_; |
|
||||||
|
|
||||||
std::shared_ptr<database::Result<database::IndexRecord>> initial_page_; |
|
||||||
std::deque<std::shared_ptr<database::Result<database::IndexRecord>>> |
|
||||||
current_pages_; |
|
||||||
}; |
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
@ -1,182 +0,0 @@ |
|||||||
/*
|
|
||||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
|
||||||
* |
|
||||||
* SPDX-License-Identifier: GPL-3.0-only |
|
||||||
*/ |
|
||||||
|
|
||||||
#include "modal_add_to_queue.hpp" |
|
||||||
|
|
||||||
#include "core/lv_event.h" |
|
||||||
#include "core/lv_obj.h" |
|
||||||
#include "core/lv_obj_tree.h" |
|
||||||
#include "esp_log.h" |
|
||||||
|
|
||||||
#include "core/lv_group.h" |
|
||||||
#include "core/lv_obj_pos.h" |
|
||||||
#include "event_queue.hpp" |
|
||||||
#include "extra/layouts/flex/lv_flex.h" |
|
||||||
#include "extra/widgets/list/lv_list.h" |
|
||||||
#include "extra/widgets/menu/lv_menu.h" |
|
||||||
#include "extra/widgets/spinner/lv_spinner.h" |
|
||||||
#include "extra/widgets/tabview/lv_tabview.h" |
|
||||||
#include "hal/lv_hal_disp.h" |
|
||||||
#include "index.hpp" |
|
||||||
#include "misc/lv_area.h" |
|
||||||
#include "misc/lv_color.h" |
|
||||||
#include "source.hpp" |
|
||||||
#include "themes.hpp" |
|
||||||
#include "track_queue.hpp" |
|
||||||
#include "ui_events.hpp" |
|
||||||
#include "ui_fsm.hpp" |
|
||||||
#include "widget_top_bar.hpp" |
|
||||||
#include "widgets/lv_btn.h" |
|
||||||
#include "widgets/lv_label.h" |
|
||||||
|
|
||||||
namespace ui { |
|
||||||
namespace modals { |
|
||||||
|
|
||||||
AddToQueue::AddToQueue(Screen* host, |
|
||||||
audio::TrackQueue& queue, |
|
||||||
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); |
|
||||||
|
|
||||||
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); |
|
||||||
|
|
||||||
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); |
|
||||||
|
|
||||||
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; |
|
||||||
}); |
|
||||||
|
|
||||||
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_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; |
|
||||||
}); |
|
||||||
|
|
||||||
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); |
|
||||||
lv_obj_set_flex_align(button_container, LV_FLEX_ALIGN_SPACE_EVENLY, |
|
||||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); |
|
||||||
|
|
||||||
lv_obj_t* btn = lv_btn_create(button_container); |
|
||||||
lv_obj_t* label = lv_label_create(btn); |
|
||||||
lv_label_set_text(label, "Play now"); |
|
||||||
lv_group_add_obj(group_, btn); |
|
||||||
|
|
||||||
lv_bind(btn, LV_EVENT_CLICKED, [this](lv_obj_t*) { |
|
||||||
queue_.Clear(); |
|
||||||
if (all_tracks_) { |
|
||||||
queue_.IncludeNext(item_); |
|
||||||
} else { |
|
||||||
auto track = item_->Current(); |
|
||||||
if (track) { |
|
||||||
queue_.AddNext(*track); |
|
||||||
} |
|
||||||
} |
|
||||||
events::Ui().Dispatch(internal::ModalCancelPressed{}); |
|
||||||
events::Ui().Dispatch(internal::ShowNowPlaying{}); |
|
||||||
}); |
|
||||||
|
|
||||||
bool has_queue = queue.GetCurrent().has_value(); |
|
||||||
|
|
||||||
if (has_queue) { |
|
||||||
label = lv_label_create(root_); |
|
||||||
lv_label_set_text(label, "Enqueue"); |
|
||||||
|
|
||||||
lv_obj_t* spacer = lv_obj_create(root_); |
|
||||||
lv_obj_set_size(spacer, 1, 4); |
|
||||||
|
|
||||||
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); |
|
||||||
|
|
||||||
btn = lv_btn_create(button_container); |
|
||||||
label = lv_label_create(btn); |
|
||||||
lv_label_set_text(label, "Next"); |
|
||||||
lv_group_add_obj(group_, btn); |
|
||||||
|
|
||||||
lv_bind(btn, LV_EVENT_CLICKED, [this](lv_obj_t*) { |
|
||||||
if (all_tracks_) { |
|
||||||
queue_.IncludeNext(item_); |
|
||||||
} else { |
|
||||||
queue_.AddNext(item_->Current().value()); |
|
||||||
} |
|
||||||
events::Ui().Dispatch(internal::ModalCancelPressed{}); |
|
||||||
}); |
|
||||||
|
|
||||||
btn = lv_btn_create(button_container); |
|
||||||
label = lv_label_create(btn); |
|
||||||
lv_label_set_text(label, "Last"); |
|
||||||
lv_group_add_obj(group_, btn); |
|
||||||
|
|
||||||
lv_bind(btn, LV_EVENT_CLICKED, [this](lv_obj_t*) { |
|
||||||
if (all_tracks_) { |
|
||||||
queue_.IncludeLast(item_); |
|
||||||
} else { |
|
||||||
queue_.AddLast(item_->Current().value()); |
|
||||||
} |
|
||||||
events::Ui().Dispatch(internal::ModalCancelPressed{}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
lv_obj_t* spacer = lv_obj_create(root_); |
|
||||||
lv_obj_set_size(spacer, 1, 4); |
|
||||||
|
|
||||||
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_END, |
|
||||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); |
|
||||||
|
|
||||||
btn = lv_btn_create(button_container); |
|
||||||
label = lv_label_create(btn); |
|
||||||
lv_label_set_text(label, "Cancel"); |
|
||||||
lv_group_add_obj(group_, btn); |
|
||||||
lv_obj_set_style_text_color(label, lv_palette_main(LV_PALETTE_RED), |
|
||||||
LV_PART_MAIN); |
|
||||||
|
|
||||||
lv_bind(btn, LV_EVENT_CLICKED, [](lv_obj_t*) { |
|
||||||
events::Ui().Dispatch(internal::ModalCancelPressed{}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
} // namespace modals
|
|
||||||
} // namespace ui
|
|
@ -1,146 +0,0 @@ |
|||||||
/*
|
|
||||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
|
||||||
* |
|
||||||
* SPDX-License-Identifier: GPL-3.0-only |
|
||||||
*/ |
|
||||||
|
|
||||||
#include "screen_onboarding.hpp" |
|
||||||
|
|
||||||
#include "core/lv_event.h" |
|
||||||
#include "core/lv_obj_pos.h" |
|
||||||
#include "draw/lv_draw_rect.h" |
|
||||||
#include "event_queue.hpp" |
|
||||||
#include "extra/libs/qrcode/lv_qrcode.h" |
|
||||||
#include "extra/widgets/win/lv_win.h" |
|
||||||
#include "font/lv_symbol_def.h" |
|
||||||
#include "misc/lv_color.h" |
|
||||||
#include "ui_events.hpp" |
|
||||||
#include "widgets/lv_btn.h" |
|
||||||
#include "widgets/lv_label.h" |
|
||||||
#include "widgets/lv_switch.h" |
|
||||||
|
|
||||||
static const char kManualUrl[] = "https://tangara.gay/onboarding"; |
|
||||||
|
|
||||||
namespace ui { |
|
||||||
namespace screens { |
|
||||||
|
|
||||||
static void next_btn_cb(lv_event_t* ev) { |
|
||||||
events::Ui().Dispatch(internal::OnboardingNavigate{.forwards = true}); |
|
||||||
} |
|
||||||
|
|
||||||
static void prev_btn_cb(lv_event_t* ev) { |
|
||||||
events::Ui().Dispatch(internal::OnboardingNavigate{.forwards = false}); |
|
||||||
} |
|
||||||
|
|
||||||
Onboarding::Onboarding(const std::pmr::string& title, |
|
||||||
bool show_prev, |
|
||||||
bool show_next) { |
|
||||||
window_ = lv_win_create(root_, 18); |
|
||||||
if (show_prev) { |
|
||||||
prev_button_ = lv_win_add_btn(window_, LV_SYMBOL_LEFT, 20); |
|
||||||
lv_obj_add_event_cb(prev_button_, prev_btn_cb, LV_EVENT_CLICKED, NULL); |
|
||||||
lv_group_add_obj(group_, prev_button_); |
|
||||||
} |
|
||||||
title_ = lv_win_add_title(window_, title.c_str()); |
|
||||||
if (show_next) { |
|
||||||
next_button_ = lv_win_add_btn(window_, LV_SYMBOL_RIGHT, 20); |
|
||||||
lv_obj_add_event_cb(next_button_, next_btn_cb, LV_EVENT_CLICKED, NULL); |
|
||||||
lv_group_add_obj(group_, next_button_); |
|
||||||
} |
|
||||||
|
|
||||||
content_ = lv_win_get_content(window_); |
|
||||||
lv_obj_set_layout(content_, LV_LAYOUT_FLEX); |
|
||||||
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); |
|
||||||
lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, |
|
||||||
LV_FLEX_ALIGN_CENTER); |
|
||||||
} |
|
||||||
|
|
||||||
namespace onboarding { |
|
||||||
|
|
||||||
LinkToManual::LinkToManual() : Onboarding("Welcome!", false, true) { |
|
||||||
lv_obj_t* intro = lv_label_create(content_); |
|
||||||
lv_label_set_text(intro, "For full instructions, see the manual:"); |
|
||||||
lv_label_set_long_mode(intro, LV_LABEL_LONG_WRAP); |
|
||||||
lv_obj_set_size(intro, lv_pct(100), LV_SIZE_CONTENT); |
|
||||||
|
|
||||||
lv_obj_t* qr = |
|
||||||
lv_qrcode_create(content_, 80, lv_color_black(), lv_color_white()); |
|
||||||
lv_qrcode_update(qr, kManualUrl, sizeof(kManualUrl)); |
|
||||||
} |
|
||||||
|
|
||||||
static void create_radio_button(lv_obj_t* parent, |
|
||||||
const std::pmr::string& text) { |
|
||||||
lv_obj_t* obj = lv_checkbox_create(parent); |
|
||||||
lv_checkbox_set_text(obj, text.c_str()); |
|
||||||
// TODO: radio styling
|
|
||||||
} |
|
||||||
|
|
||||||
Controls::Controls() : Onboarding("Controls", true, true) { |
|
||||||
lv_obj_t* label = lv_label_create(content_); |
|
||||||
lv_label_set_text(label, "this screen changes your control scheme."); |
|
||||||
|
|
||||||
label = lv_label_create(content_); |
|
||||||
lv_label_set_text(label, "how does the touch wheel behave?"); |
|
||||||
|
|
||||||
create_radio_button(content_, "iPod-style"); |
|
||||||
create_radio_button(content_, "Directional"); |
|
||||||
create_radio_button(content_, "One Big Button"); |
|
||||||
|
|
||||||
label = lv_label_create(content_); |
|
||||||
lv_label_set_text(label, "how do the side buttons behave?"); |
|
||||||
|
|
||||||
create_radio_button(content_, "Adjust volume"); |
|
||||||
create_radio_button(content_, "Scroll"); |
|
||||||
} |
|
||||||
|
|
||||||
MissingSdCard::MissingSdCard() : Onboarding("SD Card", true, false) { |
|
||||||
lv_obj_t* label = lv_label_create(content_); |
|
||||||
lv_label_set_text(label, |
|
||||||
"It looks like there isn't an SD card present. Please " |
|
||||||
"insert one to continue."); |
|
||||||
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); |
|
||||||
lv_obj_set_size(label, lv_pct(100), LV_SIZE_CONTENT); |
|
||||||
} |
|
||||||
|
|
||||||
FormatSdCard::FormatSdCard() : Onboarding("SD Card", true, false) { |
|
||||||
lv_obj_t* label = lv_label_create(content_); |
|
||||||
lv_label_set_text(label, |
|
||||||
"It looks like there is an SD card present, but it has not " |
|
||||||
"been formatted. Would you like to format it?"); |
|
||||||
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); |
|
||||||
lv_obj_set_size(label, lv_pct(100), LV_SIZE_CONTENT); |
|
||||||
|
|
||||||
lv_obj_t* button = lv_btn_create(content_); |
|
||||||
label = lv_label_create(button); |
|
||||||
lv_label_set_text(label, "Format"); |
|
||||||
|
|
||||||
lv_obj_t* exfat_con = lv_obj_create(content_); |
|
||||||
lv_obj_set_layout(exfat_con, LV_LAYOUT_FLEX); |
|
||||||
lv_obj_set_size(exfat_con, lv_pct(100), LV_SIZE_CONTENT); |
|
||||||
lv_obj_set_flex_flow(exfat_con, LV_FLEX_FLOW_ROW); |
|
||||||
lv_obj_set_flex_align(exfat_con, LV_FLEX_ALIGN_SPACE_EVENLY, |
|
||||||
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); |
|
||||||
|
|
||||||
label = lv_label_create(exfat_con); |
|
||||||
lv_label_set_text(label, "Use exFAT"); |
|
||||||
lv_switch_create(exfat_con); |
|
||||||
} |
|
||||||
|
|
||||||
InitDatabase::InitDatabase() : Onboarding("Database", true, true) { |
|
||||||
lv_obj_t* label = lv_label_create(content_); |
|
||||||
lv_label_set_text(label, |
|
||||||
"Many of Tangara's browsing features rely building an " |
|
||||||
"index of your music. Would you like to do this now? It " |
|
||||||
"will take some time if you have a large collection."); |
|
||||||
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); |
|
||||||
lv_obj_set_size(label, lv_pct(100), LV_SIZE_CONTENT); |
|
||||||
|
|
||||||
lv_obj_t* button = lv_btn_create(content_); |
|
||||||
label = lv_label_create(button); |
|
||||||
lv_label_set_text(label, "Index"); |
|
||||||
} |
|
||||||
|
|
||||||
} // namespace onboarding
|
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
@ -1,431 +0,0 @@ |
|||||||
/*
|
|
||||||
* Copyright 2023 jacqueline <me@jacqueline.id.au> |
|
||||||
* |
|
||||||
* SPDX-License-Identifier: GPL-3.0-only |
|
||||||
*/ |
|
||||||
|
|
||||||
#include <algorithm> |
|
||||||
#include <memory> |
|
||||||
|
|
||||||
#include "core/lv_obj.h" |
|
||||||
#include "core/lv_obj_scroll.h" |
|
||||||
#include "core/lv_obj_tree.h" |
|
||||||
#include "database.hpp" |
|
||||||
#include "event_queue.hpp" |
|
||||||
#include "extra/layouts/flex/lv_flex.h" |
|
||||||
#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 "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 "source.hpp" |
|
||||||
#include "themes.hpp" |
|
||||||
#include "track_queue.hpp" |
|
||||||
#include "ui_events.hpp" |
|
||||||
#include "ui_fsm.hpp" |
|
||||||
#include "widget_top_bar.hpp" |
|
||||||
#include "widgets/lv_label.h" |
|
||||||
|
|
||||||
[[maybe_unused]] static constexpr char kTag[] = "browser"; |
|
||||||
|
|
||||||
static constexpr int kMaxPages = 4; |
|
||||||
static constexpr int kPageBuffer = 6; |
|
||||||
|
|
||||||
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( |
|
||||||
models::TopBar& top_bar_model, |
|
||||||
audio::TrackQueue& queue, |
|
||||||
std::weak_ptr<database::Database> db, |
|
||||||
const std::pmr::vector<std::pmr::string>& crumbs, |
|
||||||
std::future<database::Result<database::IndexRecord>*>&& initial_page) |
|
||||||
: 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_(), |
|
||||||
current_pages_() { |
|
||||||
lv_obj_set_layout(content_, LV_LAYOUT_FLEX); |
|
||||||
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); |
|
||||||
lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, |
|
||||||
LV_FLEX_ALIGN_CENTER); |
|
||||||
|
|
||||||
widgets::TopBar::Configuration config{ |
|
||||||
.show_back_button = true, |
|
||||||
.title = breadcrumbs_[0], |
|
||||||
}; |
|
||||||
auto top_bar = CreateTopBar(content_, config, top_bar_model); |
|
||||||
back_button_ = top_bar->button(); |
|
||||||
|
|
||||||
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.
|
|
||||||
// TODO/FIXME: this doesn't actually turn off the scrollbar, it seems.
|
|
||||||
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 { |
|
||||||
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) { |
|
||||||
std::shared_ptr<database::Result<database::IndexRecord>> 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 res = GetItemIndex(lv_event_get_target(ev)); |
|
||||||
if (!res) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
auto index = *res; |
|
||||||
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, |
|
||||||
}); |
|
||||||
return; |
|
||||||
} |
|
||||||
index--; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
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, |
|
||||||
std::shared_ptr<database::Result<database::IndexRecord>> results) -> void { |
|
||||||
if (loading_indicator_ != nullptr) { |
|
||||||
lv_obj_del(loading_indicator_); |
|
||||||
loading_indicator_ = nullptr; |
|
||||||
} |
|
||||||
|
|
||||||
if (initial_page_ == nullptr) { |
|
||||||
initial_page_ = results; |
|
||||||
} |
|
||||||
|
|
||||||
auto fn = [&](const std::shared_ptr<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_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) { |
|
||||||
lv_obj_move_to_index(item, 0); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
lv_obj_t* focused = lv_group_get_focused(group_); |
|
||||||
|
|
||||||
// Adding objects at the start of the list will artificially scroll the list
|
|
||||||
// up. Scroll it down by the height we're adding so that the user doesn't
|
|
||||||
// notice any jank.
|
|
||||||
if (pos == START) { |
|
||||||
int num_to_add = results->values().size(); |
|
||||||
// Assuming that all items are the same height, this item's y pos should be
|
|
||||||
// exactly the height of the new items.
|
|
||||||
lv_obj_t* representative_item = lv_obj_get_child(list_, num_to_add); |
|
||||||
if (representative_item != nullptr) { |
|
||||||
int scroll_adjustment = lv_obj_get_y(representative_item); |
|
||||||
lv_obj_scroll_by(list_, 0, -scroll_adjustment, LV_ANIM_OFF); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
switch (pos) { |
|
||||||
case START: |
|
||||||
std::for_each(results->values().rbegin(), results->values().rend(), fn); |
|
||||||
current_pages_.push_front(results); |
|
||||||
break; |
|
||||||
case END: |
|
||||||
std::for_each(results->values().begin(), results->values().end(), fn); |
|
||||||
current_pages_.push_back(results); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
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)); |
|
||||||
} |
|
||||||
lv_group_focus_obj(focused); |
|
||||||
} |
|
||||||
|
|
||||||
auto TrackBrowser::DropPage(Position pos) -> void { |
|
||||||
if (pos == START) { |
|
||||||
// Removing objects from the start of the list will artificially scroll the
|
|
||||||
// list down. Scroll it up by the height we're removing so that the user
|
|
||||||
// doesn't notice any jank.
|
|
||||||
int num_to_remove = current_pages_.front()->values().size(); |
|
||||||
lv_obj_t* new_top_obj = lv_obj_get_child(list_, num_to_remove); |
|
||||||
if (new_top_obj != nullptr) { |
|
||||||
int scroll_adjustment = lv_obj_get_y(new_top_obj); |
|
||||||
lv_obj_scroll_by(list_, 0, scroll_adjustment, LV_ANIM_OFF); |
|
||||||
} |
|
||||||
|
|
||||||
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; |
|
||||||
} |
|
||||||
|
|
||||||
std::optional<database::Continuation> cont; |
|
||||||
switch (pos) { |
|
||||||
case START: |
|
||||||
cont = current_pages_.front()->prev_page(); |
|
||||||
break; |
|
||||||
case END: |
|
||||||
cont = current_pages_.back()->next_page(); |
|
||||||
break; |
|
||||||
} |
|
||||||
if (!cont) { |
|
||||||
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; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
loading_pos_ = pos; |
|
||||||
loading_page_ = db->GetPage<database::IndexRecord>(&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 {}; |
|
||||||
} |
|
||||||
|
|
||||||
} // namespace screens
|
|
||||||
} // namespace ui
|
|
Loading…
Reference in new issue