diff --git a/lib/luavgl/src/lvgl.lua b/lib/luavgl/src/lvgl.lua index f30335cb..f4505a67 100644 --- a/lib/luavgl/src/lvgl.lua +++ b/lib/luavgl/src/lvgl.lua @@ -335,6 +335,13 @@ end function lvgl.Object(parent, property) 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 --- @param parent? Object | nil --- @param property? StyleProp @@ -471,6 +478,13 @@ obj = {} function obj:Object(property) end +--- +--- Create bar on object +--- @param property? BarStyle +--- @return Bar +function obj:Bar(property) +end + --- --- Create button on object --- @param property? ButtonStyle @@ -866,6 +880,18 @@ end function calendar:Dropdown(p) 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 ---@class Button:Object @@ -1375,6 +1401,11 @@ end --- @class LabelStyle :StyleProp --- @field text string +--- Bar style +--- @class BarStyle :StyleProp +--- @field range BarRangePara +--- @field value integer + --- Button style --- @class ButtonStyle :StyleProp @@ -1472,6 +1503,13 @@ end --- @field align_content flexAlignOptions +--- +--- BarRange para +--- @class BarRangePara +--- @field min integer +--- @field max integer +--- + --- --- CalendarToday para --- @class CalendarDatePara diff --git a/lib/luavgl/src/obj.c b/lib/luavgl/src/obj.c index a316f59c..eccfd753 100644 --- a/lib/luavgl/src/obj.c +++ b/lib/luavgl/src/obj.c @@ -9,8 +9,7 @@ static int luavgl_anim_create(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)) { lv_obj_align(obj, lua_tointeger(L, -1), 0, 0); return; @@ -42,13 +41,11 @@ static void _lv_obj_set_align(void *obj, lua_State *L) * * 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); } -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_pushlightuserdata(L, e->current_target); 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); - 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; + } luavgl_obj_delete(L); return; @@ -73,8 +78,7 @@ pop_exit: * one. result stack: table(from uservalue) * 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); if (type == LUA_TTABLE) return type; @@ -93,13 +97,11 @@ LUALIB_API int luavgl_obj_getuserdatauv(lua_State *L, int idx) 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); } -static int luavgl_obj_delete(lua_State *L) -{ +static int luavgl_obj_delete(lua_State *L) { luavgl_obj_t *lobj; /** @@ -149,8 +151,7 @@ static int luavgl_obj_delete(lua_State *L) 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); if (lobj == NULL || lobj->obj == NULL) return 0; @@ -171,8 +172,7 @@ static int luavgl_obj_clean(lua_State *L) 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); 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}) */ -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); if (!lua_istable(L, 2)) { @@ -220,16 +219,14 @@ static int luavgl_obj_align_to(lua_State *L) 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 *parent = luavgl_to_obj(L, 2); lv_obj_set_parent(obj, parent); 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 *screen = lv_obj_get_screen(obj); @@ -247,8 +244,7 @@ static int luavgl_obj_get_screen(lua_State *L) 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 *parent = lv_obj_get_parent(obj); @@ -265,8 +261,7 @@ static int luavgl_obj_get_parent(lua_State *L) 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); if (!lua_isnoneornil(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); } -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); int id = luavgl_tointeger(L, 2); @@ -297,15 +291,13 @@ static int luavgl_obj_get_child(lua_State *L) 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); lua_pushinteger(L, lv_obj_get_child_cnt(obj)); 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_state_t state = lv_obj_get_state(obj); 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, 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); if (!lua_istable(L, -1)) { @@ -348,16 +339,14 @@ static int luavgl_obj_scroll_to(lua_State *L) 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); lua_pushboolean(L, lv_obj_is_visible(obj)); 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_flag_t flag = lua_tointeger(L, 2); lv_obj_add_flag(obj, flag); @@ -365,8 +354,7 @@ static int luavgl_obj_add_flag(lua_State *L) 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_flag_t flag = lua_tointeger(L, 2); lv_obj_clear_flag(obj, flag); @@ -374,16 +362,14 @@ static int luavgl_obj_clear_flag(lua_State *L) 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_state_t state = lua_tointeger(L, 2); lv_obj_add_state(obj, state); 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_state_t state = lua_tointeger(L, 2); 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) */ -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); int x = luavgl_tointeger(L, 2); int y = luavgl_tointeger(L, 3); @@ -404,8 +389,7 @@ static int luavgl_obj_scroll_by(lua_State *L) 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); int dx = luavgl_tointeger(L, 2); int dy = luavgl_tointeger(L, 3); @@ -415,8 +399,7 @@ static int luavgl_obj_scroll_by_bounded(lua_State *L) 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); int anim_en = luavgl_tointeger(L, 2); @@ -424,8 +407,7 @@ static int luavgl_obj_scroll_to_view(lua_State *L) 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); int anim_en = luavgl_tointeger(L, 2); @@ -433,8 +415,7 @@ static int luavgl_obj_scroll_to_view_recursive(lua_State *L) 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); int x = luavgl_tointeger(L, 2); int y = luavgl_tointeger(L, 3); @@ -443,72 +424,62 @@ static int luavgl_obj_scroll_by_raw(lua_State *L) 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); lua_pushboolean(L, lv_obj_is_scrolling(obj)); 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_scrollbar_invalidate(obj); 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); int anim_en = luavgl_tointeger(L, 2); lv_obj_readjust_scroll(obj, anim_en); 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); lua_pushboolean(L, lv_obj_is_editable(obj)); 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); lua_pushboolean(L, lv_obj_is_group_def(obj)); 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); lua_pushboolean(L, lv_obj_is_layout_positioned(obj)); 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_mark_layout_as_dirty(obj); 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_center(obj); 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_invalidate(obj); 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_flex_flow_t flow = luavgl_tointeger(L, 2); @@ -516,8 +487,7 @@ static int luavgl_obj_set_flex_flow(lua_State *L) 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_flex_align_t m = luavgl_tointeger(L, 2); 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; } -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); uint8_t grow = luavgl_tointeger(L, 2); @@ -536,8 +505,7 @@ static int luavgl_obj_set_flex_grow(lua_State *L) 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_point_t point; if (lua_istable(L, 2)) { @@ -567,8 +535,7 @@ static int luavgl_obj_indev_search(lua_State *L) 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_obj_t *obj = luavgl_to_obj(L, 1); 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 */ -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); lua_newtable(L); @@ -615,15 +581,13 @@ static int luavgl_obj_get_pos(lua_State *L) /** * 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_anim_del(obj, NULL); 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 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 @@ -647,62 +611,61 @@ static int luavgl_obj_gc(lua_State *L) } static const luaL_Reg luavgl_obj_methods[] = { - {"set", luavgl_obj_set }, - {"set_style", luavgl_obj_set_style }, - {"align_to", luavgl_obj_align_to }, - {"delete", luavgl_obj_delete }, - {"clean", luavgl_obj_clean }, - - /* misc. functions */ - {"parent", luavgl_obj_set_get_parent }, - {"set_parent", luavgl_obj_set_parent }, - {"get_parent", luavgl_obj_get_parent }, - {"get_child", luavgl_obj_get_child }, - {"get_child_cnt", luavgl_obj_get_child_cnt }, - {"get_screen", luavgl_obj_get_screen }, - {"get_state", luavgl_obj_get_state }, - {"scroll_to", luavgl_obj_scroll_to }, - {"is_scrolling", luavgl_obj_is_scrolling }, - {"is_visible", luavgl_obj_is_visible }, - {"add_flag", luavgl_obj_add_flag }, - {"clear_flag", luavgl_obj_clear_flag }, - {"add_state", luavgl_obj_add_state }, - {"clear_state", luavgl_obj_clear_state }, - {"add_style", luavgl_obj_add_style }, - {"remove_style", luavgl_obj_remove_style }, - {"remove_style_all", luavgl_obj_remove_style_all }, - {"scroll_by", luavgl_obj_scroll_by }, - {"scroll_by_bounded", luavgl_obj_scroll_by_bounded }, - {"scroll_to_view", luavgl_obj_scroll_to_view }, + {"set", luavgl_obj_set}, + {"set_style", luavgl_obj_set_style}, + {"align_to", luavgl_obj_align_to}, + {"delete", luavgl_obj_delete}, + {"clean", luavgl_obj_clean}, + + /* misc. functions */ + {"parent", luavgl_obj_set_get_parent}, + {"set_parent", luavgl_obj_set_parent}, + {"get_parent", luavgl_obj_get_parent}, + {"get_child", luavgl_obj_get_child}, + {"get_child_cnt", luavgl_obj_get_child_cnt}, + {"get_screen", luavgl_obj_get_screen}, + {"get_state", luavgl_obj_get_state}, + {"scroll_to", luavgl_obj_scroll_to}, + {"is_scrolling", luavgl_obj_is_scrolling}, + {"is_visible", luavgl_obj_is_visible}, + {"add_flag", luavgl_obj_add_flag}, + {"clear_flag", luavgl_obj_clear_flag}, + {"add_state", luavgl_obj_add_state}, + {"clear_state", luavgl_obj_clear_state}, + {"add_style", luavgl_obj_add_style}, + {"remove_style", luavgl_obj_remove_style}, + {"remove_style_all", luavgl_obj_remove_style_all}, + {"scroll_by", luavgl_obj_scroll_by}, + {"scroll_by_bounded", luavgl_obj_scroll_by_bounded}, + {"scroll_to_view", luavgl_obj_scroll_to_view}, {"scroll_to_view_recursive", luavgl_obj_scroll_to_view_recursive}, - {"scroll_by_raw", luavgl_obj_scroll_by_raw }, - {"scrollbar_invalidate", luavgl_obj_scrollbar_invalidate }, - {"readjust_scroll", luavgl_obj_readjust_scroll }, - {"is_editable", luavgl_obj_is_editable }, - {"is_group_def", luavgl_obj_is_group_def }, - {"is_layout_positioned", luavgl_obj_is_layout_positioned }, - {"mark_layout_as_dirty", luavgl_obj_mark_layout_as_dirty }, - {"center", luavgl_obj_center }, - {"invalidate", luavgl_obj_invalidate }, - {"set_flex_flow", luavgl_obj_set_flex_flow }, - {"set_flex_align", luavgl_obj_set_flex_align }, - {"set_flex_grow", luavgl_obj_set_flex_grow }, - {"indev_search", luavgl_obj_indev_search }, - {"get_coords", luavgl_obj_get_coords }, - {"get_pos", luavgl_obj_get_pos }, - - {"onevent", luavgl_obj_on_event }, - {"onPressed", luavgl_obj_on_pressed }, - {"onClicked", luavgl_obj_on_clicked }, - {"onShortClicked", luavgl_obj_on_short_clicked }, - {"anim", luavgl_anim_create }, - {"Anim", luavgl_anim_create }, - {"remove_all_anim", luavgl_obj_remove_anim_all }, /* remove all */ - {NULL, NULL }, + {"scroll_by_raw", luavgl_obj_scroll_by_raw}, + {"scrollbar_invalidate", luavgl_obj_scrollbar_invalidate}, + {"readjust_scroll", luavgl_obj_readjust_scroll}, + {"is_editable", luavgl_obj_is_editable}, + {"is_group_def", luavgl_obj_is_group_def}, + {"is_layout_positioned", luavgl_obj_is_layout_positioned}, + {"mark_layout_as_dirty", luavgl_obj_mark_layout_as_dirty}, + {"center", luavgl_obj_center}, + {"invalidate", luavgl_obj_invalidate}, + {"set_flex_flow", luavgl_obj_set_flex_flow}, + {"set_flex_align", luavgl_obj_set_flex_align}, + {"set_flex_grow", luavgl_obj_set_flex_grow}, + {"indev_search", luavgl_obj_indev_search}, + {"get_coords", luavgl_obj_get_coords}, + {"get_pos", luavgl_obj_get_pos}, + + {"onevent", luavgl_obj_on_event}, + {"onPressed", luavgl_obj_on_pressed}, + {"onClicked", luavgl_obj_on_clicked}, + {"onShortClicked", luavgl_obj_on_short_clicked}, + {"anim", luavgl_anim_create}, + {"Anim", luavgl_anim_create}, + {"remove_all_anim", luavgl_obj_remove_anim_all}, /* remove all */ + {NULL, NULL}, }; -static void luavgl_obj_init(lua_State *L) -{ +static void luavgl_obj_init(lua_State *L) { /* base lv_obj */ luavgl_obj_newmetatable(L, &lv_obj_class, "lv_obj", luavgl_obj_methods); 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[] = { - {"x", 0, {.setter = (setter_int_t)lv_obj_set_x} }, - {"y", 0, {.setter = (setter_int_t)lv_obj_set_y} }, - {"w", 0, {.setter = (setter_int_t)lv_obj_set_width} }, - {"h", 0, {.setter = (setter_int_t)lv_obj_set_height} }, - {"align", SETTER_TYPE_STACK, {.setter_stack = _lv_obj_set_align} }, - - {"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_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} }, + {"x", 0, {.setter = (setter_int_t)lv_obj_set_x}}, + {"y", 0, {.setter = (setter_int_t)lv_obj_set_y}}, + {"w", 0, {.setter = (setter_int_t)lv_obj_set_width}}, + {"h", 0, {.setter = (setter_int_t)lv_obj_set_height}}, + {"align", SETTER_TYPE_STACK, {.setter_stack = _lv_obj_set_align}}, + + {"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_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}}, }; /** @@ -751,8 +714,7 @@ static const luavgl_value_setter_t obj_property_table[] = { * stack[-2]: key(property name) * 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; 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, - lv_obj_t *(*create)(lv_obj_t *parent)) -{ + lv_obj_t *(*create)(lv_obj_t *parent)) { luavgl_ctx_t *ctx = luavgl_context(L); 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 * 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; /* In rare case, obj may be deleted but not gc'ed in lua, and lvgl quickly diff --git a/lib/luavgl/src/widgets/bar.c b/lib/luavgl/src/widgets/bar.c new file mode 100644 index 00000000..bab38aae --- /dev/null +++ b/lib/luavgl/src/widgets/bar.c @@ -0,0 +1,87 @@ +#include "luavgl.h" +#include "private.h" +#include +#include + +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); +} diff --git a/lib/luavgl/src/widgets/widgets.c b/lib/luavgl/src/widgets/widgets.c index 19b789a7..e5f64f23 100644 --- a/lib/luavgl/src/widgets/widgets.c +++ b/lib/luavgl/src/widgets/widgets.c @@ -1,6 +1,10 @@ #include "luavgl.h" #include "private.h" +#if LV_USE_BAR +#include "bar.c" +#endif + #if LV_USE_BTN #include "btn.c" #endif @@ -50,6 +54,10 @@ static int luavgl_obj_create(lua_State *L); static const luaL_Reg widget_create_methods[] = { {"Object", luavgl_obj_create }, +#if LV_USE_BAR + {"Bar", luavgl_bar_create}, +#endif + #if LV_USE_BTN {"Button", luavgl_btn_create}, #endif @@ -142,4 +150,8 @@ static void luavgl_widgets_init(lua_State *L) luavgl_btn_init(L); #endif +#if LV_USE_BAR + luavgl_bar_init(L); +#endif + } diff --git a/lua/browser.lua b/lua/browser.lua index f07d80bc..415e5dbb 100644 --- a/lua/browser.lua +++ b/lua/browser.lua @@ -5,6 +5,7 @@ local database = require("database") local backstack = require("backstack") local font = require("font") local queue = require("queue") +local playing = require("playing") local browser = {} @@ -73,6 +74,7 @@ function browser.create(opts) play:onClicked(function() queue.clear() queue.add(original_iterator) + backstack.push(playing) end ) end @@ -107,8 +109,7 @@ function browser.create(opts) else queue.clear() queue.add(contents) - legacy_ui.open_now_playing() - -- backstack.push(playing) + backstack.push(playing) end end) btn:onevent(lvgl.EVENT.FOCUSED, function() diff --git a/lua/img/next.png b/lua/img/next.png new file mode 100644 index 00000000..1b22a509 Binary files /dev/null and b/lua/img/next.png differ diff --git a/lua/img/pause.png b/lua/img/pause.png new file mode 100644 index 00000000..29fa4b90 Binary files /dev/null and b/lua/img/pause.png differ diff --git a/lua/img/play.png b/lua/img/play.png new file mode 100644 index 00000000..cc10cab5 Binary files /dev/null and b/lua/img/play.png differ diff --git a/lua/img/prev.png b/lua/img/prev.png new file mode 100644 index 00000000..f17e6162 Binary files /dev/null and b/lua/img/prev.png differ diff --git a/lua/main_menu.lua b/lua/main_menu.lua index c0b9b1d1..c2d052a3 100644 --- a/lua/main_menu.lua +++ b/lua/main_menu.lua @@ -4,6 +4,7 @@ local legacy_ui = require("legacy_ui") local database = require("database") local backstack = require("backstack") local browser = require("browser") +local playing = require("playing") return function() local menu = {} @@ -29,7 +30,7 @@ return function() }) menu.list:add_btn(nil, "Now Playing"):onClicked(function() - legacy_ui.open_now_playing(); + backstack.push(playing) end) local indexes = database.indexes() diff --git a/lua/playing.lua b/lua/playing.lua index 89bb27f7..a183e1ab 100644 --- a/lua/playing.lua +++ b/lua/playing.lua @@ -2,6 +2,8 @@ local lvgl = require("lvgl") local widgets = require("widgets") local backstack = require("backstack") local font = require("font") +local playback = require("playback") +local queue = require("queue") return function(opts) local screen = {} @@ -23,72 +25,142 @@ return function(opts) transparent_bg = true, }) - local track_info = screen.root:Object { + local info = screen.root:Object { flex = { flex_direction = "column", + flex_wrap = "wrap", justify_content = "center", align_items = "center", align_content = "center", }, - w = lvgl.SIZE_CONTENT, + w = lvgl.HOR_RES(), + h = lvgl.SIZE_CONTENT, flex_grow = 1, } - local artist = track_info:Label { - text = "Cool Artist", - text_font = font.fusion_10, - } - - local artist = track_info:Label { - text = "Good Album", + local artist = info:Label { + w = lvgl.SIZE_CONTENT, + h = lvgl.SIZE_CONTENT, + text = "", text_font = font.fusion_10, } - local title = track_info:Label { - text = "A really good song", + local title = info:Label { + w = lvgl.SIZE_CONTENT, + h = lvgl.SIZE_CONTENT, + text = "", } - local scrubber = screen.root:Object {} - - local times = screen.root:Object { + local playlist = screen.root:Object { flex = { flex_direction = "row", justify_content = "center", - align_items = "space-between", + align_items = "center", align_content = "center", }, - w = lvgl.PCT(100), + w = 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 { - text = "4:20", + playlist:Label { + 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 { flex = { flex_direction = "row", justify_content = "center", - align_items = "space-evenly", + align_items = "center", align_content = "center", }, w = lvgl.PCT(100), 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:Label { - text = ">", + + controls:Object({ flex_grow = 1, h = 1 }) -- spacer + + controls:Image { + src = "//lua/img/prev.png", } - controls:Label { - text = ">", + local play_pause_img = controls:Image { + src = "//lua/img/pause.png", } - controls:Label { - text = ">", + controls:Image { + src = "//lua/img/next.png", } - controls:Label { - text = ">", + 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, + } + + 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) + } + scrubber:set { value = pos } + end), + playback.track:bind(function(track) + if not track then return end + end_time:set { + text = format_time(track.duration) + } + title:set { text = track.title } + artist:set { text = track.artist } + scrubber:set { + range = { min = 0, max = track.duration } + } + end), + 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 diff --git a/lua/widgets.lua b/lua/widgets.lua index b601326b..bd0b2405 100644 --- a/lua/widgets.lua +++ b/lua/widgets.lua @@ -1,7 +1,6 @@ local lvgl = require("lvgl") local power = require("power") local bluetooth = require("bluetooth") -local playback = require("playback") local font = require("font") local widgets = {} @@ -53,7 +52,6 @@ function widgets.StatusBar(parent, opts) status_bar.title:set { text = opts.title } end - status_bar.playing = status_bar.root:Image {} status_bar.bluetooth = status_bar.root:Image {} status_bar.battery = status_bar.root:Image {} status_bar.chg = status_bar.battery:Image { @@ -64,7 +62,7 @@ function widgets.StatusBar(parent, opts) local is_charging = nil local percent = nil - function update_battery_icon() + local function update_battery_icon() if is_charging == nil or percent == nil then return end local src if percent >= 95 then @@ -101,20 +99,6 @@ function widgets.StatusBar(parent, opts) is_charging = p update_battery_icon() 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) if en then status_bar.bluetooth:clear_flag(lvgl.FLAG.HIDDEN) diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 0cf8eacd..95bab4c2 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -6,6 +6,7 @@ idf_component_register( SRCS "audio_decoder.cpp" "fatfs_audio_input.cpp" "i2s_audio_output.cpp" "track_queue.cpp" "audio_fsm.cpp" "audio_converter.cpp" "resample.cpp" "fatfs_source.cpp" "bt_audio_output.cpp" "readahead_source.cpp" + "audio_source.cpp" INCLUDE_DIRS "include" REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist" "speexdsp") diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index a58268b0..fd011c51 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -50,12 +50,16 @@ namespace audio { static constexpr std::size_t kCodecBufferLength = drivers::kI2SBufferLengthFrames * sizeof(sample::Sample) * 2; -Timer::Timer(const codecs::ICodec::OutputFormat& format) - : current_seconds_(0), +Timer::Timer(std::shared_ptr t, + const codecs::ICodec::OutputFormat& format) + : track_(t), + current_seconds_(0), current_sample_in_second_(0), samples_per_second_(format.sample_rate_hz * format.num_channels), 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 { bool incremented = false; @@ -69,10 +73,10 @@ auto Timer::AddSamples(std::size_t samples) -> void { if (incremented) { if (total_duration_seconds_ < current_seconds_) { total_duration_seconds_ = current_seconds_; + track_->duration = total_duration_seconds_; } - PlaybackUpdate ev{.seconds_elapsed = current_seconds_, - .seconds_total = total_duration_seconds_}; + PlaybackUpdate ev{.seconds_elapsed = current_seconds_, .track = track_}; events::Audio().Dispatch(ev); events::Ui().Dispatch(ev); } @@ -102,7 +106,7 @@ Decoder::Decoder(std::shared_ptr source, void Decoder::Main() { for (;;) { if (source_->HasNewStream() || !stream_) { - std::shared_ptr new_stream = source_->NextStream(); + std::shared_ptr new_stream = source_->NextStream(); ESP_LOGI(kTag, "decoder has new stream"); if (new_stream && BeginDecoding(new_stream)) { stream_ = new_stream; @@ -118,7 +122,7 @@ void Decoder::Main() { } } -auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { +auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { // Ensure any previous codec is freed before creating a new one. codec_.reset(); codec_.reset(codecs::CreateCodecForType(stream->type()).value_or(nullptr)); @@ -136,7 +140,13 @@ auto Decoder::BeginDecoding(std::shared_ptr stream) -> bool { stream->SetPreambleFinished(); if (open_res->total_samples) { - timer_.reset(new Timer(open_res.value())); + timer_.reset(new Timer(std::shared_ptr{new Track{ + .tags = stream->tags(), + .db_info = {}, + .bitrate_kbps = 0, + .encoding = stream->type(), + }}, + open_res.value())); } else { timer_.reset(); } diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index f43d0ce2..e33a2cab 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -211,7 +211,7 @@ void Playback::react(const TogglePlayPause& ev) { void Playback::react(const PlaybackUpdate& ev) { ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, - ev.seconds_total); + ev.track->duration); } void Playback::react(const internal::InputFileOpened& ev) {} diff --git a/src/audio/audio_source.cpp b/src/audio/audio_source.cpp new file mode 100644 index 00000000..b9262b45 --- /dev/null +++ b/src/audio/audio_source.cpp @@ -0,0 +1,41 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "audio_source.hpp" +#include "codec.hpp" +#include "types.hpp" + +namespace audio { + +TaggedStream::TaggedStream(std::shared_ptr t, + std::unique_ptr w) + : codecs::IStream(w->type()), tags_(t), wrapped_(std::move(w)) {} + +auto TaggedStream::tags() -> std::shared_ptr { + return tags_; +} + +auto TaggedStream::Read(cpp::span 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 diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index 6580f301..5594718f 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -85,7 +85,7 @@ auto FatfsAudioInput::HasNewStream() -> bool { return has_new_stream_; } -auto FatfsAudioInput::NextStream() -> std::shared_ptr { +auto FatfsAudioInput::NextStream() -> std::shared_ptr { while (true) { has_new_stream_.wait(false); @@ -147,8 +147,7 @@ auto FatfsAudioInput::OpenFile(const std::pmr::string& path) -> bool { auto source = std::make_unique(stream_type.value(), std::move(file)); - // new_stream_.reset(new ReadaheadSource(bg_worker_, std::move(source))); - new_stream_ = std::move(source); + new_stream_.reset(new TaggedStream(tags, std::move(source))); return true; } diff --git a/src/audio/include/audio_decoder.hpp b/src/audio/include/audio_decoder.hpp index 1759f6e4..318e6fd4 100644 --- a/src/audio/include/audio_decoder.hpp +++ b/src/audio/include/audio_decoder.hpp @@ -10,6 +10,7 @@ #include #include "audio_converter.hpp" +#include "audio_events.hpp" #include "audio_sink.hpp" #include "audio_source.hpp" #include "codec.hpp" @@ -23,11 +24,13 @@ namespace audio { */ class Timer { public: - Timer(const codecs::ICodec::OutputFormat& format); + Timer(std::shared_ptr, const codecs::ICodec::OutputFormat& format); auto AddSamples(std::size_t) -> void; private: + std::shared_ptr track_; + uint32_t current_seconds_; uint32_t current_sample_in_second_; uint32_t samples_per_second_; @@ -54,7 +57,7 @@ class Decoder { Decoder(std::shared_ptr source, std::shared_ptr converter); - auto BeginDecoding(std::shared_ptr) -> bool; + auto BeginDecoding(std::shared_ptr) -> bool; auto ContinueDecoding() -> bool; std::shared_ptr source_; diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index b130938c..9994a9f6 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -8,20 +8,31 @@ #include #include +#include #include #include "tinyfsm.hpp" #include "track.hpp" #include "track_queue.hpp" +#include "types.hpp" namespace audio { +struct Track { + std::shared_ptr tags; + std::shared_ptr db_info; + + uint32_t duration; + uint32_t bitrate_kbps; + codecs::StreamType encoding; +}; + struct PlaybackStarted : tinyfsm::Event {}; struct PlaybackUpdate : tinyfsm::Event { uint32_t seconds_elapsed; - uint32_t seconds_total; + std::shared_ptr track; }; struct PlaybackFinished : tinyfsm::Event {}; diff --git a/src/audio/include/audio_source.hpp b/src/audio/include/audio_source.hpp index a0d690a6..a54cb260 100644 --- a/src/audio/include/audio_source.hpp +++ b/src/audio/include/audio_source.hpp @@ -6,16 +6,41 @@ #pragma once +#include #include "codec.hpp" +#include "track.hpp" +#include "types.hpp" namespace audio { +class TaggedStream : public codecs::IStream { + public: + TaggedStream(std::shared_ptr, + std::unique_ptr wrapped); + + auto tags() -> std::shared_ptr; + + auto Read(cpp::span 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 tags_; + std::unique_ptr wrapped_; +}; + class IAudioSource { public: virtual ~IAudioSource() {} virtual auto HasNewStream() -> bool = 0; - virtual auto NextStream() -> std::shared_ptr = 0; + virtual auto NextStream() -> std::shared_ptr = 0; }; } // namespace audio diff --git a/src/audio/include/fatfs_audio_input.hpp b/src/audio/include/fatfs_audio_input.hpp index 9b516478..c7d52ca3 100644 --- a/src/audio/include/fatfs_audio_input.hpp +++ b/src/audio/include/fatfs_audio_input.hpp @@ -43,7 +43,7 @@ class FatfsAudioInput : public IAudioSource { auto SetPath() -> void; auto HasNewStream() -> bool override; - auto NextStream() -> std::shared_ptr override; + auto NextStream() -> std::shared_ptr override; FatfsAudioInput(const FatfsAudioInput&) = delete; FatfsAudioInput& operator=(const FatfsAudioInput&) = delete; @@ -58,7 +58,7 @@ class FatfsAudioInput : public IAudioSource { tasks::Worker& bg_worker_; std::mutex new_stream_mutex_; - std::shared_ptr new_stream_; + std::shared_ptr new_stream_; std::atomic has_new_stream_; diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp index 49c0d61b..0be2384a 100644 --- a/src/audio/include/track_queue.hpp +++ b/src/audio/include/track_queue.hpp @@ -72,6 +72,9 @@ class TrackQueue { */ auto Clear() -> void; + auto Position() -> size_t; + auto Size() -> size_t; + TrackQueue(const TrackQueue&) = delete; TrackQueue& operator=(const TrackQueue&) = delete; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index 86f6e034..c400e66a 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -8,10 +8,12 @@ #include #include +#include #include #include "audio_events.hpp" #include "audio_fsm.hpp" +#include "database.hpp" #include "event_queue.hpp" #include "source.hpp" #include "track.hpp" @@ -217,4 +219,12 @@ auto TrackQueue::Clear() -> void { 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 diff --git a/src/codecs/codec.cpp b/src/codecs/codec.cpp index a4c1a5cf..3610dea8 100644 --- a/src/codecs/codec.cpp +++ b/src/codecs/codec.cpp @@ -17,6 +17,23 @@ 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 { switch (type) { case StreamType::kMp3: diff --git a/src/codecs/include/types.hpp b/src/codecs/include/types.hpp index e0bba47d..c9eefe45 100644 --- a/src/codecs/include/types.hpp +++ b/src/codecs/include/types.hpp @@ -18,4 +18,6 @@ enum class StreamType { kOpus, }; +auto StreamTypeToString(StreamType t) -> std::string; + } // namespace codecs diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp index 72296e8d..8a24024f 100644 --- a/src/database/include/track.hpp +++ b/src/database/include/track.hpp @@ -56,6 +56,8 @@ enum class Tag { kDuration = 5, }; +auto TagToString(Tag t) -> std::string; + /* * Owning container for tag-related track metadata that was extracted from a * file. diff --git a/src/database/track.cpp b/src/database/track.cpp index d30264cd..871e3087 100644 --- a/src/database/track.cpp +++ b/src/database/track.cpp @@ -13,6 +13,25 @@ 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 { tags_[key] = val; } @@ -64,5 +83,4 @@ auto Track::TitleOrFilename() const -> std::pmr::string { } return data().filepath.substr(start); } - } // namespace database diff --git a/src/lua/include/property.hpp b/src/lua/include/property.hpp index 60f9906a..207696bd 100644 --- a/src/lua/include/property.hpp +++ b/src/lua/include/property.hpp @@ -9,13 +9,15 @@ #include #include +#include "audio_events.hpp" #include "lua.hpp" #include "lvgl.h" #include "service_locator.hpp" namespace lua { -using LuaValue = std::variant; +using LuaValue = + std::variant; using LuaFunction = std::function; class Property { diff --git a/src/lua/property.cpp b/src/lua/property.cpp index c63d243f..3e492237 100644 --- a/src/lua/property.cpp +++ b/src/lua/property.cpp @@ -9,10 +9,13 @@ #include #include +#include "lua.h" #include "lua.hpp" #include "lua_thread.hpp" #include "lvgl.h" #include "service_locator.hpp" +#include "track.hpp" +#include "types.hpp" namespace lua { @@ -51,7 +54,7 @@ static auto property_bind(lua_State* state) -> int { lua_pushvalue(state, 2); p->PushValue(*state); - CallProtected(state, 1, 0); // Invoke the initial binding. + CallProtected(state, 1, 0); // Invoke the initial binding. lua_pushstring(state, kBindingsTable); lua_gettable(state, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] @@ -171,6 +174,31 @@ auto Property::PushValue(lua_State& s) -> int { lua_pushboolean(&s, arg); } else if constexpr (std::is_same_v) { lua_pushstring(&s, arg.c_str()); + } else if constexpr (std::is_same_v) { + 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 { static_assert(always_false_v, "PushValue missing type"); } @@ -228,8 +256,8 @@ auto Property::Update(const LuaValue& v) -> void { continue; } - PushValue(*b.first); // push the argument - CallProtected(b.first, 1, 0); // invoke the closure + PushValue(*b.first); // push the argument + CallProtected(b.first, 1, 0); // invoke the closure } } diff --git a/src/lua/stubs/playback.lua b/src/lua/stubs/playback.lua index d32febae..340da37d 100644 --- a/src/lua/stubs/playback.lua +++ b/src/lua/stubs/playback.lua @@ -8,4 +8,8 @@ local playback = {} -- @treturn types.Property a boolean property function playback.playing() end +function playback:track() end + +function playback:position() end + return playback diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index d37f3fdf..a869053d 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -3,15 +3,11 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" - "encoder_input.cpp" "screen_track_browser.cpp" "screen_playing.cpp" - "themes.cpp" "widget_top_bar.cpp" "screen.cpp" "screen_onboarding.cpp" - "modal_progress.cpp" "modal.cpp" "modal_confirm.cpp" "screen_settings.cpp" - "event_binding.cpp" "modal_add_to_queue.cpp" "screen_lua.cpp" + SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "encoder_input.cpp" + "themes.cpp" "widget_top_bar.cpp" "screen.cpp" "modal_progress.cpp" + "modal.cpp" "modal_confirm.cpp" "screen_settings.cpp" "event_binding.cpp" + "screen_lua.cpp" "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" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "bindey" "lua" "luavgl") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/ui/include/modal_add_to_queue.hpp b/src/ui/include/modal_add_to_queue.hpp deleted file mode 100644 index e6417cd4..00000000 --- a/src/ui/include/modal_add_to_queue.hpp +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include - -#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, - bool all_tracks_only = false); - - private: - audio::TrackQueue& queue_; - std::shared_ptr item_; - lv_obj_t* container_; - - lv_obj_t* selected_track_btn_; - lv_obj_t* all_tracks_btn_; - bool all_tracks_; -}; - -} // namespace modals -} // namespace ui diff --git a/src/ui/include/screen_onboarding.hpp b/src/ui/include/screen_onboarding.hpp deleted file mode 100644 index 0c3c61fb..00000000 --- a/src/ui/include/screen_onboarding.hpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include - -#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 diff --git a/src/ui/include/screen_playing.hpp b/src/ui/include/screen_playing.hpp deleted file mode 100644 index 185c55cc..00000000 --- a/src/ui/include/screen_playing.hpp +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include -#include -#include - -#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 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 db_; - audio::TrackQueue& queue_; - - bindey::property> current_track_; - bindey::property>> next_tracks_; - - std::unique_ptr>> - new_track_; - std::unique_ptr< - database::FutureFetcher>>> - 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 diff --git a/src/ui/include/screen_track_browser.hpp b/src/ui/include/screen_track_browser.hpp deleted file mode 100644 index 0b2d6fc3..00000000 --- a/src/ui/include/screen_track_browser.hpp +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#pragma once - -#include -#include -#include -#include - -#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 db, - const std::pmr::vector& breadcrumbs, - std::future*>&& 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>) - -> void; - auto DropPage(Position pos) -> void; - auto FetchNewPage(Position pos) -> void; - - auto GetNumRecords() -> std::size_t; - auto GetItemIndex(lv_obj_t* obj) -> std::optional; - - audio::TrackQueue& queue_; - std::weak_ptr 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 breadcrumbs_; - - std::optional loading_pos_; - std::optional*>> - loading_page_; - - std::shared_ptr> initial_page_; - std::deque>> - current_pages_; -}; - -} // namespace screens -} // namespace ui diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 9e81259a..9f530d71 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -6,8 +6,7 @@ #pragma once -#include -#include +#include #include #include @@ -23,7 +22,6 @@ #include "nvs.hpp" #include "property.hpp" #include "relative_wheel.hpp" -#include "screen_playing.hpp" #include "screen_settings.hpp" #include "service_locator.hpp" #include "tinyfsm.hpp" @@ -60,16 +58,14 @@ class UiState : public tinyfsm::Fsm { virtual void react(const system_fsm::BatteryStateChanged&); virtual void react(const audio::PlaybackStarted&); virtual void react(const audio::PlaybackFinished&); - void react(const audio::PlaybackUpdate&); - void react(const audio::QueueUpdate&); + virtual void react(const audio::PlaybackUpdate&); + virtual void react(const audio::QueueUpdate&); virtual void react(const system_fsm::KeyLockChanged&); virtual void react(const OnLuaError&) {} virtual void react(const internal::RecordSelected&) {} - virtual void react(const internal::IndexSelected&) {} virtual void react(const internal::BackPressed&) {} - virtual void react(const internal::ShowNowPlaying&){}; virtual void react(const internal::ShowSettingsPage&){}; virtual void react(const internal::ModalCancelPressed&) { sCurrentModal.reset(); @@ -127,12 +123,12 @@ class Lua : public UiState { 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 system_fsm::BatteryStateChanged&) override; + void react(const audio::QueueUpdate&) override; void react(const audio::PlaybackStarted&) override; + void react(const audio::PlaybackUpdate&) override; void react(const audio::PlaybackFinished&) override; using UiState::react; @@ -144,32 +140,23 @@ class Lua : public UiState { std::shared_ptr battery_pct_; std::shared_ptr battery_mv_; std::shared_ptr battery_charging_; + std::shared_ptr bluetooth_en_; + std::shared_ptr playback_playing_; std::shared_ptr playback_track_; -}; - -class Onboarding : public UiState { - public: - void entry() override; - - void react(const internal::OnboardingNavigate&) override; + std::shared_ptr playback_position_; - using UiState::react; - - private: - uint8_t progress_; - bool has_formatted_; + std::shared_ptr queue_position_; + std::shared_ptr queue_size_; }; class Browse : public UiState { public: void entry() override; - void react(const internal::RecordSelected&) override; void react(const internal::BackPressed&) override; - void react(const internal::ShowNowPlaying&) override; void react(const internal::ShowSettingsPage&) override; void react(const internal::ReindexDatabase&) override; @@ -178,16 +165,6 @@ class Browse : public UiState { 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 { public: void entry() override; diff --git a/src/ui/modal_add_to_queue.cpp b/src/ui/modal_add_to_queue.cpp deleted file mode 100644 index e102fae8..00000000 --- a/src/ui/modal_add_to_queue.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * 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 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 diff --git a/src/ui/screen_lua.cpp b/src/ui/screen_lua.cpp index ae49ffd5..5130b4f7 100644 --- a/src/ui/screen_lua.cpp +++ b/src/ui/screen_lua.cpp @@ -6,9 +6,9 @@ #include "screen_lua.hpp" -#include "lauxlib.h" -#include "lua.h" +#include "core/lv_obj_tree.h" #include "lua.hpp" + #include "luavgl.h" namespace ui { diff --git a/src/ui/screen_onboarding.cpp b/src/ui/screen_onboarding.cpp deleted file mode 100644 index f5ce004f..00000000 --- a/src/ui/screen_onboarding.cpp +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * 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 diff --git a/src/ui/screen_playing.cpp b/src/ui/screen_playing.cpp deleted file mode 100644 index c29d342e..00000000 --- a/src/ui/screen_playing.cpp +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include "screen_playing.hpp" -#include -#include - -#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(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(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 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& 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>( - db->GetTrack(*id))); - })); - - data_bindings_.emplace_back(current_track_.onChangedAndNow( - [=](const std::shared_ptr& 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(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& 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>>( - db->GetBulkTracks(ids))); - })); - - data_bindings_.emplace_back(next_tracks_.onChangedAndNow( - [=](const std::vector>& 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> 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 diff --git a/src/ui/screen_track_browser.cpp b/src/ui/screen_track_browser.cpp deleted file mode 100644 index c7b035ad..00000000 --- a/src/ui/screen_track_browser.cpp +++ /dev/null @@ -1,431 +0,0 @@ -/* - * Copyright 2023 jacqueline - * - * SPDX-License-Identifier: GPL-3.0-only - */ - -#include -#include - -#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(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(ev->user_data); - instance->OnItemSelected(ev); -} - -TrackBrowser::TrackBrowser( - models::TopBar& top_bar_model, - audio::TrackQueue& queue, - std::weak_ptr db, - const std::pmr::vector& crumbs, - std::future*>&& 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> 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> 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& 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 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(&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 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 diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 5b4ea2a7..557dc1a3 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -30,7 +30,6 @@ #include "event_queue.hpp" #include "gpios.hpp" #include "lvgl_task.hpp" -#include "modal_add_to_queue.hpp" #include "modal_confirm.hpp" #include "modal_progress.hpp" #include "model_playback.hpp" @@ -39,11 +38,8 @@ #include "relative_wheel.hpp" #include "screen.hpp" #include "screen_lua.hpp" -#include "screen_onboarding.hpp" -#include "screen_playing.hpp" #include "screen_settings.hpp" #include "screen_splash.hpp" -#include "screen_track_browser.hpp" #include "source.hpp" #include "spiffs.hpp" #include "storage.hpp" @@ -51,15 +47,12 @@ #include "touchwheel.hpp" #include "track_queue.hpp" #include "ui_events.hpp" -#include "widget_top_bar.hpp" #include "widgets/lv_label.h" namespace ui { [[maybe_unused]] static constexpr char kTag[] = "ui_fsm"; -static const std::size_t kRecordsPerPage = 15; - std::unique_ptr UiState::sTask; std::shared_ptr UiState::sServices; std::unique_ptr UiState::sDisplay; @@ -116,27 +109,17 @@ void UiState::react(const system_fsm::BatteryStateChanged& ev) { sTopBarModel.battery_state.set(ev.new_state); } -void UiState::react(const audio::PlaybackStarted&) { - sPlaybackModel.is_playing.set(true); -} +void UiState::react(const audio::PlaybackStarted&) {} -void UiState::react(const audio::PlaybackFinished&) { - sPlaybackModel.is_playing.set(false); -} +void UiState::react(const audio::PlaybackFinished&) {} -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::PlaybackUpdate& ev) {} void UiState::react(const audio::QueueUpdate&) { auto& queue = sServices->track_queue(); bool had_queue = sPlaybackModel.current_track.get().has_value(); sPlaybackModel.current_track.set(queue.GetCurrent()); sPlaybackModel.upcoming_tracks.set(queue.GetUpcoming(10)); - if (!had_queue) { - transit(); - } } void UiState::react(const internal::ControlSchemeChanged&) { @@ -192,8 +175,13 @@ void Lua::entry() { battery_charging_ = std::make_shared(bat.is_charging); bluetooth_en_ = std::make_shared(false); + + queue_position_ = std::make_shared(0); + queue_size_ = std::make_shared(0); + playback_playing_ = std::make_shared(false); playback_track_ = std::make_shared(); + playback_position_ = std::make_shared(); sLua.reset(lua::LuaThread::Start(*sServices, sCurrentScreen->content())); sLua->bridge().AddPropertyModule("power", @@ -211,7 +199,12 @@ void Lua::entry() { { {"playing", playback_playing_}, {"track", playback_track_}, + {"position", playback_position_}, }); + sLua->bridge().AddPropertyModule("queue", { + {"position", queue_position_}, + {"size", queue_size_}, + }); sLua->bridge().AddPropertyModule( "backstack", { @@ -233,7 +226,7 @@ auto Lua::PushLuaScreen(lua_State* s) -> int { auto new_screen = std::make_shared(); // 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()); // Call the constructor for this screen. @@ -252,7 +245,7 @@ auto Lua::PushLuaScreen(lua_State* s) -> int { auto Lua::PopLuaScreen(lua_State* s) -> int { PopScreen(); - luavgl_set_root(s, sCurrentScreen->root()); + luavgl_set_root(s, sCurrentScreen->content()); lv_group_set_default(sCurrentScreen->group()); return 0; } @@ -265,25 +258,6 @@ void Lua::react(const OnLuaError& err) { 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 crumbs = {""}; - PushScreen(std::make_shared( - sTopBarModel, sServices->track_queue(), sServices->database(), crumbs, - std::move(query))); - transit(); -} - -void Lua::react(const internal::ShowNowPlaying&) { - transit(); -} - void Lua::react(const internal::ShowSettingsPage& ev) { PushScreen(std::shared_ptr(new screens::Settings(sTopBarModel))); transit(); @@ -294,71 +268,27 @@ void Lua::react(const system_fsm::BatteryStateChanged& ev) { battery_mv_->Update(static_cast(ev.new_state.millivolts)); } -void Lua::react(const audio::PlaybackStarted&) { - playback_playing_->Update(true); +void Lua::react(const audio::QueueUpdate&) { + auto& queue = sServices->track_queue(); + queue_size_->Update(static_cast(queue.Size())); + queue_position_->Update(static_cast(queue.Position())); } -void Lua::react(const audio::PlaybackFinished&) { - playback_playing_->Update(false); +void Lua::react(const audio::PlaybackStarted& ev) { + playback_playing_->Update(true); } -void Onboarding::entry() { - progress_ = 0; - has_formatted_ = false; - sCurrentScreen.reset(new screens::onboarding::LinkToManual()); +void Lua::react(const audio::PlaybackUpdate& ev) { + playback_track_->Update(*ev.track); + playback_position_->Update(static_cast(ev.seconds_elapsed)); } -void Onboarding::react(const internal::OnboardingNavigate& ev) { - int dir = ev.forwards ? 1 : -1; - 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(); - return; - } - } +void Lua::react(const audio::PlaybackFinished&) { + playback_playing_->Update(false); } void Browse::entry() {} -void Browse::react(const internal::ShowNowPlaying& ev) { - transit(); -} - void Browse::react(const internal::ShowSettingsPage& ev) { std::shared_ptr screen; std::shared_ptr 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( - 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(); - } - } else { - ESP_LOGI(kTag, "selected record '%s'", record->text()->c_str()); - auto cont = record->Expand(kRecordsPerPage); - if (!cont) { - return; - } - auto query = db->GetPage(&cont.value()); - if (ev.show_menu) { - std::shared_ptr> 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( - sTopBarModel, sServices->track_queue(), sServices->database(), - ev.new_crumbs, std::move(query))); - } - } -} - void Browse::react(const internal::BackPressed& ev) { if (PopScreen() == 0) { transit(); @@ -455,28 +344,6 @@ void Browse::react(const internal::ReindexDatabase& ev) { transit(); } -static std::shared_ptr 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(); - } else { - transit(); - } -} - static std::shared_ptr sIndexProgress; void Indexing::entry() { diff --git a/src/ui/widget_top_bar.cpp b/src/ui/widget_top_bar.cpp index 348ffb6b..fbad5548 100644 --- a/src/ui/widget_top_bar.cpp +++ b/src/ui/widget_top_bar.cpp @@ -19,16 +19,6 @@ #include "widgets/lv_img.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 widgets { @@ -64,46 +54,6 @@ TopBar::TopBar(lv_obj_t* parent, lv_label_set_text(title_, config.title.c_str()); 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& 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); }