diff --git a/lib/lvgl/lv_conf.h b/lib/lvgl/lv_conf.h index 06f19f40..07bfc9ad 100644 --- a/lib/lvgl/lv_conf.h +++ b/lib/lvgl/lv_conf.h @@ -597,9 +597,9 @@ /*File system interfaces for common APIs */ /*API for fopen, fread, etc*/ -#define LV_USE_FS_STDIO 0 +#define LV_USE_FS_STDIO 1 #if LV_USE_FS_STDIO - #define LV_FS_STDIO_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ + #define LV_FS_STDIO_LETTER '/' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/ #define LV_FS_STDIO_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/ #define LV_FS_STDIO_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/ #endif @@ -628,7 +628,7 @@ #endif /*PNG decoder library*/ -#define LV_USE_PNG 0 +#define LV_USE_PNG 1 /*BMP decoder library*/ #define LV_USE_BMP 0 diff --git a/tools/icons/raw/audio.png b/lua/assets/audio.png similarity index 100% rename from tools/icons/raw/audio.png rename to lua/assets/audio.png diff --git a/tools/icons/raw/battery_20.png b/lua/assets/battery_20.png similarity index 100% rename from tools/icons/raw/battery_20.png rename to lua/assets/battery_20.png diff --git a/tools/icons/raw/battery_40.png b/lua/assets/battery_40.png similarity index 100% rename from tools/icons/raw/battery_40.png rename to lua/assets/battery_40.png diff --git a/tools/icons/raw/battery_60.png b/lua/assets/battery_60.png similarity index 100% rename from tools/icons/raw/battery_60.png rename to lua/assets/battery_60.png diff --git a/tools/icons/raw/battery_80.png b/lua/assets/battery_80.png similarity index 100% rename from tools/icons/raw/battery_80.png rename to lua/assets/battery_80.png diff --git a/tools/icons/raw/battery_empty.png b/lua/assets/battery_empty.png similarity index 100% rename from tools/icons/raw/battery_empty.png rename to lua/assets/battery_empty.png diff --git a/lua/assets/battery_full.bin b/lua/assets/battery_full.bin new file mode 100644 index 00000000..b01ca61f Binary files /dev/null and b/lua/assets/battery_full.bin differ diff --git a/tools/icons/raw/battery_full.png b/lua/assets/battery_full.png similarity index 100% rename from tools/icons/raw/battery_full.png rename to lua/assets/battery_full.png diff --git a/lua/assets/bt.png b/lua/assets/bt.png new file mode 100644 index 00000000..180e6b3a Binary files /dev/null and b/lua/assets/bt.png differ diff --git a/tools/icons/raw/bluetooth.png b/lua/assets/bt_conn.png similarity index 100% rename from tools/icons/raw/bluetooth.png rename to lua/assets/bt_conn.png diff --git a/tools/icons/raw/pause.png b/lua/assets/pause.png similarity index 100% rename from tools/icons/raw/pause.png rename to lua/assets/pause.png diff --git a/tools/icons/raw/play.png b/lua/assets/play.png similarity index 100% rename from tools/icons/raw/play.png rename to lua/assets/play.png diff --git a/lua/backstack.lua b/lua/backstack.lua new file mode 100644 index 00000000..c54fbac4 --- /dev/null +++ b/lua/backstack.lua @@ -0,0 +1,37 @@ +local lvgl = require("lvgl") + +local backstack = { + root = lvgl.Object(nil, { + w = lvgl.HOR_RES(), + h = lvgl.VER_RES(), + }), + stack = {}, +} + +function backstack:Top() + return self.stack[#self.stack] +end + +function backstack:SetTopParent(parent) + local top = self:Top() + if top and top.root then + top.root:set_parent(parent) + end +end + +function backstack:Push(screen) + self:SetTopParent(nil) + table.insert(self.stack, screen) + self:SetTopParent(self.root) +end + +function backstack:Pop(num) + num = num or 1 + for _ = 1, num do + local removed = table.remove(self.stack) + removed.root:delete() + end + self:SetTopParent(self.root) +end + +return backstack diff --git a/lua/main.lua b/lua/main.lua index 2a80a571..ce9596af 100644 --- a/lua/main.lua +++ b/lua/main.lua @@ -1 +1,3 @@ -require("main_menu"):Create() +local backstack = require("backstack") +local main_menu = require("main_menu"):Create(backstack.root) +backstack:Push(main_menu) diff --git a/lua/main_menu.lua b/lua/main_menu.lua index 924b51cf..f0be33de 100644 --- a/lua/main_menu.lua +++ b/lua/main_menu.lua @@ -5,8 +5,9 @@ local database = require("database") local main_menu = {} -function main_menu:Create() - local root = lvgl.Object(nil, { +function main_menu:Create(parent) + local menu = {} + menu.root = lvgl.Object(parent, { flex = { flex_direction = "column", flex_wrap = "wrap", @@ -17,31 +18,33 @@ function main_menu:Create() w = lvgl.HOR_RES(), h = lvgl.VER_RES(), }) - root:center() + menu.root:center() - widgets.StatusBar(root, {}) + menu.status_bar = widgets.StatusBar(menu.root, {}) - local list = lvgl.List(root, { + menu.list = lvgl.List(menu.root, { w = lvgl.PCT(100), h = lvgl.PCT(100), flex_grow = 1, }) - list:add_btn(nil, "Now Playing"):onClicked(function() + menu.list:add_btn(nil, "Now Playing"):onClicked(function() legacy_ui.open_now_playing(); end) local indexes = database.get_indexes() for id, name in ipairs(indexes) do - local btn = list:add_btn(nil, name) + local btn = menu.list:add_btn(nil, name) btn:onClicked(function() legacy_ui.open_browse(id); end) end - list:add_btn(nil, "Settings"):onClicked(function() + menu.list:add_btn(nil, "Settings"):onClicked(function() legacy_ui.open_settings(); end) + + return menu end return main_menu diff --git a/lua/widgets.lua b/lua/widgets.lua index bcc3ca59..a281620e 100644 --- a/lua/widgets.lua +++ b/lua/widgets.lua @@ -1,37 +1,96 @@ local lvgl = require("lvgl") +local power = require("power") +local bluetooth = require("bluetooth") +local playback = require("playback") local widgets = {} -function widgets.StatusBar(parent) - local container = parent:Object { +function widgets.StatusBar(parent, opts) + local status_bar = {} + + status_bar.root = parent:Object { flex = { - flex_direction = "row", - justify_content = "flex-start", - align_items = "center", - align_content = "center", + flex_direction = "row", + justify_content = "flex-start", + align_items = "center", + align_content = "center", }, w = lvgl.HOR_RES(), - h = 16, + h = 18, } - container:Label { - w = lvgl.SIZE_CONTENT, - h = 12, - text = "<", - } + if opts.back_cb then + status_bar.back = status_bar.root:Label { + w = lvgl.SIZE_CONTENT, + h = 12, + text = "<", + } + status_bar.back:onClicked(opts.back_cb) + end - container:Label { + status_bar.title = status_bar.root:Label { w = lvgl.PCT(100), h = 16, - text = "cool title", + text = "", flex_grow = 1, } + if opts.title then + status_bar.title.set { text = opts.title } + end - container:Label { - w = lvgl.SIZE_CONTENT, - h = 16, - text = "69%", + status_bar.playing = status_bar.root:Image {} + status_bar.bluetooth = status_bar.root:Image {} + status_bar.battery = status_bar.root:Image {} + + status_bar.bindings = { + power.battery_pct:bind(function(percent) + local src + if percent >= 95 then + src = "battery_full.png" + elseif percent >= 75 then + src = "battery_80.png" + elseif percent >= 55 then + src = "battery_60.png" + elseif percent >= 35 then + src = "battery_40.png" + elseif percent >= 15 then + src = "battery_20.png" + else + src = "battery_empty.png" + end + status_bar.battery:set_src("//lua/assets/" .. src) + 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) + else + status_bar.bluetooth:add_flag(lvgl.FLAG.HIDDEN) + end + end), + bluetooth.connected:bind(function(connected) + if connected then + status_bar.bluetooth:set_src("//lua/assets/bt_conn.png") + else + status_bar.bluetooth:set_src("//lua/assets/bt.png") + end + end), } + + return status_bar end return widgets diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index a2dd8739..f179a881 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "lua_thread.cpp" "bridge.cpp" + SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index acc64c31..ba6f50b4 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -10,10 +10,12 @@ #include #include "esp_log.h" -#include "event_queue.hpp" -#include "lua.h" +#include "lauxlib.h" #include "lua.hpp" #include "lvgl.h" + +#include "event_queue.hpp" +#include "property.hpp" #include "service_locator.hpp" #include "ui_events.hpp" @@ -53,9 +55,7 @@ static auto lua_legacy_ui(lua_State* state) -> int { } static auto get_indexes(lua_State* state) -> int { - lua_pushstring(state, kBridgeKey); - lua_gettable(state, LUA_REGISTRYINDEX); - Bridge* instance = reinterpret_cast(lua_touserdata(state, -1)); + Bridge* instance = Bridge::Get(state); lua_newtable(state); @@ -80,8 +80,14 @@ static auto lua_database(lua_State* state) -> int { return 1; } +auto Bridge::Get(lua_State* state) -> Bridge* { + lua_pushstring(state, kBridgeKey); + lua_gettable(state, LUA_REGISTRYINDEX); + return reinterpret_cast(lua_touserdata(state, -1)); +} + Bridge::Bridge(system_fsm::ServiceLocator& services, lua_State& s) - : services_(services), state_(s) { + : services_(services), state_(s), bindings_(s) { lua_pushstring(&s, kBridgeKey); lua_pushlightuserdata(&s, this); lua_settable(&s, LUA_REGISTRYINDEX); @@ -93,4 +99,31 @@ Bridge::Bridge(system_fsm::ServiceLocator& services, lua_State& s) lua_pop(&s, 1); } +static auto new_property_module(lua_State* state) -> int { + const char* name = luaL_checkstring(state, 1); + luaL_newmetatable(state, name); + + lua_pushstring(state, "__index"); + lua_pushvalue(state, -2); + lua_settable(state, -3); // metatable.__index = metatable + + return 1; +} + +auto Bridge::AddPropertyModule( + const std::string& name, + std::vector>> props) + -> void { + // Create the module (or retrieve it if one with this name already exists) + luaL_requiref(&state_, name.c_str(), new_property_module, true); + + for (const auto& prop : props) { + lua_pushstring(&state_, prop.first.c_str()); + bindings_.Register(&state_, prop.second.get()); + lua_settable(&state_, -3); // metatable.propname = property + } + + lua_pop(&state_, 1); // pop the module off the stack +} + } // namespace lua diff --git a/src/lua/include/bridge.hpp b/src/lua/include/bridge.hpp index 059d0604..26401d14 100644 --- a/src/lua/include/bridge.hpp +++ b/src/lua/include/bridge.hpp @@ -11,19 +11,28 @@ #include "lua.hpp" #include "lvgl.h" +#include "property.hpp" #include "service_locator.hpp" namespace lua { class Bridge { public: + static auto Get(lua_State* state) -> Bridge*; + Bridge(system_fsm::ServiceLocator&, lua_State& s); + auto AddPropertyModule( + const std::string&, + std::vector>>) -> void; + system_fsm::ServiceLocator& services() { return services_; } + PropertyBindings& bindings() { return bindings_; } private: system_fsm::ServiceLocator& services_; lua_State& state_; + PropertyBindings bindings_; }; } // namespace lua diff --git a/src/lua/include/lua_thread.hpp b/src/lua/include/lua_thread.hpp index 381b1bdb..939d0cda 100644 --- a/src/lua/include/lua_thread.hpp +++ b/src/lua/include/lua_thread.hpp @@ -27,6 +27,8 @@ class LuaThread { auto RunScript(const std::string& path) -> bool; + auto bridge() -> Bridge& { return *bridge_; } + private: LuaThread(std::unique_ptr&, std::unique_ptr&, lua_State*); diff --git a/src/lua/include/property.hpp b/src/lua/include/property.hpp new file mode 100644 index 00000000..b6b4718f --- /dev/null +++ b/src/lua/include/property.hpp @@ -0,0 +1,47 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include + +#include "lua.hpp" +#include "lvgl.h" +#include "service_locator.hpp" + +namespace lua { + +using LuaValue = std::variant; + +class Property { + public: + Property() : Property(std::monostate{}) {} + Property(const LuaValue&); + Property(const LuaValue&, std::function); + + auto IsTwoWay() -> bool { return cb_.has_value(); } + + auto PushValue(lua_State& s) -> int; + auto PopValue(lua_State& s) -> bool; + auto Update(const LuaValue& new_val) -> void; + + auto AddLuaBinding(lua_State*, int ref) -> void; + + private: + LuaValue value_; + std::optional> cb_; + std::vector> bindings_; +}; + +class PropertyBindings { + public: + PropertyBindings(lua_State&); + + auto Register(lua_State*, Property*) -> void; +}; + +} // namespace lua diff --git a/src/lua/lua_thread.cpp b/src/lua/lua_thread.cpp index cb7066a5..eb2f5107 100644 --- a/src/lua/lua_thread.cpp +++ b/src/lua/lua_thread.cpp @@ -5,11 +5,11 @@ */ #include "lua_thread.hpp" + #include #include "esp_heap_caps.h" #include "esp_log.h" -#include "lua.h" #include "lua.hpp" #include "luavgl.h" #include "service_locator.hpp" diff --git a/src/lua/property.cpp b/src/lua/property.cpp new file mode 100644 index 00000000..3130077b --- /dev/null +++ b/src/lua/property.cpp @@ -0,0 +1,196 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "property.hpp" + +#include +#include + +#include "lua.h" +#include "lua.hpp" +#include "lvgl.h" +#include "service_locator.hpp" + +namespace lua { + +static const char kMetatableName[] = "property"; +static const char kBindingsTable[] = "bindings"; + +static auto check_property(lua_State* state) -> Property* { + void* data = luaL_checkudata(state, 1, kMetatableName); + luaL_argcheck(state, data != NULL, 1, "`property` expected"); + return *reinterpret_cast(data); +} + +static auto property_get(lua_State* state) -> int { + Property* p = check_property(state); + p->PushValue(*state); + return 1; +} + +static auto property_set(lua_State* state) -> int { + Property* p = check_property(state); + luaL_argcheck(state, p->IsTwoWay(), 1, "property is read-only"); + bool valid = p->PopValue(*state); + lua_pushboolean(state, valid); + return 1; +} + +static auto property_bind(lua_State* state) -> int { + Property* p = check_property(state); + luaL_checktype(state, 2, LUA_TFUNCTION); + + // Copy the function, as we need to invoke it then store our reference. + lua_pushvalue(state, 2); + // ...and another copy, since we return the original closure. + lua_pushvalue(state, 2); + + // FIXME: This should ideally be lua_pcall, for safety. + p->PushValue(*state); + lua_call(state, 1, 0); // Invoke the initial binding. + + lua_pushstring(state, kBindingsTable); + lua_gettable(state, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] + lua_insert(state, -2); // Move bindings to the bottom, with fn above. + int ref = luaL_ref(state, -2); // bindings[ref] = fn + + p->AddLuaBinding(state, ref); + + // Pop the bindings table, leaving one of the copiesw of the callback fn at + // the top of the stack. + lua_pop(state, 1); + + return 1; +} + +static const struct luaL_Reg kPropertyBindingFuncs[] = {{"get", property_get}, + {"set", property_set}, + {"bind", property_bind}, + {NULL, NULL}}; + +PropertyBindings::PropertyBindings(lua_State& s) { + // Create the metatable responsible for the Property API. + luaL_newmetatable(&s, kMetatableName); + + lua_pushliteral(&s, "__index"); + lua_pushvalue(&s, -2); + lua_settable(&s, -3); // metatable.__index = metatable + + // Add our binding funcs (get, set, bind) to the metatable. + luaL_setfuncs(&s, kPropertyBindingFuncs, 0); + + // Create a weak table in the registry to hold live bindings. + lua_pushstring(&s, kBindingsTable); + lua_newtable(&s); // bindings = {} + + // Metatable for the weak table. Values are weak. + lua_newtable(&s); // meta = {} + lua_pushliteral(&s, "__mode"); + lua_pushliteral(&s, "v"); + lua_settable(&s, -3); // meta.__mode='v' + lua_setmetatable(&s, -2); // setmetatable(bindings, meta) + + lua_settable(&s, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] = bindings +} + +auto PropertyBindings::Register(lua_State* s, Property* prop) -> void { + Property** data = + reinterpret_cast(lua_newuserdata(s, sizeof(Property*))); + *data = prop; + + luaL_setmetatable(s, kMetatableName); +} + +template +inline constexpr bool always_false_v = false; + +Property::Property(const LuaValue& val) : value_(val), cb_() {} + +Property::Property(const LuaValue& val, + std::function cb) + : value_(val), cb_(cb) {} + +auto Property::PushValue(lua_State& s) -> int { + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + lua_pushnil(&s); + } else if constexpr (std::is_same_v) { + lua_pushinteger(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushnumber(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushboolean(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushstring(&s, arg.c_str()); + } else { + static_assert(always_false_v, "PushValue missing type"); + } + }, + value_); + return 1; +} + +auto Property::PopValue(lua_State& s) -> bool { + LuaValue new_val; + switch (lua_type(&s, 2)) { + case LUA_TNIL: + new_val = std::monostate{}; + break; + case LUA_TNUMBER: + if (lua_isinteger(&s, 2)) { + new_val = lua_tointeger(&s, 2); + } else { + new_val = lua_tonumber(&s, 2); + } + break; + case LUA_TBOOLEAN: + new_val = lua_toboolean(&s, 2); + break; + case LUA_TSTRING: + new_val = lua_tostring(&s, 2); + break; + default: + return false; + } + + if (cb_ && std::invoke(*cb_, new_val)) { + Update(new_val); + return true; + } + return false; +} + +auto Property::Update(const LuaValue& v) -> void { + value_ = v; + + for (int i = bindings_.size() - 1; i >= 0; i--) { + auto& b = bindings_[i]; + + lua_pushstring(b.first, kBindingsTable); + lua_gettable(b.first, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] + int type = lua_rawgeti(b.first, -1, b.second); // push bindings[i] + + // Has closure has been GCed? + if (type == LUA_TNIL) { + // Clean up after ourselves. + lua_pop(b.first, 1); + // Remove the binding. + bindings_.erase(bindings_.begin() + i); + continue; + } + + PushValue(*b.first); // push the argument + lua_call(b.first, 1, 0); // invoke the closure + } +} + +auto Property::AddLuaBinding(lua_State* state, int ref) -> void { + bindings_.push_back({state, ref}); +} + +} // namespace lua diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 7d1d62d6..39fae4b0 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -21,6 +21,7 @@ #include "model_playback.hpp" #include "model_top_bar.hpp" #include "nvs.hpp" +#include "property.hpp" #include "relative_wheel.hpp" #include "screen_playing.hpp" #include "screen_settings.hpp" @@ -56,9 +57,9 @@ class UiState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} - void react(const system_fsm::BatteryStateChanged&); - void react(const audio::PlaybackStarted&); - void react(const audio::PlaybackFinished&); + 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&); @@ -127,7 +128,19 @@ class Lua : public UiState { void react(const internal::ShowNowPlaying&) override; void react(const internal::ShowSettingsPage&) override; + void react(const system_fsm::BatteryStateChanged&) override; + void react(const audio::PlaybackStarted&) override; + void react(const audio::PlaybackFinished&) override; + using UiState::react; + + private: + 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 { diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 748e08f9..9ecc9b7c 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -33,6 +33,7 @@ #include "modal_progress.hpp" #include "model_playback.hpp" #include "nvs.hpp" +#include "property.hpp" #include "relative_wheel.hpp" #include "screen.hpp" #include "screen_lua.hpp" @@ -183,7 +184,36 @@ void Lua::entry() { sCurrentScreen.reset(new Screen()); lv_group_set_default(sCurrentScreen->group()); + auto bat = + sServices->battery().State().value_or(battery::Battery::BatteryState{}); + battery_pct_ = + std::make_shared(static_cast(bat.percent)); + battery_mv_ = + std::make_shared(static_cast(bat.millivolts)); + battery_charging_ = std::make_shared(bat.is_charging); + + bluetooth_en_ = std::make_shared(false); + playback_playing_ = std::make_shared(false); + playback_track_ = std::make_shared(); + sLua.reset(lua::LuaThread::Start(*sServices, sCurrentScreen->content())); + sLua->bridge().AddPropertyModule("power", + { + {"battery_pct", battery_pct_}, + {"battery_millivolts", battery_mv_}, + {"plugged_in", battery_charging_}, + }); + sLua->bridge().AddPropertyModule("bluetooth", + { + {"enabled", bluetooth_en_}, + {"connected", bluetooth_en_}, + }); + sLua->bridge().AddPropertyModule("playback", + { + {"playing", playback_playing_}, + {"track", playback_track_}, + }); + sLua->RunScript("/lua/main.lua"); lv_group_set_default(NULL); @@ -216,6 +246,19 @@ void Lua::react(const internal::ShowSettingsPage& ev) { transit(); } +void Lua::react(const system_fsm::BatteryStateChanged& ev) { + battery_pct_->Update(static_cast(ev.new_state.percent)); + battery_mv_->Update(static_cast(ev.new_state.millivolts)); +} + +void Lua::react(const audio::PlaybackStarted&) { + playback_playing_->Update(true); +} + +void Lua::react(const audio::PlaybackFinished&) { + playback_playing_->Update(false); +} + void Onboarding::entry() { progress_ = 0; has_formatted_ = false;