Migrate 'now playing' screen to lua

custom
jacqueline 1 year ago
parent 230721cd62
commit 7c6eb2997c
  1. 38
      lib/luavgl/src/lvgl.lua
  2. 268
      lib/luavgl/src/obj.c
  3. 87
      lib/luavgl/src/widgets/bar.c
  4. 12
      lib/luavgl/src/widgets/widgets.c
  5. 5
      lua/browser.lua
  6. BIN
      lua/img/next.png
  7. BIN
      lua/img/pause.png
  8. BIN
      lua/img/play.png
  9. BIN
      lua/img/prev.png
  10. 3
      lua/main_menu.lua
  11. 130
      lua/playing.lua
  12. 18
      lua/widgets.lua
  13. 1
      src/audio/CMakeLists.txt
  14. 26
      src/audio/audio_decoder.cpp
  15. 2
      src/audio/audio_fsm.cpp
  16. 41
      src/audio/audio_source.cpp
  17. 5
      src/audio/fatfs_audio_input.cpp
  18. 7
      src/audio/include/audio_decoder.hpp
  19. 13
      src/audio/include/audio_events.hpp
  20. 27
      src/audio/include/audio_source.hpp
  21. 4
      src/audio/include/fatfs_audio_input.hpp
  22. 3
      src/audio/include/track_queue.hpp
  23. 10
      src/audio/track_queue.cpp
  24. 17
      src/codecs/codec.cpp
  25. 2
      src/codecs/include/types.hpp
  26. 2
      src/database/include/track.hpp
  27. 20
      src/database/track.cpp
  28. 4
      src/lua/include/property.hpp
  29. 28
      src/lua/property.cpp
  30. 4
      src/lua/stubs/playback.lua
  31. 12
      src/ui/CMakeLists.txt
  32. 41
      src/ui/include/modal_add_to_queue.hpp
  33. 63
      src/ui/include/screen_onboarding.hpp
  34. 73
      src/ui/include/screen_playing.hpp
  35. 74
      src/ui/include/screen_track_browser.hpp
  36. 43
      src/ui/include/ui_fsm.hpp
  37. 182
      src/ui/modal_add_to_queue.cpp
  38. 4
      src/ui/screen_lua.cpp
  39. 146
      src/ui/screen_onboarding.cpp
  40. 338
      src/ui/screen_playing.cpp
  41. 431
      src/ui/screen_track_browser.cpp
  42. 185
      src/ui/ui_fsm.cpp
  43. 50
      src/ui/widget_top_bar.cpp

@ -335,6 +335,13 @@ end
function lvgl.Object(parent, property) function lvgl.Object(parent, property)
end end
--- Create Bar widget on parent
--- @param parent? Object | nil
--- @param property? BarProp
--- @return Bar
function lvgl.Bar(parent, property)
end
--- Create Button widget on parent --- Create Button widget on parent
--- @param parent? Object | nil --- @param parent? Object | nil
--- @param property? StyleProp --- @param property? StyleProp
@ -471,6 +478,13 @@ obj = {}
function obj:Object(property) function obj:Object(property)
end end
---
--- Create bar on object
--- @param property? BarStyle
--- @return Bar
function obj:Bar(property)
end
--- ---
--- Create button on object --- Create button on object
--- @param property? ButtonStyle --- @param property? ButtonStyle
@ -866,6 +880,18 @@ end
function calendar:Dropdown(p) function calendar:Dropdown(p)
end end
---
--- Bar widget
---@class Bar:Object
---
local bar = {}
--- set method for bar widget
--- @param p BarStyle
--- @return nil
function bar:set(p)
end
--- ---
--- Button widget --- Button widget
---@class Button:Object ---@class Button:Object
@ -1375,6 +1401,11 @@ end
--- @class LabelStyle :StyleProp --- @class LabelStyle :StyleProp
--- @field text string --- @field text string
--- Bar style
--- @class BarStyle :StyleProp
--- @field range BarRangePara
--- @field value integer
--- Button style --- Button style
--- @class ButtonStyle :StyleProp --- @class ButtonStyle :StyleProp
@ -1472,6 +1503,13 @@ end
--- @field align_content flexAlignOptions --- @field align_content flexAlignOptions
---
--- BarRange para
--- @class BarRangePara
--- @field min integer
--- @field max integer
---
--- ---
--- CalendarToday para --- CalendarToday para
--- @class CalendarDatePara --- @class CalendarDatePara

