diff --git a/lib/lvgl/src/extra/widgets/list/lv_list.c b/lib/lvgl/src/extra/widgets/list/lv_list.c index 29355fd3..1d58615f 100644 --- a/lib/lvgl/src/extra/widgets/list/lv_list.c +++ b/lib/lvgl/src/extra/widgets/list/lv_list.c @@ -91,7 +91,7 @@ lv_obj_t * lv_list_add_btn(lv_obj_t * list, const void * icon, const char * txt) if(txt) { lv_obj_t * label = lv_label_create(obj); lv_label_set_text(label, txt); - lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_label_set_long_mode(label, LV_LABEL_LONG_DOT); lv_obj_set_flex_grow(label, 1); } diff --git a/lua/assets/audio.png b/lua/assets/audio.png index b8ad9071..6151307f 100644 Binary files a/lua/assets/audio.png and b/lua/assets/audio.png differ diff --git a/lua/assets/battery_20.png b/lua/assets/battery_20.png index 9012376f..3a702397 100644 Binary files a/lua/assets/battery_20.png and b/lua/assets/battery_20.png differ diff --git a/lua/assets/battery_40.png b/lua/assets/battery_40.png index 88a0b448..ae7e3d7b 100644 Binary files a/lua/assets/battery_40.png and b/lua/assets/battery_40.png differ diff --git a/lua/assets/battery_60.png b/lua/assets/battery_60.png index d86c997a..44ba7954 100644 Binary files a/lua/assets/battery_60.png and b/lua/assets/battery_60.png differ diff --git a/lua/assets/battery_80.png b/lua/assets/battery_80.png index 344b3703..33bae9fc 100644 Binary files a/lua/assets/battery_80.png and b/lua/assets/battery_80.png differ diff --git a/lua/assets/battery_empty.png b/lua/assets/battery_empty.png index c9176e8c..5f3bfdbc 100644 Binary files a/lua/assets/battery_empty.png and b/lua/assets/battery_empty.png differ diff --git a/lua/assets/battery_full.png b/lua/assets/battery_full.png index 57122a23..f5da13a3 100644 Binary files a/lua/assets/battery_full.png and b/lua/assets/battery_full.png differ diff --git a/lua/assets/bt.png b/lua/assets/bt.png index 180e6b3a..73f3179f 100644 Binary files a/lua/assets/bt.png and b/lua/assets/bt.png differ diff --git a/lua/assets/bt_conn.png b/lua/assets/bt_conn.png index 7a5f2b27..91f9964d 100644 Binary files a/lua/assets/bt_conn.png and b/lua/assets/bt_conn.png differ diff --git a/lua/assets/pause.png b/lua/assets/pause.png index ec388cd5..f1a3d8cb 100644 Binary files a/lua/assets/pause.png and b/lua/assets/pause.png differ diff --git a/lua/assets/play.png b/lua/assets/play.png index 0d0bb34d..81927a8a 100644 Binary files a/lua/assets/play.png and b/lua/assets/play.png differ diff --git a/lua/browser.lua b/lua/browser.lua new file mode 100644 index 00000000..380922a8 --- /dev/null +++ b/lua/browser.lua @@ -0,0 +1,113 @@ +local lvgl = require("lvgl") +local widgets = require("widgets") +local legacy_ui = require("legacy_ui") +local database = require("database") +local backstack = require("backstack") + +local browser = {} + +function browser.create(opts) + local screen = {} + screen.root = lvgl.Object(nil, { + flex = { + flex_direction = "column", + flex_wrap = "wrap", + justify_content = "flex-start", + align_items = "flex-start", + align_content = "flex-start", + }, + w = lvgl.HOR_RES(), + h = lvgl.VER_RES(), + }) + screen.root:center() + + screen.status_bar = widgets.StatusBar(screen.root, { + back_cb = backstack.pop, + title = opts.title, + }) + + if opts.breadcrumb then + local header = screen.root:Object { + flex = { + flex_direction = "column", + flex_wrap = "wrap", + justify_content = "flex-start", + align_items = "flex-start", + align_content = "flex-start", + }, + w = lvgl.HOR_RES(), + h = lvgl.SIZE_CONTENT, + pad_left = 4, + pad_right = 4, + pad_top = 2, + pad_bottom = 2, + bg_opa = lvgl.OPA(100), + bg_color = "#f3e5f5", + scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, + } + + header:Label { text = opts.breadcrumb } + + local buttons = header:Object({ + flex = { + flex_direction = "row", + flex_wrap = "wrap", + justify_content = "flex-end", + align_items = "center", + align_content = "center", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_column = 4, + }) + local enqueue = buttons:Button {} + enqueue:Label { text = "Enqueue" } + enqueue:add_flag(lvgl.FLAG.HIDDEN) + local play = buttons:Button {} + play:Label { text = "Play all" } + end + + screen.list = lvgl.List(screen.root, { + w = lvgl.PCT(100), + h = lvgl.PCT(100), + flex_grow = 1, + scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, + }) + + screen.focused_item = 0 + screen.last_item = 0 + screen.add_item = function(item) + if not item then return end + screen.last_item = screen.last_item + 1 + local this_item = screen.last_item + local btn = screen.list:add_btn(nil, tostring(item)) + btn:onClicked(function() + local contents = item:contents() + if type(contents) == "function" then + backstack.push(function() + return browser.create({ + title = opts.title, + iterator = contents, + breadcrumb = tostring(item), + }) + end) + else + print("selected track", contents) + end + end) + btn:onevent(lvgl.EVENT.FOCUSED, function() + screen.focused_item = this_item + if screen.last_item - 5 < this_item then + opts.iterator(screen.add_item) + end + end) + end + + for _ = 1, 8 do + opts.iterator(screen.add_item) + end + + return screen +end + +return browser.create diff --git a/lua/main_menu.lua b/lua/main_menu.lua index e38ed2c1..7c236a23 100644 --- a/lua/main_menu.lua +++ b/lua/main_menu.lua @@ -2,6 +2,8 @@ local lvgl = require("lvgl") local widgets = require("widgets") local legacy_ui = require("legacy_ui") local database = require("database") +local backstack = require("backstack") +local browser = require("browser") return function() local menu = {} @@ -35,6 +37,12 @@ return function() local btn = menu.list:add_btn(nil, tostring(idx)) btn:onClicked(function() legacy_ui.open_browse(id); + -- backstack.push(function() + -- return browser { + -- title = tostring(idx), + -- iterator = idx:iter() + -- } + -- end) end) end diff --git a/lua/widgets.lua b/lua/widgets.lua index 9807bc09..76f7c839 100644 --- a/lua/widgets.lua +++ b/lua/widgets.lua @@ -17,23 +17,20 @@ function widgets.StatusBar(parent, opts) }, w = lvgl.HOR_RES(), h = lvgl.SIZE_CONTENT, - bg_opa = lvgl.OPA(100), - bg_color = "#fff", pad_top = 1, pad_bottom = 1, pad_column = 1, - shadow_width = 6, - shadow_opa = lvgl.OPA(50), - shaddow_ofs_x = 0, + bg_opa = lvgl.OPA(100), + bg_color = "#e1bee7", scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, } if opts.back_cb then - status_bar.back = status_bar.root:Label { + status_bar.back = status_bar.root:Button { w = lvgl.SIZE_CONTENT, h = 12, - text = "<", } + status_bar.back:Label({ text = "<", align = lvgl.ALIGN.CENTER }) status_bar.back:onClicked(opts.back_cb) end @@ -44,7 +41,7 @@ function widgets.StatusBar(parent, opts) flex_grow = 1, } if opts.title then - status_bar.title.set { text = opts.title } + status_bar.title:set { text = opts.title } end status_bar.playing = status_bar.root:Image {} diff --git a/src/database/database.cpp b/src/database/database.cpp index 88ae7bbe..dad983d0 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -846,8 +846,7 @@ auto IndexRecord::Expand(std::size_t page_size) const if (track_) { return {}; } - IndexKey::Header new_header = ExpandHeader(key_.header, key_.item); - std::string new_prefix = EncodeIndexPrefix(new_header); + std::string new_prefix = EncodeIndexPrefix(ExpandHeader()); return Continuation{ .prefix = {new_prefix.data(), new_prefix.size()}, .start_key = {new_prefix.data(), new_prefix.size()}, @@ -857,6 +856,10 @@ auto IndexRecord::Expand(std::size_t page_size) const }; } +auto IndexRecord::ExpandHeader() const -> IndexKey::Header { + return ::database::ExpandHeader(key_.header, key_.item); +} + Iterator::Iterator(std::weak_ptr db, const IndexInfo& idx) : db_(db), pos_mutex_(), current_pos_(), prev_pos_() { std::string prefix = EncodeIndexPrefix( @@ -887,6 +890,11 @@ auto Iterator::Next(Callback cb) -> void { db->dbGetPage(*current_pos_)}; prev_pos_ = current_pos_; current_pos_ = res->next_page(); + if (!res || res->values().empty() || !res->values()[0]) { + ESP_LOGI(kTag, "dropping empty result"); + InvokeNull(cb); + return; + } std::invoke(cb, *res->values()[0]); }); } diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index 63014bed..e18701eb 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -80,6 +80,7 @@ class IndexRecord { auto track() const -> std::optional; auto Expand(std::size_t) const -> std::optional; + auto ExpandHeader() const -> IndexKey::Header; private: IndexKey key_; diff --git a/src/lua/lua_database.cpp b/src/lua/lua_database.cpp index d8ae86f6..79916115 100644 --- a/src/lua/lua_database.cpp +++ b/src/lua/lua_database.cpp @@ -20,7 +20,9 @@ #include "event_queue.hpp" #include "index.hpp" #include "property.hpp" +#include "records.hpp" #include "service_locator.hpp" +#include "track.hpp" #include "ui_events.hpp" namespace lua { @@ -55,6 +57,42 @@ static auto indexes(lua_State* state) -> int { static const struct luaL_Reg kDatabaseFuncs[] = {{"indexes", indexes}, {NULL, NULL}}; +/* + * Struct to be used as userdata for the Lua representation of database records. + * In order to push these large values into PSRAM as much as possible, memory + * for these is allocated and managed by Lua. This struct must therefore be + * trivially copyable. + */ +struct LuaRecord { + database::TrackId id_or_zero; + database::IndexKey::Header header_at_next_depth; + size_t text_size; + char text[]; +}; + +static_assert(std::is_trivially_copyable_v == true); + +static auto push_lua_record(lua_State* L, const database::IndexRecord& r) + -> void { + // Bake out the text into something concrete. + auto text = r.text().value_or(""); + + // Create and init the userdata. + LuaRecord* record = reinterpret_cast( + lua_newuserdata(L, sizeof(LuaRecord) + text.size())); + luaL_setmetatable(L, kDbRecordMetatable); + + // Init all the fields + *record = { + .id_or_zero = r.track().value_or(0), + .header_at_next_depth = r.ExpandHeader(), + .text_size = text.size(), + }; + + // Copy the string data across. + std::memcpy(record->text, text.data(), text.size()); +} + static auto db_iterate(lua_State* state) -> int { luaL_checktype(state, 1, LUA_TFUNCTION); int callback_ref = luaL_ref(state, LUA_REGISTRYINDEX); @@ -66,11 +104,7 @@ static auto db_iterate(lua_State* state) -> int { events::Ui().RunOnTask([=]() { lua_rawgeti(state, LUA_REGISTRYINDEX, callback_ref); if (res) { - database::IndexRecord** record = - reinterpret_cast( - lua_newuserdata(state, sizeof(uintptr_t))); - *record = new database::IndexRecord(*res); - luaL_setmetatable(state, kDbRecordMetatable); + push_lua_record(state, *res); } else { lua_pushnil(state); } @@ -105,40 +139,37 @@ static auto push_iterator( lua_pushcclosure(state, db_iterate, 1); } + static auto record_text(lua_State* state) -> int { - database::IndexRecord* data = *reinterpret_cast( + LuaRecord* data = reinterpret_cast( luaL_checkudata(state, 1, kDbRecordMetatable)); - lua_pushstring(state, - data->text().value_or("[tell jacqueline u saw this]").c_str()); + lua_pushlstring(state, data->text, data->text_size); return 1; } static auto record_contents(lua_State* state) -> int { - database::IndexRecord* data = *reinterpret_cast( + LuaRecord* data = reinterpret_cast( luaL_checkudata(state, 1, kDbRecordMetatable)); - if (data->track()) { - lua_pushinteger(state, *data->track()); + if (data->id_or_zero) { + lua_pushinteger(state, data->id_or_zero); } else { - push_iterator(state, data->Expand(1).value()); + std::string p = database::EncodeIndexPrefix(data->header_at_next_depth); + push_iterator(state, database::Continuation{ + .prefix = {p.data(), p.size()}, + .start_key = {p.data(), p.size()}, + .forward = true, + .was_prev_forward = true, + .page_size = 1, + }); } return 1; } -static auto record_gc(lua_State* state) -> int { - database::IndexRecord** data = reinterpret_cast( - luaL_checkudata(state, 1, kDbRecordMetatable)); - if (data != NULL) { - delete *data; - } - return 0; -} - static const struct luaL_Reg kDbRecordFuncs[] = {{"title", record_text}, {"contents", record_contents}, {"__tostring", record_text}, - {"__gc", record_gc}, {NULL, NULL}}; static auto index_name(lua_State* state) -> int { @@ -207,4 +238,4 @@ auto RegisterDatabaseModule(lua_State* s) -> void { lua_pop(s, 1); } -} // namespace lua \ No newline at end of file +} // namespace lua diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index d5de53f0..ed0624df 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -219,6 +219,7 @@ void Lua::entry() { {"pop", [&](lua_State* s) { return PopLuaScreen(s); }}, }); + sCurrentScreen.reset(); sLua->RunScript("/lua/main.lua"); } } @@ -243,12 +244,6 @@ auto Lua::PushLuaScreen(lua_State* s) -> int { // Store the reference for the table the constructor returned. new_screen->SetObjRef(s); - // Ensure that we don't pollute the new screen's group. We leave the luavgl - // root alone. - // FIXME: maybe we should set the luavgl root to some catch-all that throws - // when anything is added to it? this may help catch bugs! - lv_group_set_default(NULL); - // Finally, push the now-initialised screen as if it were a regular C++ // screen. PushScreen(new_screen); @@ -256,12 +251,16 @@ auto Lua::PushLuaScreen(lua_State* s) -> int { return 0; } -auto Lua::PopLuaScreen(lua_State*) -> int { +auto Lua::PopLuaScreen(lua_State* s) -> int { PopScreen(); + luavgl_set_root(s, sCurrentScreen->root()); + lv_group_set_default(sCurrentScreen->group()); return 0; } -void Lua::exit() {} +void Lua::exit() { + lv_group_set_default(NULL); +} void Lua::react(const internal::IndexSelected& ev) { auto db = sServices->database().lock();