@ -9,8 +9,7 @@
static int luavgl_anim_create(lua_State *L); static int luavgl_anim_create(lua_State *L);
static int luavgl_obj_delete(lua_State *L); static int luavgl_obj_delete(lua_State *L);
static void _lv_obj_set_align(void *obj, lua_State *L) static void _lv_obj_set_align(void *obj, lua_State *L) {
{
if (lua_isinteger(L, -1)) { if (lua_isinteger(L, -1)) {
lv_obj_align(obj, lua_tointeger(L, -1), 0, 0); lv_obj_align(obj, lua_tointeger(L, -1), 0, 0);
return; return;
@ -42,13 +41,11 @@ static void _lv_obj_set_align(void *obj, lua_State *L)
* *
* Internally used. * Internally used.
*/ */
static inline void luavgl_setup_obj(lua_State *L, lv_obj_t *obj) static inline void luavgl_setup_obj(lua_State *L, lv_obj_t *obj) {
{
luavgl_iterate(L, -1, luavgl_obj_set_property_kv, obj); luavgl_iterate(L, -1, luavgl_obj_set_property_kv, obj);
} }
static void obj_delete_cb(lv_event_t *e) static void obj_delete_cb(lv_event_t *e) {
{
lua_State *L = e->user_data; lua_State *L = e->user_data;
lua_pushlightuserdata(L, e->current_target); lua_pushlightuserdata(L, e->current_target);
lua_rawget(L, LUA_REGISTRYINDEX); lua_rawget(L, LUA_REGISTRYINDEX);
@ -57,8 +54,16 @@ static void obj_delete_cb(lv_event_t *e)
} }
luavgl_obj_t *lobj = luavgl_to_lobj(L, -1); luavgl_obj_t *lobj = luavgl_to_lobj(L, -1);
if (lobj->lua_created) if (lobj->lua_created) {
// The underlying object is now gone, so don't keep a reference to it.
lobj->obj = NULL;
// Ensure there's no dangling reference in the registry either.
lua_pushlightuserdata(L, e->current_target);
lua_pushnil(L);
lua_rawset(L, LUA_REGISTRYINDEX);
goto pop_exit; goto pop_exit;
}
luavgl_obj_delete(L); luavgl_obj_delete(L);
return; return;
@ -73,8 +78,7 @@ pop_exit:
* one. result stack: table(from uservalue) * one. result stack: table(from uservalue)
* return uservalue type: LUA_TTABLE * return uservalue type: LUA_TTABLE
*/ */
LUALIB_API int luavgl_obj_getuserdatauv(lua_State *L, int idx) LUALIB_API int luavgl_obj_getuserdatauv(lua_State *L, int idx) {
{
int type = lua_getuservalue(L, idx); int type = lua_getuservalue(L, idx);
if (type == LUA_TTABLE) if (type == LUA_TTABLE)
return type; return type;
@ -93,13 +97,11 @@ LUALIB_API int luavgl_obj_getuserdatauv(lua_State *L, int idx)
return LUA_TTABLE; return LUA_TTABLE;
} }
static int luavgl_obj_create(lua_State *L) static int luavgl_obj_create(lua_State *L) {
{
return luavgl_obj_create_helper(L, lv_obj_create); return luavgl_obj_create_helper(L, lv_obj_create);
} }
static int luavgl_obj_delete(lua_State *L) static int luavgl_obj_delete(lua_State *L) {
{
luavgl_obj_t *lobj; luavgl_obj_t *lobj;
/** /**
@ -149,8 +151,7 @@ static int luavgl_obj_delete(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_clean(lua_State *L) static int luavgl_obj_clean(lua_State *L) {
{
luavgl_obj_t *lobj = luavgl_to_lobj(L, -1); luavgl_obj_t *lobj = luavgl_to_lobj(L, -1);
if (lobj == NULL || lobj->obj == NULL) if (lobj == NULL || lobj->obj == NULL)
return 0; return 0;
@ -171,8 +172,7 @@ static int luavgl_obj_clean(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_set(lua_State *L) static int luavgl_obj_set(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
if (!lua_istable(L, -1)) { if (!lua_istable(L, -1)) {
@ -187,8 +187,7 @@ static int luavgl_obj_set(lua_State *L)
/** /**
* obj:align_to({base=base, type=type, x_ofs=0, y_ofs=0}) * obj:align_to({base=base, type=type, x_ofs=0, y_ofs=0})
*/ */
static int luavgl_obj_align_to(lua_State *L) static int luavgl_obj_align_to(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
if (!lua_istable(L, 2)) { if (!lua_istable(L, 2)) {
@ -220,16 +219,14 @@ static int luavgl_obj_align_to(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_set_parent(lua_State *L) static int luavgl_obj_set_parent(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_t *parent = luavgl_to_obj(L, 2); lv_obj_t *parent = luavgl_to_obj(L, 2);
lv_obj_set_parent(obj, parent); lv_obj_set_parent(obj, parent);
return 0; return 0;
} }
static int luavgl_obj_get_screen(lua_State *L) static int luavgl_obj_get_screen(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_t *screen = lv_obj_get_screen(obj); lv_obj_t *screen = lv_obj_get_screen(obj);
@ -247,8 +244,7 @@ static int luavgl_obj_get_screen(lua_State *L)
return 1; return 1;
} }
static int luavgl_obj_get_parent(lua_State *L) static int luavgl_obj_get_parent(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_t *parent = lv_obj_get_parent(obj); lv_obj_t *parent = lv_obj_get_parent(obj);
@ -265,8 +261,7 @@ static int luavgl_obj_get_parent(lua_State *L)
return 1; return 1;
} }
static int luavgl_obj_set_get_parent(lua_State *L) static int luavgl_obj_set_get_parent(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
if (!lua_isnoneornil(L, 2)) { if (!lua_isnoneornil(L, 2)) {
lv_obj_t *parent = luavgl_to_obj(L, 2); lv_obj_t *parent = luavgl_to_obj(L, 2);
@ -276,8 +271,7 @@ static int luavgl_obj_set_get_parent(lua_State *L)
return luavgl_obj_get_parent(L); return luavgl_obj_get_parent(L);
} }
static int luavgl_obj_get_child(lua_State *L) static int luavgl_obj_get_child(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
int id = luavgl_tointeger(L, 2); int id = luavgl_tointeger(L, 2);
@ -297,15 +291,13 @@ static int luavgl_obj_get_child(lua_State *L)
return 1; return 1;
} }
static int luavgl_obj_get_child_cnt(lua_State *L) static int luavgl_obj_get_child_cnt(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lua_pushinteger(L, lv_obj_get_child_cnt(obj)); lua_pushinteger(L, lv_obj_get_child_cnt(obj));
return 1; return 1;
} }
static int luavgl_obj_get_state(lua_State *L) static int luavgl_obj_get_state(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_state_t state = lv_obj_get_state(obj); lv_state_t state = lv_obj_get_state(obj);
lua_pushinteger(L, state); lua_pushinteger(L, state);
@ -318,8 +310,7 @@ static int luavgl_obj_get_state(lua_State *L)
* obj:scroll_to({x=10, anim=true}) * obj:scroll_to({x=10, anim=true})
* obj:scroll_to({x=10, y=100, anim=false}) * obj:scroll_to({x=10, y=100, anim=false})
*/ */
static int luavgl_obj_scroll_to(lua_State *L) static int luavgl_obj_scroll_to(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
if (!lua_istable(L, -1)) { if (!lua_istable(L, -1)) {
@ -348,16 +339,14 @@ static int luavgl_obj_scroll_to(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_is_visible(lua_State *L) static int luavgl_obj_is_visible(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lua_pushboolean(L, lv_obj_is_visible(obj)); lua_pushboolean(L, lv_obj_is_visible(obj));
return 1; return 1;
} }
static int luavgl_obj_add_flag(lua_State *L) static int luavgl_obj_add_flag(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_flag_t flag = lua_tointeger(L, 2); lv_obj_flag_t flag = lua_tointeger(L, 2);
lv_obj_add_flag(obj, flag); lv_obj_add_flag(obj, flag);
@ -365,8 +354,7 @@ static int luavgl_obj_add_flag(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_clear_flag(lua_State *L) static int luavgl_obj_clear_flag(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_flag_t flag = lua_tointeger(L, 2); lv_obj_flag_t flag = lua_tointeger(L, 2);
lv_obj_clear_flag(obj, flag); lv_obj_clear_flag(obj, flag);
@ -374,16 +362,14 @@ static int luavgl_obj_clear_flag(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_add_state(lua_State *L) static int luavgl_obj_add_state(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_state_t state = lua_tointeger(L, 2); lv_state_t state = lua_tointeger(L, 2);
lv_obj_add_state(obj, state); lv_obj_add_state(obj, state);
return 0; return 0;
} }
static int luavgl_obj_clear_state(lua_State *L) static int luavgl_obj_clear_state(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_state_t state = lua_tointeger(L, 2); lv_state_t state = lua_tointeger(L, 2);
lv_obj_clear_state(obj, state); lv_obj_clear_state(obj, state);
@ -393,8 +379,7 @@ static int luavgl_obj_clear_state(lua_State *L)
/** /**
* obj:scroll_by(x, y, anim_en) * obj:scroll_by(x, y, anim_en)
*/ */
static int luavgl_obj_scroll_by(lua_State *L) static int luavgl_obj_scroll_by(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
int x = luavgl_tointeger(L, 2); int x = luavgl_tointeger(L, 2);
int y = luavgl_tointeger(L, 3); int y = luavgl_tointeger(L, 3);
@ -404,8 +389,7 @@ static int luavgl_obj_scroll_by(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_scroll_by_bounded(lua_State *L) static int luavgl_obj_scroll_by_bounded(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
int dx = luavgl_tointeger(L, 2); int dx = luavgl_tointeger(L, 2);
int dy = luavgl_tointeger(L, 3); int dy = luavgl_tointeger(L, 3);
@ -415,8 +399,7 @@ static int luavgl_obj_scroll_by_bounded(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_scroll_to_view(lua_State *L) static int luavgl_obj_scroll_to_view(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
int anim_en = luavgl_tointeger(L, 2); int anim_en = luavgl_tointeger(L, 2);
@ -424,8 +407,7 @@ static int luavgl_obj_scroll_to_view(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_scroll_to_view_recursive(lua_State *L) static int luavgl_obj_scroll_to_view_recursive(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
int anim_en = luavgl_tointeger(L, 2); int anim_en = luavgl_tointeger(L, 2);
@ -433,8 +415,7 @@ static int luavgl_obj_scroll_to_view_recursive(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_scroll_by_raw(lua_State *L) static int luavgl_obj_scroll_by_raw(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
int x = luavgl_tointeger(L, 2); int x = luavgl_tointeger(L, 2);
int y = luavgl_tointeger(L, 3); int y = luavgl_tointeger(L, 3);
@ -443,72 +424,62 @@ static int luavgl_obj_scroll_by_raw(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_is_scrolling(lua_State *L) static int luavgl_obj_is_scrolling(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lua_pushboolean(L, lv_obj_is_scrolling(obj)); lua_pushboolean(L, lv_obj_is_scrolling(obj));
return 1; return 1;
} }
static int luavgl_obj_scrollbar_invalidate(lua_State *L) static int luavgl_obj_scrollbar_invalidate(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_scrollbar_invalidate(obj); lv_obj_scrollbar_invalidate(obj);
return 0; return 0;
} }
static int luavgl_obj_readjust_scroll(lua_State *L) static int luavgl_obj_readjust_scroll(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
int anim_en = luavgl_tointeger(L, 2); int anim_en = luavgl_tointeger(L, 2);
lv_obj_readjust_scroll(obj, anim_en); lv_obj_readjust_scroll(obj, anim_en);
return 0; return 0;
} }
static int luavgl_obj_is_editable(lua_State *L) static int luavgl_obj_is_editable(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lua_pushboolean(L, lv_obj_is_editable(obj)); lua_pushboolean(L, lv_obj_is_editable(obj));
return 1; return 1;
} }
static int luavgl_obj_is_group_def(lua_State *L) static int luavgl_obj_is_group_def(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lua_pushboolean(L, lv_obj_is_group_def(obj)); lua_pushboolean(L, lv_obj_is_group_def(obj));
return 1; return 1;
} }
static int luavgl_obj_is_layout_positioned(lua_State *L) static int luavgl_obj_is_layout_positioned(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lua_pushboolean(L, lv_obj_is_layout_positioned(obj)); lua_pushboolean(L, lv_obj_is_layout_positioned(obj));
return 1; return 1;
} }
static int luavgl_obj_mark_layout_as_dirty(lua_State *L) static int luavgl_obj_mark_layout_as_dirty(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_mark_layout_as_dirty(obj); lv_obj_mark_layout_as_dirty(obj);
return 0; return 0;
} }
static int luavgl_obj_center(lua_State *L) static int luavgl_obj_center(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_center(obj); lv_obj_center(obj);
return 0; return 0;
} }
static int luavgl_obj_invalidate(lua_State *L) static int luavgl_obj_invalidate(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_invalidate(obj); lv_obj_invalidate(obj);
return 0; return 0;
} }
static int luavgl_obj_set_flex_flow(lua_State *L) static int luavgl_obj_set_flex_flow(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_flex_flow_t flow = luavgl_tointeger(L, 2); lv_flex_flow_t flow = luavgl_tointeger(L, 2);
@ -516,8 +487,7 @@ static int luavgl_obj_set_flex_flow(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_set_flex_align(lua_State *L) static int luavgl_obj_set_flex_align(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_flex_align_t m = luavgl_tointeger(L, 2); lv_flex_align_t m = luavgl_tointeger(L, 2);
lv_flex_align_t c = luavgl_tointeger(L, 3); lv_flex_align_t c = luavgl_tointeger(L, 3);
@ -527,8 +497,7 @@ static int luavgl_obj_set_flex_align(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_set_flex_grow(lua_State *L) static int luavgl_obj_set_flex_grow(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
uint8_t grow = luavgl_tointeger(L, 2); uint8_t grow = luavgl_tointeger(L, 2);
@ -536,8 +505,7 @@ static int luavgl_obj_set_flex_grow(lua_State *L)
return 0; return 0;
} }
static int luavgl_obj_indev_search(lua_State *L) static int luavgl_obj_indev_search(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_point_t point; lv_point_t point;
if (lua_istable(L, 2)) { if (lua_istable(L, 2)) {
@ -567,8 +535,7 @@ static int luavgl_obj_indev_search(lua_State *L)
return 1; return 1;
} }
static int luavgl_obj_get_coords(lua_State *L) static int luavgl_obj_get_coords(lua_State *L) {
{
lv_area_t area; lv_area_t area;
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_obj_get_coords(obj, &area); lv_obj_get_coords(obj, &area);
@ -592,8 +559,7 @@ static int luavgl_obj_get_coords(lua_State *L)
/** /**
* get object real position using lv_obj_get_x/x2/y/y2 * get object real position using lv_obj_get_x/x2/y/y2
*/ */
static int luavgl_obj_get_pos(lua_State *L) static int luavgl_obj_get_pos(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lua_newtable(L); lua_newtable(L);
@ -615,15 +581,13 @@ static int luavgl_obj_get_pos(lua_State *L)
/** /**
* Remove all animations associates to this object * Remove all animations associates to this object
*/ */
static int luavgl_obj_remove_anim_all(lua_State *L) static int luavgl_obj_remove_anim_all(lua_State *L) {
{
lv_obj_t *obj = luavgl_to_obj(L, 1); lv_obj_t *obj = luavgl_to_obj(L, 1);
lv_anim_del(obj, NULL); lv_anim_del(obj, NULL);
return 1; return 1;
} }
static int luavgl_obj_gc(lua_State *L) static int luavgl_obj_gc(lua_State *L) {
{
if (lua_type(L, 1) != LUA_TUSERDATA) { if (lua_type(L, 1) != LUA_TUSERDATA) {
/* If t = setmetatable({}, obj_meta_table), this will happen when t is /* If t = setmetatable({}, obj_meta_table), this will happen when t is
* gc;ed. Currently all metatables for classes based on obj, that has no own * gc;ed. Currently all metatables for classes based on obj, that has no own
@ -647,62 +611,61 @@ static int luavgl_obj_gc(lua_State *L)
} }
static const luaL_Reg luavgl_obj_methods[] = { static const luaL_Reg luavgl_obj_methods[] = {
{"set", luavgl_obj_set }, {"set", luavgl_obj_set},
{"set_style", luavgl_obj_set_style }, {"set_style", luavgl_obj_set_style},
{"align_to", luavgl_obj_align_to }, {"align_to", luavgl_obj_align_to},
{"delete", luavgl_obj_delete }, {"delete", luavgl_obj_delete},
{"clean", luavgl_obj_clean }, {"clean", luavgl_obj_clean},
/* misc. functions */ /* misc. functions */
{"parent", luavgl_obj_set_get_parent }, {"parent", luavgl_obj_set_get_parent},
{"set_parent", luavgl_obj_set_parent }, {"set_parent", luavgl_obj_set_parent},
{"get_parent", luavgl_obj_get_parent }, {"get_parent", luavgl_obj_get_parent},
{"get_child", luavgl_obj_get_child }, {"get_child", luavgl_obj_get_child},
{"get_child_cnt", luavgl_obj_get_child_cnt }, {"get_child_cnt", luavgl_obj_get_child_cnt},
{"get_screen", luavgl_obj_get_screen }, {"get_screen", luavgl_obj_get_screen},
{"get_state", luavgl_obj_get_state }, {"get_state", luavgl_obj_get_state},
{"scroll_to", luavgl_obj_scroll_to }, {"scroll_to", luavgl_obj_scroll_to},
{"is_scrolling", luavgl_obj_is_scrolling }, {"is_scrolling", luavgl_obj_is_scrolling},
{"is_visible", luavgl_obj_is_visible }, {"is_visible", luavgl_obj_is_visible},
{"add_flag", luavgl_obj_add_flag }, {"add_flag", luavgl_obj_add_flag},
{"clear_flag", luavgl_obj_clear_flag }, {"clear_flag", luavgl_obj_clear_flag},
{"add_state", luavgl_obj_add_state }, {"add_state", luavgl_obj_add_state},
{"clear_state", luavgl_obj_clear_state }, {"clear_state", luavgl_obj_clear_state},
{"add_style", luavgl_obj_add_style }, {"add_style", luavgl_obj_add_style},
{"remove_style", luavgl_obj_remove_style }, {"remove_style", luavgl_obj_remove_style},
{"remove_style_all", luavgl_obj_remove_style_all }, {"remove_style_all", luavgl_obj_remove_style_all},
{"scroll_by", luavgl_obj_scroll_by }, {"scroll_by", luavgl_obj_scroll_by},
{"scroll_by_bounded", luavgl_obj_scroll_by_bounded }, {"scroll_by_bounded", luavgl_obj_scroll_by_bounded},
{"scroll_to_view", luavgl_obj_scroll_to_view }, {"scroll_to_view", luavgl_obj_scroll_to_view},
{"scroll_to_view_recursive", luavgl_obj_scroll_to_view_recursive}, {"scroll_to_view_recursive", luavgl_obj_scroll_to_view_recursive},
{"scroll_by_raw", luavgl_obj_scroll_by_raw }, {"scroll_by_raw", luavgl_obj_scroll_by_raw},
{"scrollbar_invalidate", luavgl_obj_scrollbar_invalidate }, {"scrollbar_invalidate", luavgl_obj_scrollbar_invalidate},
{"readjust_scroll", luavgl_obj_readjust_scroll }, {"readjust_scroll", luavgl_obj_readjust_scroll},
{"is_editable", luavgl_obj_is_editable }, {"is_editable", luavgl_obj_is_editable},
{"is_group_def", luavgl_obj_is_group_def }, {"is_group_def", luavgl_obj_is_group_def},
{"is_layout_positioned", luavgl_obj_is_layout_positioned }, {"is_layout_positioned", luavgl_obj_is_layout_positioned},
{"mark_layout_as_dirty", luavgl_obj_mark_layout_as_dirty }, {"mark_layout_as_dirty", luavgl_obj_mark_layout_as_dirty},
{"center", luavgl_obj_center }, {"center", luavgl_obj_center},
{"invalidate", luavgl_obj_invalidate }, {"invalidate", luavgl_obj_invalidate},
{"set_flex_flow", luavgl_obj_set_flex_flow }, {"set_flex_flow", luavgl_obj_set_flex_flow},
{"set_flex_align", luavgl_obj_set_flex_align }, {"set_flex_align", luavgl_obj_set_flex_align},
{"set_flex_grow", luavgl_obj_set_flex_grow }, {"set_flex_grow", luavgl_obj_set_flex_grow},
{"indev_search", luavgl_obj_indev_search }, {"indev_search", luavgl_obj_indev_search},
{"get_coords", luavgl_obj_get_coords }, {"get_coords", luavgl_obj_get_coords},
{"get_pos", luavgl_obj_get_pos }, {"get_pos", luavgl_obj_get_pos},
{"onevent", luavgl_obj_on_event }, {"onevent", luavgl_obj_on_event},
{"onPressed", luavgl_obj_on_pressed }, {"onPressed", luavgl_obj_on_pressed},
{"onClicked", luavgl_obj_on_clicked }, {"onClicked", luavgl_obj_on_clicked},
{"onShortClicked", luavgl_obj_on_short_clicked }, {"onShortClicked", luavgl_obj_on_short_clicked},
{"anim", luavgl_anim_create }, {"anim", luavgl_anim_create},
{"Anim", luavgl_anim_create }, {"Anim", luavgl_anim_create},
{"remove_all_anim", luavgl_obj_remove_anim_all }, /* remove all */ {"remove_all_anim", luavgl_obj_remove_anim_all}, /* remove all */
{NULL, NULL }, {NULL, NULL},
}; };
static void luavgl_obj_init(lua_State *L) static void luavgl_obj_init(lua_State *L) {
{
/* base lv_obj */ /* base lv_obj */
luavgl_obj_newmetatable(L, &lv_obj_class, "lv_obj", luavgl_obj_methods); luavgl_obj_newmetatable(L, &lv_obj_class, "lv_obj", luavgl_obj_methods);
lua_pushcfunction(L, luavgl_obj_gc); lua_pushcfunction(L, luavgl_obj_gc);
@ -728,16 +691,16 @@ static void luavgl_obj_init(lua_State *L)
} }
static const luavgl_value_setter_t obj_property_table[] = { static const luavgl_value_setter_t obj_property_table[] = {
{"x", 0, {.setter = (setter_int_t)lv_obj_set_x} }, {"x", 0, {.setter = (setter_int_t)lv_obj_set_x}},
{"y", 0, {.setter = (setter_int_t)lv_obj_set_y} }, {"y", 0, {.setter = (setter_int_t)lv_obj_set_y}},
{"w", 0, {.setter = (setter_int_t)lv_obj_set_width} }, {"w", 0, {.setter = (setter_int_t)lv_obj_set_width}},
{"h", 0, {.setter = (setter_int_t)lv_obj_set_height} }, {"h", 0, {.setter = (setter_int_t)lv_obj_set_height}},
{"align", SETTER_TYPE_STACK, {.setter_stack = _lv_obj_set_align} }, {"align", SETTER_TYPE_STACK, {.setter_stack = _lv_obj_set_align}},
{"scrollbar_mode", 0, {.setter = (setter_int_t)lv_obj_set_scrollbar_mode}}, {"scrollbar_mode", 0, {.setter = (setter_int_t)lv_obj_set_scrollbar_mode}},
{"scroll_dir", 0, {.setter = (setter_int_t)lv_obj_set_scroll_dir} }, {"scroll_dir", 0, {.setter = (setter_int_t)lv_obj_set_scroll_dir}},
{"scroll_snap_x", 0, {.setter = (setter_int_t)lv_obj_set_scroll_snap_x} }, {"scroll_snap_x", 0, {.setter = (setter_int_t)lv_obj_set_scroll_snap_x}},
{"scroll_snap_y", 0, {.setter = (setter_int_t)lv_obj_set_scroll_snap_y} }, {"scroll_snap_y", 0, {.setter = (setter_int_t)lv_obj_set_scroll_snap_y}},
}; };
/** /**
@ -751,8 +714,7 @@ static const luavgl_value_setter_t obj_property_table[] = {
* stack[-2]: key(property name) * stack[-2]: key(property name)
* stack[-1]: value(could be any lua data) * stack[-1]: value(could be any lua data)
*/ */
LUALIB_API int luavgl_obj_set_property_kv(lua_State *L, void *data) LUALIB_API int luavgl_obj_set_property_kv(lua_State *L, void *data) {
{
lv_obj_t *obj = data; lv_obj_t *obj = data;
int ret = luavgl_set_property(L, obj, obj_property_table); int ret = luavgl_set_property(L, obj, obj_property_table);
@ -764,8 +726,7 @@ LUALIB_API int luavgl_obj_set_property_kv(lua_State *L, void *data)
} }
LUALIB_API int luavgl_obj_create_helper(lua_State *L, LUALIB_API int luavgl_obj_create_helper(lua_State *L,
lv_obj_t *(*create)(lv_obj_t *parent)) lv_obj_t *(*create)(lv_obj_t *parent)) {
{
luavgl_ctx_t *ctx = luavgl_context(L); luavgl_ctx_t *ctx = luavgl_context(L);
lv_obj_t *parent; lv_obj_t *parent;
@ -811,8 +772,7 @@ LUALIB_API int luavgl_obj_create_helper(lua_State *L,
* If no metatable not found for this obj class, then lv_obj_class metatable is * If no metatable not found for this obj class, then lv_obj_class metatable is
* used * used
*/ */
LUALIB_API luavgl_obj_t *luavgl_add_lobj(lua_State *L, lv_obj_t *obj) LUALIB_API luavgl_obj_t *luavgl_add_lobj(lua_State *L, lv_obj_t *obj) {
{
luavgl_obj_t *lobj; luavgl_obj_t *lobj;
/* In rare case, obj may be deleted but not gc'ed in lua, and lvgl quickly /* In rare case, obj may be deleted but not gc'ed in lua, and lvgl quickly

@ -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);
}

@ -1,6 +1,10 @@
#include "luavgl.h" #include "luavgl.h"
#include "private.h" #include "private.h"
#if LV_USE_BAR
#include "bar.c"
#endif
#if LV_USE_BTN #if LV_USE_BTN
#include "btn.c" #include "btn.c"
#endif #endif
@ -50,6 +54,10 @@ static int luavgl_obj_create(lua_State *L);
static const luaL_Reg widget_create_methods[] = { static const luaL_Reg widget_create_methods[] = {
{"Object", luavgl_obj_create }, {"Object", luavgl_obj_create },
#if LV_USE_BAR
{"Bar", luavgl_bar_create},
#endif
#if LV_USE_BTN #if LV_USE_BTN
{"Button", luavgl_btn_create}, {"Button", luavgl_btn_create},
#endif #endif
@ -142,4 +150,8 @@ static void luavgl_widgets_init(lua_State *L)
luavgl_btn_init(L); luavgl_btn_init(L);
#endif #endif
#if LV_USE_BAR
luavgl_bar_init(L);
#endif
} }

@ -5,6 +5,7 @@ local database = require("database")
local backstack = require("backstack") local backstack = require("backstack")
local font = require("font") local font = require("font")
local queue = require("queue") local queue = require("queue")
local playing = require("playing")
local browser = {} local browser = {}
@ -73,6 +74,7 @@ function browser.create(opts)
play:onClicked(function() play:onClicked(function()
queue.clear() queue.clear()
queue.add(original_iterator) queue.add(original_iterator)
backstack.push(playing)
end end
) )
end end
@ -107,8 +109,7 @@ function browser.create(opts)
else else
queue.clear() queue.clear()
queue.add(contents) queue.add(contents)
legacy_ui.open_now_playing() backstack.push(playing)
-- backstack.push(playing)
end end
end) end)
btn:onevent(lvgl.EVENT.FOCUSED, function() btn:onevent(lvgl.EVENT.FOCUSED, function()

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

@ -4,6 +4,7 @@ local legacy_ui = require("legacy_ui")
local database = require("database") local database = require("database")
local backstack = require("backstack") local backstack = require("backstack")
local browser = require("browser") local browser = require("browser")
local playing = require("playing")
return function() return function()
local menu = {} local menu = {}
@ -29,7 +30,7 @@ return function()
}) })
menu.list:add_btn(nil, "Now Playing"):onClicked(function() menu.list:add_btn(nil, "Now Playing"):onClicked(function()
legacy_ui.open_now_playing(); backstack.push(playing)
end) end)
local indexes = database.indexes() local indexes = database.indexes()

@ -2,6 +2,8 @@ local lvgl = require("lvgl")
local widgets = require("widgets") local widgets = require("widgets")
local backstack = require("backstack") local backstack = require("backstack")
local font = require("font") local font = require("font")
local playback = require("playback")
local queue = require("queue")
return function(opts) return function(opts)
local screen = {} local screen = {}
@ -23,72 +25,142 @@ return function(opts)
transparent_bg = true, transparent_bg = true,
}) })
local track_info = screen.root:Object { local info = screen.root:Object {
flex = { flex = {
flex_direction = "column", flex_direction = "column",
flex_wrap = "wrap",
justify_content = "center", justify_content = "center",
align_items = "center", align_items = "center",
align_content = "center", align_content = "center",
}, },
w = lvgl.SIZE_CONTENT, w = lvgl.HOR_RES(),
h = lvgl.SIZE_CONTENT,
flex_grow = 1, flex_grow = 1,
} }
local artist = track_info:Label { local artist = info:Label {
text = "Cool Artist", w = lvgl.SIZE_CONTENT,
text_font = font.fusion_10, h = lvgl.SIZE_CONTENT,
} text = "",
local artist = track_info:Label {
text = "Good Album",
text_font = font.fusion_10, text_font = font.fusion_10,
} }
local title = track_info:Label { local title = info:Label {
text = "A really good song", w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT,
text = "",
} }
local scrubber = screen.root:Object {} local playlist = screen.root:Object {
local times = screen.root:Object {
flex = { flex = {
flex_direction = "row", flex_direction = "row",
justify_content = "center", justify_content = "center",
align_items = "space-between", align_items = "center",
align_content = "center", align_content = "center",
}, },
w = lvgl.PCT(100), w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT, h = lvgl.SIZE_CONTENT,
} }
local cur_time = track_info:Label {
text = "1:09", local playlist_pos = playlist:Label {
text = "",
text_font = font.fusion_10,
} }
local end_time = track_info:Label { playlist:Label {
text = "4:20", text = "/",
text_font = font.fusion_10,
}
local playlist_total = playlist:Label {
text = "",
text_font = font.fusion_10,
} }
local scrubber = screen.root:Bar {
w = lvgl.PCT(100),
h = 5,
range = { min = 0, max = 100 },
value = 0,
}
local controls = screen.root:Object { local controls = screen.root:Object {
flex = { flex = {
flex_direction = "row", flex_direction = "row",
justify_content = "center", justify_content = "center",
align_items = "space-evenly", align_items = "center",
align_content = "center", align_content = "center",
}, },
w = lvgl.PCT(100), w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT, h = lvgl.SIZE_CONTENT,
pad_column = 8,
pad_all = 2,
}
local cur_time = controls:Label {
w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT,
text = "",
text_font = font.fusion_10,
}
controls:Object({ flex_grow = 1, h = 1 }) -- spacer
controls:Image {
src = "//lua/img/prev.png",
}
local play_pause_img = controls:Image {
src = "//lua/img/pause.png",
}
controls:Image {
src = "//lua/img/next.png",
}
controls:Object({ flex_grow = 1, h = 1 }) -- spacer
local end_time = controls:Label {
w = lvgl.SIZE_CONTENT,
h = lvgl.SIZE_CONTENT,
align = lvgl.ALIGN.RIGHT_MID,
text = "",
text_font = font.fusion_10,
} }
controls:Label {
text = ">", local format_time = function(time)
return string.format("%d:%02d", time // 60, time % 60)
end
screen.bindings = {
playback.playing:bind(function(playing)
if playing then
play_pause_img:set_src("//lua/img/pause.png")
else
play_pause_img:set_src("//lua/img/play.png")
end
end),
playback.position:bind(function(pos)
if not pos then return end
cur_time:set {
text = format_time(pos)
} }
controls:Label { scrubber:set { value = pos }
text = ">", end),
playback.track:bind(function(track)
if not track then return end
end_time:set {
text = format_time(track.duration)
} }
controls:Label { title:set { text = track.title }
text = ">", artist:set { text = track.artist }
scrubber:set {
range = { min = 0, max = track.duration }
} }
controls:Label { end),
text = ">", queue.position:bind(function(pos)
if not pos then return end
playlist_pos:set { text = tostring(pos) }
end),
queue.size:bind(function(num)
if not num then return end
playlist_total:set { text = tostring(num) }
end),
} }
return screen return screen

@ -1,7 +1,6 @@
local lvgl = require("lvgl") local lvgl = require("lvgl")
local power = require("power") local power = require("power")
local bluetooth = require("bluetooth") local bluetooth = require("bluetooth")
local playback = require("playback")
local font = require("font") local font = require("font")
local widgets = {} local widgets = {}
@ -53,7 +52,6 @@ function widgets.StatusBar(parent, opts)
status_bar.title:set { text = opts.title } status_bar.title:set { text = opts.title }
end end
status_bar.playing = status_bar.root:Image {}
status_bar.bluetooth = status_bar.root:Image {} status_bar.bluetooth = status_bar.root:Image {}
status_bar.battery = status_bar.root:Image {} status_bar.battery = status_bar.root:Image {}
status_bar.chg = status_bar.battery:Image { status_bar.chg = status_bar.battery:Image {
@ -64,7 +62,7 @@ function widgets.StatusBar(parent, opts)
local is_charging = nil local is_charging = nil
local percent = nil local percent = nil
function update_battery_icon() local function update_battery_icon()
if is_charging == nil or percent == nil then return end if is_charging == nil or percent == nil then return end
local src local src
if percent >= 95 then if percent >= 95 then
@ -101,20 +99,6 @@ function widgets.StatusBar(parent, opts)
is_charging = p is_charging = p
update_battery_icon() update_battery_icon()
end), end),
playback.playing:bind(function(playing)
if playing then
status_bar.playing:set_src("//lua/assets/play.png")
else
status_bar.playing:set_src("//lua/assets/pause.png")
end
end),
playback.track:bind(function(track)
if track then
status_bar.playing:clear_flag(lvgl.FLAG.HIDDEN)
else
status_bar.playing:add_flag(lvgl.FLAG.HIDDEN)
end
end),
bluetooth.enabled:bind(function(en) bluetooth.enabled:bind(function(en)
if en then if en then
status_bar.bluetooth:clear_flag(lvgl.FLAG.HIDDEN) status_bar.bluetooth:clear_flag(lvgl.FLAG.HIDDEN)

@ -6,6 +6,7 @@ idf_component_register(
SRCS "audio_decoder.cpp" "fatfs_audio_input.cpp" "i2s_audio_output.cpp" SRCS "audio_decoder.cpp" "fatfs_audio_input.cpp" "i2s_audio_output.cpp"
"track_queue.cpp" "audio_fsm.cpp" "audio_converter.cpp" "resample.cpp" "track_queue.cpp" "audio_fsm.cpp" "audio_converter.cpp" "resample.cpp"
"fatfs_source.cpp" "bt_audio_output.cpp" "readahead_source.cpp" "fatfs_source.cpp" "bt_audio_output.cpp" "readahead_source.cpp"
"audio_source.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm"
"database" "system_fsm" "playlist" "speexdsp") "database" "system_fsm" "playlist" "speexdsp")

@ -50,12 +50,16 @@ namespace audio {
static constexpr std::size_t kCodecBufferLength = static constexpr std::size_t kCodecBufferLength =
drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2; drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2;
Timer::Timer(const codecs::ICodec::OutputFormat& format) Timer::Timer(std::shared_ptr<Track> t,
: current_seconds_(0), const codecs::ICodec::OutputFormat& format)
: track_(t),
current_seconds_(0),
current_sample_in_second_(0), current_sample_in_second_(0),
samples_per_second_(format.sample_rate_hz * format.num_channels), samples_per_second_(format.sample_rate_hz * format.num_channels),
total_duration_seconds_(format.total_samples.value_or(0) / total_duration_seconds_(format.total_samples.value_or(0) /
format.num_channels / format.sample_rate_hz) {} format.num_channels / format.sample_rate_hz) {
track_->duration = total_duration_seconds_;
}
auto Timer::AddSamples(std::size_t samples) -> void { auto Timer::AddSamples(std::size_t samples) -> void {
bool incremented = false; bool incremented = false;
@ -69,10 +73,10 @@ auto Timer::AddSamples(std::size_t samples) -> void {
if (incremented) { if (incremented) {
if (total_duration_seconds_ < current_seconds_) { if (total_duration_seconds_ < current_seconds_) {
total_duration_seconds_ = current_seconds_; total_duration_seconds_ = current_seconds_;
track_->duration = total_duration_seconds_;
} }
PlaybackUpdate ev{.seconds_elapsed = current_seconds_, PlaybackUpdate ev{.seconds_elapsed = current_seconds_, .track = track_};
.seconds_total = total_duration_seconds_};
events::Audio().Dispatch(ev); events::Audio().Dispatch(ev);
events::Ui().Dispatch(ev); events::Ui().Dispatch(ev);
} }
@ -102,7 +106,7 @@ Decoder::Decoder(std::shared_ptr<IAudioSource> source,
void Decoder::Main() { void Decoder::Main() {
for (;;) { for (;;) {
if (source_->HasNewStream() || !stream_) { if (source_->HasNewStream() || !stream_) {
std::shared_ptr<codecs::IStream> new_stream = source_->NextStream(); std::shared_ptr<TaggedStream> new_stream = source_->NextStream();
ESP_LOGI(kTag, "decoder has new stream"); ESP_LOGI(kTag, "decoder has new stream");
if (new_stream && BeginDecoding(new_stream)) { if (new_stream && BeginDecoding(new_stream)) {
stream_ = new_stream; stream_ = new_stream;
@ -118,7 +122,7 @@ void Decoder::Main() {
} }
} }
auto Decoder::BeginDecoding(std::shared_ptr<codecs::IStream> stream) -> bool { auto Decoder::BeginDecoding(std::shared_ptr<TaggedStream> stream) -> bool {
// Ensure any previous codec is freed before creating a new one. // Ensure any previous codec is freed before creating a new one.
codec_.reset(); codec_.reset();
codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr)); codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr));
@ -136,7 +140,13 @@ auto Decoder::BeginDecoding(std::shared_ptr<codecs::IStream> stream) -> bool {
stream->SetPreambleFinished(); stream->SetPreambleFinished();
if (open_res->total_samples) { if (open_res->total_samples) {
timer_.reset(new Timer(open_res.value())); timer_.reset(new Timer(std::shared_ptr<Track>{new Track{
.tags = stream->tags(),
.db_info = {},
.bitrate_kbps = 0,
.encoding = stream->type(),
}},
open_res.value()));
} else { } else {
timer_.reset(); timer_.reset();
} }

@ -211,7 +211,7 @@ void Playback::react(const TogglePlayPause& ev) {
void Playback::react(const PlaybackUpdate& ev) { void Playback::react(const PlaybackUpdate& ev) {
ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed,
ev.seconds_total); ev.track->duration);
} }
void Playback::react(const internal::InputFileOpened& ev) {} void Playback::react(const internal::InputFileOpened& ev) {}

@ -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

@ -85,7 +85,7 @@ auto FatfsAudioInput::HasNewStream() -> bool {
return has_new_stream_; return has_new_stream_;
} }
auto FatfsAudioInput::NextStream() -> std::shared_ptr<codecs::IStream> { auto FatfsAudioInput::NextStream() -> std::shared_ptr<TaggedStream> {
while (true) { while (true) {
has_new_stream_.wait(false); has_new_stream_.wait(false);
@ -147,8 +147,7 @@ auto FatfsAudioInput::OpenFile(const std::pmr::string& path) -> bool {
auto source = auto source =
std::make_unique<FatfsSource>(stream_type.value(), std::move(file)); std::make_unique<FatfsSource>(stream_type.value(), std::move(file));
// new_stream_.reset(new ReadaheadSource(bg_worker_, std::move(source))); new_stream_.reset(new TaggedStream(tags, std::move(source)));
new_stream_ = std::move(source);
return true; return true;
} }

@ -10,6 +10,7 @@
#include <memory> #include <memory>
#include "audio_converter.hpp" #include "audio_converter.hpp"
#include "audio_events.hpp"
#include "audio_sink.hpp" #include "audio_sink.hpp"
#include "audio_source.hpp" #include "audio_source.hpp"
#include "codec.hpp" #include "codec.hpp"
@ -23,11 +24,13 @@ namespace audio {
*/ */
class Timer { class Timer {
public: public:
Timer(const codecs::ICodec::OutputFormat& format); Timer(std::shared_ptr<Track>, const codecs::ICodec::OutputFormat& format);
auto AddSamples(std::size_t) -> void; auto AddSamples(std::size_t) -> void;
private: private:
std::shared_ptr<Track> track_;
uint32_t current_seconds_; uint32_t current_seconds_;
uint32_t current_sample_in_second_; uint32_t current_sample_in_second_;
uint32_t samples_per_second_; uint32_t samples_per_second_;
@ -54,7 +57,7 @@ class Decoder {
Decoder(std::shared_ptr<IAudioSource> source, Decoder(std::shared_ptr<IAudioSource> source,
std::shared_ptr<SampleConverter> converter); std::shared_ptr<SampleConverter> converter);
auto BeginDecoding(std::shared_ptr<codecs::IStream>) -> bool; auto BeginDecoding(std::shared_ptr<TaggedStream>) -> bool;
auto ContinueDecoding() -> bool; auto ContinueDecoding() -> bool;
std::shared_ptr<IAudioSource> source_; std::shared_ptr<IAudioSource> source_;

@ -8,20 +8,31 @@
#include <stdint.h> #include <stdint.h>
#include <cstdint> #include <cstdint>
#include <memory>
#include <string> #include <string>
#include "tinyfsm.hpp" #include "tinyfsm.hpp"
#include "track.hpp" #include "track.hpp"
#include "track_queue.hpp" #include "track_queue.hpp"
#include "types.hpp"
namespace audio { namespace audio {
struct Track {
std::shared_ptr<database::TrackTags> tags;
std::shared_ptr<database::TrackData> db_info;
uint32_t duration;
uint32_t bitrate_kbps;
codecs::StreamType encoding;
};
struct PlaybackStarted : tinyfsm::Event {}; struct PlaybackStarted : tinyfsm::Event {};
struct PlaybackUpdate : tinyfsm::Event { struct PlaybackUpdate : tinyfsm::Event {
uint32_t seconds_elapsed; uint32_t seconds_elapsed;
uint32_t seconds_total; std::shared_ptr<Track> track;
}; };
struct PlaybackFinished : tinyfsm::Event {}; struct PlaybackFinished : tinyfsm::Event {};

@ -6,16 +6,41 @@
#pragma once #pragma once
#include <memory>
#include "codec.hpp" #include "codec.hpp"
#include "track.hpp"
#include "types.hpp"
namespace audio { namespace audio {
class TaggedStream : public codecs::IStream {
public:
TaggedStream(std::shared_ptr<database::TrackTags>,
std::unique_ptr<codecs::IStream> wrapped);
auto tags() -> std::shared_ptr<database::TrackTags>;
auto Read(cpp::span<std::byte> dest) -> ssize_t override;
auto CanSeek() -> bool override;
auto SeekTo(int64_t destination, SeekFrom from) -> void override;
auto CurrentPosition() -> int64_t override;
auto SetPreambleFinished() -> void override;
private:
std::shared_ptr<database::TrackTags> tags_;
std::unique_ptr<codecs::IStream> wrapped_;
};
class IAudioSource { class IAudioSource {
public: public:
virtual ~IAudioSource() {} virtual ~IAudioSource() {}
virtual auto HasNewStream() -> bool = 0; virtual auto HasNewStream() -> bool = 0;
virtual auto NextStream() -> std::shared_ptr<codecs::IStream> = 0; virtual auto NextStream() -> std::shared_ptr<TaggedStream> = 0;
}; };
} // namespace audio } // namespace audio

@ -43,7 +43,7 @@ class FatfsAudioInput : public IAudioSource {
auto SetPath() -> void; auto SetPath() -> void;
auto HasNewStream() -> bool override; auto HasNewStream() -> bool override;
auto NextStream() -> std::shared_ptr<codecs::IStream> override; auto NextStream() -> std::shared_ptr<TaggedStream> override;
FatfsAudioInput(const FatfsAudioInput&) = delete; FatfsAudioInput(const FatfsAudioInput&) = delete;
FatfsAudioInput& operator=(const FatfsAudioInput&) = delete; FatfsAudioInput& operator=(const FatfsAudioInput&) = delete;
@ -58,7 +58,7 @@ class FatfsAudioInput : public IAudioSource {
tasks::Worker& bg_worker_; tasks::Worker& bg_worker_;
std::mutex new_stream_mutex_; std::mutex new_stream_mutex_;
std::shared_ptr<codecs::IStream> new_stream_; std::shared_ptr<TaggedStream> new_stream_;
std::atomic<bool> has_new_stream_; std::atomic<bool> has_new_stream_;

@ -72,6 +72,9 @@ class TrackQueue {
*/ */
auto Clear() -> void; auto Clear() -> void;
auto Position() -> size_t;
auto Size() -> size_t;
TrackQueue(const TrackQueue&) = delete; TrackQueue(const TrackQueue&) = delete;
TrackQueue& operator=(const TrackQueue&) = delete; TrackQueue& operator=(const TrackQueue&) = delete;

@ -8,10 +8,12 @@
#include <algorithm> #include <algorithm>
#include <mutex> #include <mutex>
#include <optional>
#include <variant> #include <variant>
#include "audio_events.hpp" #include "audio_events.hpp"
#include "audio_fsm.hpp" #include "audio_fsm.hpp"
#include "database.hpp"
#include "event_queue.hpp" #include "event_queue.hpp"
#include "source.hpp" #include "source.hpp"
#include "track.hpp" #include "track.hpp"
@ -217,4 +219,12 @@ auto TrackQueue::Clear() -> void {
events::Ui().Dispatch(ev); events::Ui().Dispatch(ev);
} }
auto TrackQueue::Position() -> size_t {
return played_.size() + (enqueued_.empty() ? 0 : 1);
}
auto TrackQueue::Size() -> size_t {
return played_.size() + enqueued_.size();
}
} // namespace audio } // namespace audio

@ -17,6 +17,23 @@
namespace codecs { namespace codecs {
auto StreamTypeToString(StreamType t) -> std::string {
switch (t) {
case StreamType::kMp3:
return "Mp3";
case StreamType::kPcm:
return "Wav";
case StreamType::kVorbis:
return "Vorbis";
case StreamType::kFlac:
return "Flac";
case StreamType::kOpus:
return "Opus";
default:
return "";
}
}
auto CreateCodecForType(StreamType type) -> std::optional<ICodec*> { auto CreateCodecForType(StreamType type) -> std::optional<ICodec*> {
switch (type) { switch (type) {
case StreamType::kMp3: case StreamType::kMp3:

@ -18,4 +18,6 @@ enum class StreamType {
kOpus, kOpus,
}; };
auto StreamTypeToString(StreamType t) -> std::string;
} // namespace codecs } // namespace codecs

@ -56,6 +56,8 @@ enum class Tag {
kDuration = 5, kDuration = 5,
}; };
auto TagToString(Tag t) -> std::string;
/* /*
* Owning container for tag-related track metadata that was extracted from a * Owning container for tag-related track metadata that was extracted from a
* file. * file.

@ -13,6 +13,25 @@
namespace database { namespace database {
auto TagToString(Tag t) -> std::string {
switch (t) {
case Tag::kTitle:
return "title";
case Tag::kArtist:
return "artist";
case Tag::kAlbum:
return "album";
case Tag::kAlbumTrack:
return "album_track";
case Tag::kGenre:
return "genre";
case Tag::kDuration:
return "duration";
default:
return "";
}
}
auto TrackTags::set(const Tag& key, const std::pmr::string& val) -> void { auto TrackTags::set(const Tag& key, const std::pmr::string& val) -> void {
tags_[key] = val; tags_[key] = val;
} }
@ -64,5 +83,4 @@ auto Track::TitleOrFilename() const -> std::pmr::string {
} }
return data().filepath.substr(start); return data().filepath.substr(start);
} }
} // namespace database } // namespace database

@ -9,13 +9,15 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include "audio_events.hpp"
#include "lua.hpp" #include "lua.hpp"
#include "lvgl.h" #include "lvgl.h"
#include "service_locator.hpp" #include "service_locator.hpp"
namespace lua { namespace lua {
using LuaValue = std::variant<std::monostate, int, float, bool, std::string>; using LuaValue =
std::variant<std::monostate, int, float, bool, std::string, audio::Track>;
using LuaFunction = std::function<int(lua_State*)>; using LuaFunction = std::function<int(lua_State*)>;
class Property { class Property {

@ -9,10 +9,13 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include "lua.h"
#include "lua.hpp" #include "lua.hpp"
#include "lua_thread.hpp" #include "lua_thread.hpp"
#include "lvgl.h" #include "lvgl.h"
#include "service_locator.hpp" #include "service_locator.hpp"
#include "track.hpp"
#include "types.hpp"
namespace lua { namespace lua {
@ -171,6 +174,31 @@ auto Property::PushValue(lua_State& s) -> int {
lua_pushboolean(&s, arg); lua_pushboolean(&s, arg);
} else if constexpr (std::is_same_v<T, std::string>) { } else if constexpr (std::is_same_v<T, std::string>) {
lua_pushstring(&s, arg.c_str()); lua_pushstring(&s, arg.c_str());
} else if constexpr (std::is_same_v<T, audio::Track>) {
lua_newtable(&s);
int table = lua_gettop(&s);
for (const auto& [key, val] : arg.tags->tags()) {
lua_pushstring(&s, database::TagToString(key).c_str());
lua_pushstring(&s, val.c_str());
lua_settable(&s, table);
}
if (arg.db_info) {
lua_pushliteral(&s, "id");
lua_pushinteger(&s, arg.db_info->id);
lua_settable(&s, table);
}
lua_pushliteral(&s, "duration");
lua_pushinteger(&s, arg.duration);
lua_settable(&s, table);
lua_pushliteral(&s, "bitrate_kbps");
lua_pushinteger(&s, arg.bitrate_kbps);
lua_settable(&s, table);
lua_pushliteral(&s, "encoding");
lua_pushstring(&s, codecs::StreamTypeToString(arg.encoding).c_str());
lua_settable(&s, table);
} else { } else {
static_assert(always_false_v<T>, "PushValue missing type"); static_assert(always_false_v<T>, "PushValue missing type");
} }

@ -8,4 +8,8 @@ local playback = {}
-- @treturn types.Property a boolean property -- @treturn types.Property a boolean property
function playback.playing() end function playback.playing() end
function playback:track() end
function playback:position() end
return playback return playback

@ -3,15 +3,11 @@
# SPDX-License-Identifier: GPL-3.0-only # SPDX-License-Identifier: GPL-3.0-only
idf_component_register( idf_component_register(
SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "encoder_input.cpp"
"encoder_input.cpp" "screen_track_browser.cpp" "screen_playing.cpp" "themes.cpp" "widget_top_bar.cpp" "screen.cpp" "modal_progress.cpp"
"themes.cpp" "widget_top_bar.cpp" "screen.cpp" "screen_onboarding.cpp" "modal.cpp" "modal_confirm.cpp" "screen_settings.cpp" "event_binding.cpp"
"modal_progress.cpp" "modal.cpp" "modal_confirm.cpp" "screen_settings.cpp" "screen_lua.cpp"
"event_binding.cpp" "modal_add_to_queue.cpp" "screen_lua.cpp"
"splash.c" "font_fusion_12.c" "font_fusion_10.c" "splash.c" "font_fusion_12.c" "font_fusion_10.c"
"icons/battery_empty.c" "icons/battery_full.c" "icons/battery_20.c"
"icons/battery_40.c" "icons/battery_60.c" "icons/battery_80.c" "icons/play.c"
"icons/pause.c" "icons/bluetooth.c"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "bindey" "lua" "luavgl") REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "bindey" "lua" "luavgl")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -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

@ -6,8 +6,7 @@
#pragma once #pragma once
#include <stdint.h> #include <cstdint>
#include <sys/_stdint.h>
#include <memory> #include <memory>
#include <stack> #include <stack>
@ -23,7 +22,6 @@
#include "nvs.hpp" #include "nvs.hpp"
#include "property.hpp" #include "property.hpp"
#include "relative_wheel.hpp" #include "relative_wheel.hpp"
#include "screen_playing.hpp"
#include "screen_settings.hpp" #include "screen_settings.hpp"
#include "service_locator.hpp" #include "service_locator.hpp"
#include "tinyfsm.hpp" #include "tinyfsm.hpp"
@ -60,16 +58,14 @@ class UiState : public tinyfsm::Fsm<UiState> {
virtual void react(const system_fsm::BatteryStateChanged&); virtual void react(const system_fsm::BatteryStateChanged&);
virtual void react(const audio::PlaybackStarted&); virtual void react(const audio::PlaybackStarted&);
virtual void react(const audio::PlaybackFinished&); virtual void react(const audio::PlaybackFinished&);
void react(const audio::PlaybackUpdate&); virtual void react(const audio::PlaybackUpdate&);
void react(const audio::QueueUpdate&); virtual void react(const audio::QueueUpdate&);
virtual void react(const system_fsm::KeyLockChanged&); virtual void react(const system_fsm::KeyLockChanged&);
virtual void react(const OnLuaError&) {} virtual void react(const OnLuaError&) {}
virtual void react(const internal::RecordSelected&) {} virtual void react(const internal::RecordSelected&) {}
virtual void react(const internal::IndexSelected&) {}
virtual void react(const internal::BackPressed&) {} virtual void react(const internal::BackPressed&) {}
virtual void react(const internal::ShowNowPlaying&){};
virtual void react(const internal::ShowSettingsPage&){}; virtual void react(const internal::ShowSettingsPage&){};
virtual void react(const internal::ModalCancelPressed&) { virtual void react(const internal::ModalCancelPressed&) {
sCurrentModal.reset(); sCurrentModal.reset();
@ -127,12 +123,12 @@ class Lua : public UiState {
void react(const OnLuaError&) override; void react(const OnLuaError&) override;
void react(const internal::IndexSelected&) override;
void react(const internal::ShowNowPlaying&) override;
void react(const internal::ShowSettingsPage&) override; void react(const internal::ShowSettingsPage&) override;
void react(const system_fsm::BatteryStateChanged&) override; void react(const system_fsm::BatteryStateChanged&) override;
void react(const audio::QueueUpdate&) override;
void react(const audio::PlaybackStarted&) override; void react(const audio::PlaybackStarted&) override;
void react(const audio::PlaybackUpdate&) override;
void react(const audio::PlaybackFinished&) override; void react(const audio::PlaybackFinished&) override;
using UiState::react; using UiState::react;
@ -144,32 +140,23 @@ class Lua : public UiState {
std::shared_ptr<lua::Property> battery_pct_; std::shared_ptr<lua::Property> battery_pct_;
std::shared_ptr<lua::Property> battery_mv_; std::shared_ptr<lua::Property> battery_mv_;
std::shared_ptr<lua::Property> battery_charging_; std::shared_ptr<lua::Property> battery_charging_;
std::shared_ptr<lua::Property> bluetooth_en_; std::shared_ptr<lua::Property> bluetooth_en_;
std::shared_ptr<lua::Property> playback_playing_; std::shared_ptr<lua::Property> playback_playing_;
std::shared_ptr<lua::Property> playback_track_; std::shared_ptr<lua::Property> playback_track_;
}; std::shared_ptr<lua::Property> playback_position_;
class Onboarding : public UiState {
public:
void entry() override;
void react(const internal::OnboardingNavigate&) override;
using UiState::react; std::shared_ptr<lua::Property> queue_position_;
std::shared_ptr<lua::Property> queue_size_;
private:
uint8_t progress_;
bool has_formatted_;
}; };
class Browse : public UiState { class Browse : public UiState {
public: public:
void entry() override; void entry() override;
void react(const internal::RecordSelected&) override;
void react(const internal::BackPressed&) override; void react(const internal::BackPressed&) override;
void react(const internal::ShowNowPlaying&) override;
void react(const internal::ShowSettingsPage&) override; void react(const internal::ShowSettingsPage&) override;
void react(const internal::ReindexDatabase&) override; void react(const internal::ReindexDatabase&) override;
@ -178,16 +165,6 @@ class Browse : public UiState {
using UiState::react; using UiState::react;
}; };
class Playing : public UiState {
public:
void entry() override;
void exit() override;
void react(const internal::BackPressed&) override;
using UiState::react;
};
class Indexing : public UiState { class Indexing : public UiState {
public: public:
void entry() override; void entry() override;

@ -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

@ -6,9 +6,9 @@
#include "screen_lua.hpp" #include "screen_lua.hpp"
#include "lauxlib.h" #include "core/lv_obj_tree.h"
#include "lua.h"
#include "lua.hpp" #include "lua.hpp"
#include "luavgl.h" #include "luavgl.h"
namespace ui { 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,338 +0,0 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "screen_playing.hpp"
#include <sys/_stdint.h>
#include <memory>
#include "audio_events.hpp"
#include "bindey/binding.h"
#include "core/lv_event.h"
#include "core/lv_obj.h"
#include "core/lv_obj_scroll.h"
#include "core/lv_obj_tree.h"
#include "database.hpp"
#include "esp_log.h"
#include "extra/layouts/flex/lv_flex.h"
#include "extra/layouts/grid/lv_grid.h"
#include "font/lv_symbol_def.h"
#include "future_fetcher.hpp"
#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 "future_fetcher.hpp"
#include "hal/lv_hal_disp.h"
#include "index.hpp"
#include "misc/lv_anim.h"
#include "misc/lv_area.h"
#include "misc/lv_color.h"
#include "misc/lv_txt.h"
#include "model_playback.hpp"
#include "model_top_bar.hpp"
#include "track.hpp"
#include "ui_events.hpp"
#include "ui_fsm.hpp"
#include "widget_top_bar.hpp"
#include "widgets/lv_btn.h"
#include "widgets/lv_img.h"
#include "widgets/lv_label.h"
#include "widgets/lv_slider.h"
namespace ui {
namespace screens {
static void above_fold_focus_cb(lv_event_t* ev) {
if (ev->user_data == NULL) {
return;
}
Playing* instance = reinterpret_cast<Playing*>(ev->user_data);
instance->OnFocusAboveFold();
}
static void below_fold_focus_cb(lv_event_t* ev) {
if (ev->user_data == NULL) {
return;
}
Playing* instance = reinterpret_cast<Playing*>(ev->user_data);
instance->OnFocusBelowFold();
}
static lv_style_t scrubber_style;
auto info_label(lv_obj_t* parent) -> lv_obj_t* {
lv_obj_t* label = lv_label_create(parent);
lv_obj_set_size(label, lv_pct(100), LV_SIZE_CONTENT);
lv_label_set_text(label, "");
lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_center(label);
lv_obj_set_style_bg_color(label, lv_color_black(), LV_STATE_FOCUSED);
return label;
}
auto Playing::control_button(lv_obj_t* parent, char* icon) -> lv_obj_t* {
lv_obj_t* button = lv_btn_create(parent);
lv_obj_set_size(button, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_clear_flag(button, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
lv_obj_add_event_cb(button, above_fold_focus_cb, LV_EVENT_FOCUSED, this);
lv_obj_t* icon_obj = lv_img_create(button);
lv_img_set_src(icon_obj, icon);
return button;
}
auto Playing::next_up_label(lv_obj_t* parent, const std::pmr::string& text)
-> lv_obj_t* {
lv_obj_t* button = lv_list_add_btn(parent, NULL, text.c_str());
lv_label_set_long_mode(lv_obj_get_child(button, -1), LV_LABEL_LONG_DOT);
lv_obj_add_event_cb(button, below_fold_focus_cb, LV_EVENT_FOCUSED, this);
lv_group_add_obj(group_, button);
return button;
}
Playing::Playing(models::TopBar& top_bar_model,
models::Playback& playback_model,
std::weak_ptr<database::Database> db,
audio::TrackQueue& queue)
: db_(db),
queue_(queue),
current_track_(),
next_tracks_(),
new_track_(),
new_next_tracks_() {
lv_obj_set_layout(content_, LV_LAYOUT_FLEX);
lv_group_set_wrap(group_, false);
lv_obj_set_size(content_, lv_pct(100), lv_disp_get_ver_res(NULL));
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START);
lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
lv_obj_t* above_fold_container = lv_obj_create(content_);
lv_obj_set_layout(above_fold_container, LV_LAYOUT_FLEX);
lv_obj_set_size(above_fold_container, lv_pct(100), lv_disp_get_ver_res(NULL));
lv_obj_set_flex_flow(above_fold_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(above_fold_container, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
widgets::TopBar::Configuration config{
.show_back_button = true,
.title = "Now Playing",
};
CreateTopBar(above_fold_container, config, top_bar_model);
lv_obj_t* now_playing_container = lv_obj_create(above_fold_container);
lv_obj_set_layout(now_playing_container, LV_LAYOUT_FLEX);
lv_obj_set_width(now_playing_container, lv_pct(100));
lv_obj_set_flex_grow(now_playing_container, 1);
lv_obj_set_flex_flow(now_playing_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(now_playing_container, LV_FLEX_ALIGN_SPACE_BETWEEN,
LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_left(now_playing_container, 4, LV_PART_MAIN);
lv_obj_set_style_pad_right(now_playing_container, 4, LV_PART_MAIN);
lv_obj_t* info_container = lv_obj_create(now_playing_container);
lv_obj_set_layout(info_container, LV_LAYOUT_FLEX);
lv_obj_set_width(info_container, lv_pct(100));
lv_obj_set_flex_grow(info_container, 1);
lv_obj_set_flex_flow(info_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(info_container, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t* artist_label = info_label(info_container);
lv_obj_t* album_label = info_label(info_container);
lv_obj_t* title_label = info_label(info_container);
lv_obj_t* scrubber = lv_slider_create(now_playing_container);
lv_obj_set_size(scrubber, lv_pct(100), 5);
lv_style_init(&scrubber_style);
lv_style_set_bg_color(&scrubber_style, lv_color_black());
lv_obj_add_style(scrubber, &scrubber_style, LV_PART_INDICATOR);
lv_group_add_obj(group_, scrubber);
data_bindings_.emplace_back(playback_model.current_track.onChangedAndNow(
[=, this](const std::optional<database::TrackId>& id) {
if (!id) {
return;
}
if (current_track_.get() && current_track_.get()->data().id == *id) {
return;
}
auto db = db_.lock();
if (!db) {
return;
}
// Clear the playback progress whilst we're waiting for the next
// track's data to load.
lv_slider_set_value(scrubber, 0, LV_ANIM_OFF);
new_track_.reset(
new database::FutureFetcher<std::shared_ptr<database::Track>>(
db->GetTrack(*id)));
}));
data_bindings_.emplace_back(current_track_.onChangedAndNow(
[=](const std::shared_ptr<database::Track>& t) {
if (!t) {
return;
}
lv_label_set_text(
artist_label,
t->tags().at(database::Tag::kArtist).value_or("").c_str());
lv_label_set_text(
album_label,
t->tags().at(database::Tag::kAlbum).value_or("").c_str());
lv_label_set_text(title_label, t->TitleOrFilename().c_str());
}));
data_bindings_.emplace_back(
playback_model.current_track_duration.onChangedAndNow([=](uint32_t d) {
lv_slider_set_range(scrubber, 0, std::max<uint32_t>(1, d));
}));
data_bindings_.emplace_back(
playback_model.current_track_position.onChangedAndNow(
[=](uint32_t p) { lv_slider_set_value(scrubber, p, LV_ANIM_OFF); }));
lv_obj_t* spacer = lv_obj_create(now_playing_container);
lv_obj_set_size(spacer, 1, 4);
lv_obj_t* controls_container = lv_obj_create(now_playing_container);
lv_obj_set_size(controls_container, lv_pct(100), 20);
lv_obj_set_flex_flow(controls_container, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(controls_container, LV_FLEX_ALIGN_SPACE_EVENLY,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t* play_pause_control =
control_button(controls_container, LV_SYMBOL_PLAY);
lv_group_add_obj(group_, play_pause_control);
lv_bind(play_pause_control, LV_EVENT_CLICKED, [=](lv_obj_t*) {
events::Audio().Dispatch(audio::TogglePlayPause{});
});
lv_obj_t* track_prev = control_button(controls_container, LV_SYMBOL_PREV);
lv_group_add_obj(group_, track_prev);
lv_bind(track_prev, LV_EVENT_CLICKED, [=](lv_obj_t*) { queue_.Previous(); });
lv_obj_t* track_next = control_button(controls_container, LV_SYMBOL_NEXT);
lv_group_add_obj(group_, track_next);
lv_bind(track_next, LV_EVENT_CLICKED, [=](lv_obj_t*) { queue_.Next(); });
lv_obj_t* shuffle = control_button(controls_container, LV_SYMBOL_SHUFFLE);
lv_group_add_obj(group_, shuffle);
// lv_bind(shuffle, LV_EVENT_CLICKED, [=](lv_obj_t*) { queue_ });
lv_group_add_obj(group_, control_button(controls_container, LV_SYMBOL_LOOP));
next_up_header_ = lv_obj_create(now_playing_container);
lv_obj_set_size(next_up_header_, lv_pct(100), 15);
lv_obj_set_flex_flow(next_up_header_, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(next_up_header_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_END,
LV_FLEX_ALIGN_END);
next_up_label_ = lv_label_create(next_up_header_);
lv_label_set_text(next_up_label_, "");
lv_obj_set_height(next_up_label_, lv_pct(100));
lv_obj_set_flex_grow(next_up_label_, 1);
next_up_hint_ = lv_label_create(next_up_header_);
lv_label_set_text(next_up_hint_, "");
lv_obj_set_size(next_up_hint_, LV_SIZE_CONTENT, lv_pct(100));
next_up_container_ = lv_list_create(content_);
lv_obj_set_layout(next_up_container_, LV_LAYOUT_FLEX);
lv_obj_set_size(next_up_container_, lv_pct(100), lv_disp_get_ver_res(NULL));
lv_obj_set_flex_flow(next_up_container_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(next_up_container_, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
data_bindings_.emplace_back(playback_model.upcoming_tracks.onChangedAndNow(
[=, this](const std::vector<database::TrackId>& ids) {
auto db = db_.lock();
if (!db) {
return;
}
lv_label_set_text(next_up_label_, "Next up");
new_next_tracks_.reset(new database::FutureFetcher<
std::vector<std::shared_ptr<database::Track>>>(
db->GetBulkTracks(ids)));
}));
data_bindings_.emplace_back(next_tracks_.onChangedAndNow(
[=](const std::vector<std::shared_ptr<database::Track>>& tracks) {
// TODO(jacqueline): Do a proper diff to maintain selection.
int children = lv_obj_get_child_cnt(next_up_container_);
while (children > 0) {
lv_obj_del(lv_obj_get_child(next_up_container_, 0));
children--;
}
if (tracks.empty()) {
lv_label_set_text(next_up_label_, "Nothing queued");
lv_label_set_text(next_up_hint_, "");
return;
} else {
lv_label_set_text(next_up_label_, "Next up");
lv_label_set_text(next_up_hint_, "");
}
for (const auto& track : tracks) {
lv_group_add_obj(group_, next_up_label(next_up_container_,
track->TitleOrFilename()));
}
}));
}
Playing::~Playing() {}
auto Playing::Tick() -> void {
if (new_track_ && new_track_->Finished()) {
auto res = new_track_->Result();
new_track_.reset();
if (res) {
current_track_(*res);
}
}
if (new_next_tracks_ && new_next_tracks_->Finished()) {
auto res = new_next_tracks_->Result();
new_next_tracks_.reset();
if (res) {
std::vector<std::shared_ptr<database::Track>> filtered;
for (const auto& t : *res) {
if (t) {
filtered.push_back(t);
}
}
next_tracks_.set(filtered);
}
}
}
auto Playing::OnFocusAboveFold() -> void {
lv_obj_scroll_to_y(content_, 0, LV_ANIM_ON);
}
auto Playing::OnFocusBelowFold() -> void {
if (lv_obj_get_scroll_y(content_) < lv_obj_get_y(next_up_header_) + 20) {
lv_obj_scroll_to_y(content_, lv_obj_get_y(next_up_header_) + 20,
LV_ANIM_ON);
}
}
} // 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

@ -30,7 +30,6 @@
#include "event_queue.hpp" #include "event_queue.hpp"
#include "gpios.hpp" #include "gpios.hpp"
#include "lvgl_task.hpp" #include "lvgl_task.hpp"
#include "modal_add_to_queue.hpp"
#include "modal_confirm.hpp" #include "modal_confirm.hpp"
#include "modal_progress.hpp" #include "modal_progress.hpp"
#include "model_playback.hpp" #include "model_playback.hpp"
@ -39,11 +38,8 @@
#include "relative_wheel.hpp" #include "relative_wheel.hpp"
#include "screen.hpp" #include "screen.hpp"
#include "screen_lua.hpp" #include "screen_lua.hpp"
#include "screen_onboarding.hpp"
#include "screen_playing.hpp"
#include "screen_settings.hpp" #include "screen_settings.hpp"
#include "screen_splash.hpp" #include "screen_splash.hpp"
#include "screen_track_browser.hpp"
#include "source.hpp" #include "source.hpp"
#include "spiffs.hpp" #include "spiffs.hpp"
#include "storage.hpp" #include "storage.hpp"
@ -51,15 +47,12 @@
#include "touchwheel.hpp" #include "touchwheel.hpp"
#include "track_queue.hpp" #include "track_queue.hpp"
#include "ui_events.hpp" #include "ui_events.hpp"
#include "widget_top_bar.hpp"
#include "widgets/lv_label.h" #include "widgets/lv_label.h"
namespace ui { namespace ui {
[[maybe_unused]] static constexpr char kTag[] = "ui_fsm"; [[maybe_unused]] static constexpr char kTag[] = "ui_fsm";
static const std::size_t kRecordsPerPage = 15;
std::unique_ptr<UiTask> UiState::sTask; std::unique_ptr<UiTask> UiState::sTask;
std::shared_ptr<system_fsm::ServiceLocator> UiState::sServices; std::shared_ptr<system_fsm::ServiceLocator> UiState::sServices;
std::unique_ptr<drivers::Display> UiState::sDisplay; std::unique_ptr<drivers::Display> UiState::sDisplay;
@ -116,27 +109,17 @@ void UiState::react(const system_fsm::BatteryStateChanged& ev) {
sTopBarModel.battery_state.set(ev.new_state); sTopBarModel.battery_state.set(ev.new_state);
} }
void UiState::react(const audio::PlaybackStarted&) { void UiState::react(const audio::PlaybackStarted&) {}
sPlaybackModel.is_playing.set(true);
}
void UiState::react(const audio::PlaybackFinished&) { void UiState::react(const audio::PlaybackFinished&) {}
sPlaybackModel.is_playing.set(false);
}
void UiState::react(const audio::PlaybackUpdate& ev) { void UiState::react(const audio::PlaybackUpdate& ev) {}
sPlaybackModel.current_track_duration.set(ev.seconds_total);
sPlaybackModel.current_track_position.set(ev.seconds_elapsed);
}
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(); 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&) {
@ -192,8 +175,13 @@ void Lua::entry() {
battery_charging_ = std::make_shared<lua::Property>(bat.is_charging); battery_charging_ = std::make_shared<lua::Property>(bat.is_charging);
bluetooth_en_ = std::make_shared<lua::Property>(false); bluetooth_en_ = std::make_shared<lua::Property>(false);
queue_position_ = std::make_shared<lua::Property>(0);
queue_size_ = std::make_shared<lua::Property>(0);
playback_playing_ = std::make_shared<lua::Property>(false); playback_playing_ = std::make_shared<lua::Property>(false);
playback_track_ = std::make_shared<lua::Property>(); playback_track_ = std::make_shared<lua::Property>();
playback_position_ = std::make_shared<lua::Property>();
sLua.reset(lua::LuaThread::Start(*sServices, sCurrentScreen->content())); sLua.reset(lua::LuaThread::Start(*sServices, sCurrentScreen->content()));
sLua->bridge().AddPropertyModule("power", sLua->bridge().AddPropertyModule("power",
@ -211,6 +199,11 @@ void Lua::entry() {
{ {
{"playing", playback_playing_}, {"playing", playback_playing_},
{"track", playback_track_}, {"track", playback_track_},
{"position", playback_position_},
});
sLua->bridge().AddPropertyModule("queue", {
{"position", queue_position_},
{"size", queue_size_},
}); });
sLua->bridge().AddPropertyModule( sLua->bridge().AddPropertyModule(
"backstack", "backstack",
@ -233,7 +226,7 @@ auto Lua::PushLuaScreen(lua_State* s) -> int {
auto new_screen = std::make_shared<screens::Lua>(); auto new_screen = std::make_shared<screens::Lua>();
// Tell lvgl about the new roots. // Tell lvgl about the new roots.
luavgl_set_root(s, new_screen->root()); luavgl_set_root(s, new_screen->content());
lv_group_set_default(new_screen->group()); lv_group_set_default(new_screen->group());
// Call the constructor for this screen. // Call the constructor for this screen.
@ -252,7 +245,7 @@ auto Lua::PushLuaScreen(lua_State* s) -> int {
auto Lua::PopLuaScreen(lua_State* s) -> int { auto Lua::PopLuaScreen(lua_State* s) -> int {
PopScreen(); PopScreen();
luavgl_set_root(s, sCurrentScreen->root()); luavgl_set_root(s, sCurrentScreen->content());
lv_group_set_default(sCurrentScreen->group()); lv_group_set_default(sCurrentScreen->group());
return 0; return 0;
} }
@ -265,25 +258,6 @@ void Lua::react(const OnLuaError& err) {
ESP_LOGE("lua", "%s", err.message.c_str()); ESP_LOGE("lua", "%s", err.message.c_str());
} }
void Lua::react(const internal::IndexSelected& ev) {
auto db = sServices->database().lock();
if (!db) {
return;
}
ESP_LOGI(kTag, "selected index %u", ev.id);
auto query = db->GetTracksByIndex(ev.id, kRecordsPerPage);
std::pmr::vector<std::pmr::string> crumbs = {""};
PushScreen(std::make_shared<screens::TrackBrowser>(
sTopBarModel, sServices->track_queue(), sServices->database(), crumbs,
std::move(query)));
transit<Browse>();
}
void Lua::react(const internal::ShowNowPlaying&) {
transit<Playing>();
}
void Lua::react(const internal::ShowSettingsPage& ev) { void Lua::react(const internal::ShowSettingsPage& ev) {
PushScreen(std::shared_ptr<Screen>(new screens::Settings(sTopBarModel))); PushScreen(std::shared_ptr<Screen>(new screens::Settings(sTopBarModel)));
transit<Browse>(); transit<Browse>();
@ -294,71 +268,27 @@ void Lua::react(const system_fsm::BatteryStateChanged& ev) {
battery_mv_->Update(static_cast<int>(ev.new_state.millivolts)); battery_mv_->Update(static_cast<int>(ev.new_state.millivolts));
} }
void Lua::react(const audio::PlaybackStarted&) { void Lua::react(const audio::QueueUpdate&) {
playback_playing_->Update(true); auto& queue = sServices->track_queue();
queue_size_->Update(static_cast<int>(queue.Size()));
queue_position_->Update(static_cast<int>(queue.Position()));
} }
void Lua::react(const audio::PlaybackFinished&) { void Lua::react(const audio::PlaybackStarted& ev) {
playback_playing_->Update(false); playback_playing_->Update(true);
} }
void Onboarding::entry() { void Lua::react(const audio::PlaybackUpdate& ev) {
progress_ = 0; playback_track_->Update(*ev.track);
has_formatted_ = false; playback_position_->Update(static_cast<int>(ev.seconds_elapsed));
sCurrentScreen.reset(new screens::onboarding::LinkToManual());
} }
void Onboarding::react(const internal::OnboardingNavigate& ev) { void Lua::react(const audio::PlaybackFinished&) {
int dir = ev.forwards ? 1 : -1; playback_playing_->Update(false);
progress_ += dir;
for (;;) {
if (progress_ == 0) {
sCurrentScreen.reset(new screens::onboarding::LinkToManual());
return;
} else if (progress_ == 1) {
sCurrentScreen.reset(new screens::onboarding::Controls());
return;
} else if (progress_ == 2) {
if (sServices->sd() == drivers::SdState::kNotPresent) {
sCurrentScreen.reset(new screens::onboarding::MissingSdCard());
return;
} else {
progress_ += dir;
}
} else if (progress_ == 3) {
if (sServices->sd() == drivers::SdState::kNotFormatted) {
has_formatted_ = true;
sCurrentScreen.reset(new screens::onboarding::FormatSdCard());
return;
} else {
progress_ += dir;
}
} else if (progress_ == 4) {
if (has_formatted_) {
// If we formatted the SD card during this onboarding flow, then there
// is no music that needs indexing.
progress_ += dir;
} else {
sCurrentScreen.reset(new screens::onboarding::InitDatabase());
return;
}
} else {
// We finished onboarding! Ensure this flow doesn't appear again.
sServices->nvs().HasShownOnboarding(true);
transit<Browse>();
return;
}
}
} }
void Browse::entry() {} void Browse::entry() {}
void Browse::react(const internal::ShowNowPlaying& ev) {
transit<Playing>();
}
void Browse::react(const internal::ShowSettingsPage& ev) { void Browse::react(const internal::ShowSettingsPage& ev) {
std::shared_ptr<Screen> screen; std::shared_ptr<Screen> screen;
std::shared_ptr<screens::Bluetooth> bt_screen; std::shared_ptr<screens::Bluetooth> bt_screen;
@ -397,47 +327,6 @@ void Browse::react(const internal::ShowSettingsPage& ev) {
} }
} }
void Browse::react(const internal::RecordSelected& ev) {
auto db = sServices->database().lock();
if (!db) {
return;
}
auto& queue = sServices->track_queue();
auto record = ev.page->values().at(ev.record);
if (record->track()) {
ESP_LOGI(kTag, "selected track '%s'", record->text()->c_str());
auto source = std::make_shared<playlist::IndexRecordSource>(
sServices->database(), ev.initial_page, 0, ev.page, ev.record);
if (ev.show_menu) {
sCurrentModal.reset(
new modals::AddToQueue(sCurrentScreen.get(), queue, source));
} else {
queue.Clear();
queue.AddNext(source);
transit<Playing>();
}
} else {
ESP_LOGI(kTag, "selected record '%s'", record->text()->c_str());
auto cont = record->Expand(kRecordsPerPage);
if (!cont) {
return;
}
auto query = db->GetPage<database::IndexRecord>(&cont.value());
if (ev.show_menu) {
std::shared_ptr<database::Result<database::IndexRecord>> res{query.get()};
auto source = playlist::CreateSourceFromResults(db, res);
sCurrentModal.reset(
new modals::AddToQueue(sCurrentScreen.get(), queue, source, true));
} else {
std::pmr::string title = record->text().value_or("");
PushScreen(std::make_shared<screens::TrackBrowser>(
sTopBarModel, sServices->track_queue(), sServices->database(),
ev.new_crumbs, std::move(query)));
}
}
}
void Browse::react(const internal::BackPressed& ev) { void Browse::react(const internal::BackPressed& ev) {
if (PopScreen() == 0) { if (PopScreen() == 0) {
transit<Lua>(); transit<Lua>();
@ -455,28 +344,6 @@ void Browse::react(const internal::ReindexDatabase& ev) {
transit<Indexing>(); transit<Indexing>();
} }
static std::shared_ptr<screens::Playing> sPlayingScreen;
void Playing::entry() {
ESP_LOGI(kTag, "push playing screen");
sPlayingScreen.reset(new screens::Playing(sTopBarModel, sPlaybackModel,
sServices->database(),
sServices->track_queue()));
PushScreen(sPlayingScreen);
}
void Playing::exit() {
sPlayingScreen.reset();
}
void Playing::react(const internal::BackPressed& ev) {
if (PopScreen() == 0) {
transit<Lua>();
} else {
transit<Browse>();
}
}
static std::shared_ptr<modals::Progress> sIndexProgress; static std::shared_ptr<modals::Progress> sIndexProgress;
void Indexing::entry() { void Indexing::entry() {

@ -19,16 +19,6 @@
#include "widgets/lv_img.h" #include "widgets/lv_img.h"
#include "widgets/lv_label.h" #include "widgets/lv_label.h"
LV_IMG_DECLARE(kIconBluetooth);
LV_IMG_DECLARE(kIconPlay);
LV_IMG_DECLARE(kIconPause);
LV_IMG_DECLARE(kIconBatteryEmpty);
LV_IMG_DECLARE(kIconBattery20);
LV_IMG_DECLARE(kIconBattery40);
LV_IMG_DECLARE(kIconBattery60);
LV_IMG_DECLARE(kIconBattery80);
LV_IMG_DECLARE(kIconBatteryFull);
namespace ui { namespace ui {
namespace widgets { namespace widgets {
@ -64,46 +54,6 @@ TopBar::TopBar(lv_obj_t* parent,
lv_label_set_text(title_, config.title.c_str()); lv_label_set_text(title_, config.title.c_str());
lv_label_set_long_mode(title_, LV_LABEL_LONG_DOT); lv_label_set_long_mode(title_, LV_LABEL_LONG_DOT);
lv_obj_t* playback = lv_img_create(container_);
bindings_.push_back(model.is_playing.onChangedAndNow([=](bool is_playing) {
lv_img_set_src(playback, is_playing ? &kIconPlay : &kIconPause);
}));
bindings_.push_back(model.current_track.onChangedAndNow(
[=](const std::optional<database::TrackId>& id) {
if (id) {
lv_obj_clear_flag(playback, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(playback, LV_OBJ_FLAG_HIDDEN);
}
}));
lv_obj_t* battery = lv_img_create(container_);
lv_obj_t* charging = lv_label_create(container_);
bindings_.push_back(model.battery_state.onChangedAndNow(
[=](const battery::Battery::BatteryState& state) {
if (state.is_charging) {
lv_label_set_text(charging, "+");
} else {
lv_label_set_text(charging, "");
}
if (state.percent >= 95) {
lv_img_set_src(battery, &kIconBatteryFull);
} else if (state.percent >= 75) {
lv_img_set_src(battery, &kIconBattery80);
} else if (state.percent >= 55) {
lv_img_set_src(battery, &kIconBattery60);
} else if (state.percent >= 35) {
lv_img_set_src(battery, &kIconBattery40);
} else if (state.percent >= 15) {
lv_img_set_src(battery, &kIconBattery20);
} else {
lv_img_set_src(battery, &kIconBatteryEmpty);
}
}));
themes::Theme::instance()->ApplyStyle(container_, themes::Style::kTopBar); themes::Theme::instance()->ApplyStyle(container_, themes::Style::kTopBar);
} }

Loading…
Cancel
Save