diff --git a/.env b/.env index ec2cdf69..e48a5fec 100644 --- a/.env +++ b/.env @@ -2,10 +2,7 @@ # # SPDX-License-Identifier: CC0-1.0 -# Load-bearing useless command, for bash. I have *no* idea why, but evaluating -# $@ is necessary for the rest to work. - -repo_dir=$(pwd) -export PROJ_PATH=$repo_dir -export IDF_PATH=$repo_dir/lib/esp-idf -. $IDF_PATH/export.sh +repo_dir="$(pwd)" +export PROJ_PATH="$repo_dir" +export IDF_PATH="$repo_dir/lib/esp-idf" +. "$IDF_PATH/export.sh" diff --git a/ldoc-stubs/queue.lua b/ldoc-stubs/queue.lua index 2d6a2a29..b3000040 100644 --- a/ldoc-stubs/queue.lua +++ b/ldoc-stubs/queue.lua @@ -15,6 +15,10 @@ queue.size = types.Property -- @see types.Property queue.replay = types.Property +-- Determines whether or not the current track will repeat indefinitely +-- @see types.Property +queue.repeat_track = types.Property + --- Determines whether, when progressing to the next track in the queue, the next track will be chosen randomly. The random selection algorithm used is a Miller Shuffle, which guarantees that no repeat selections will be made until every item in the queue has been played. -- @see types.Property queue.random = types.Property diff --git a/lua/img/repeat.png b/lua/img/repeat.png new file mode 100644 index 00000000..9a4da7fd Binary files /dev/null and b/lua/img/repeat.png differ diff --git a/lua/img/repeat_disabled.png b/lua/img/repeat_disabled.png new file mode 100644 index 00000000..20b6ab59 Binary files /dev/null and b/lua/img/repeat_disabled.png differ diff --git a/lua/img/shuffle.png b/lua/img/shuffle.png new file mode 100644 index 00000000..b54e359d Binary files /dev/null and b/lua/img/shuffle.png differ diff --git a/lua/img/shuffle_disabled.png b/lua/img/shuffle_disabled.png new file mode 100644 index 00000000..912d0e95 Binary files /dev/null and b/lua/img/shuffle_disabled.png differ diff --git a/lua/playing.lua b/lua/playing.lua index e31e10a2..c6a3f47e 100644 --- a/lua/playing.lua +++ b/lua/playing.lua @@ -12,6 +12,10 @@ local img = { next_disabled = "//lua/img/next_disabled.png", prev = "//lua/img/prev.png", prev_disabled = "//lua/img/prev_disabled.png", + shuffle = "//lua/img/shuffle.png", + shuffle_disabled = "//lua/img/shuffle_disabled.png", + repeat_enabled = "//lua/img/repeat.png", + repeat_disabled = "//lua/img/repeat_disabled.png", } return function(opts) @@ -69,10 +73,21 @@ return function(opts) align_items = "center", align_content = "center", }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + } + + playlist:Object({ w = 3, h = 1 }) -- spacer + + local cur_time = playlist:Label { w = lvgl.SIZE_CONTENT, h = lvgl.SIZE_CONTENT, + text = "", + text_font = font.fusion_10, } + playlist:Object({ flex_grow = 1, h = 1 }) -- spacer + local playlist_pos = playlist:Label { text = "", text_font = font.fusion_10, @@ -86,6 +101,17 @@ return function(opts) text_font = font.fusion_10, } + playlist:Object({ flex_grow = 1, h = 1 }) -- spacer + + local end_time = playlist:Label { + w = lvgl.SIZE_CONTENT, + h = lvgl.SIZE_CONTENT, + align = lvgl.ALIGN.RIGHT_MID, + text = "", + text_font = font.fusion_10, + } + playlist:Object({ w = 3, h = 1 }) -- spacer + local scrubber = screen.root:Bar { w = lvgl.PCT(100), h = 5, @@ -106,15 +132,15 @@ return function(opts) pad_all = 2, } - local cur_time = controls:Label { - w = lvgl.SIZE_CONTENT, - h = lvgl.SIZE_CONTENT, - text = "", - text_font = font.fusion_10, - } controls:Object({ flex_grow = 1, h = 1 }) -- spacer + local repeat_btn = controls:Button {} + repeat_btn:onClicked(function() + queue.repeat_track:set(not queue.repeat_track:get()) + end) + local repeat_img = repeat_btn:Image { src = img.repeat_enabled } + local prev_btn = controls:Button {} prev_btn:onClicked(queue.previous) local prev_img = prev_btn:Image { src = img.prev_disabled } @@ -130,15 +156,14 @@ return function(opts) next_btn:onClicked(queue.next) local next_img = next_btn:Image { src = img.next_disabled } + local shuffle_btn = controls:Button {} + shuffle_btn:onClicked(function() + queue.random:set(not queue.random:get()) + end) + local shuffle_img = shuffle_btn:Image { src = img.shuffle } + 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) @@ -181,6 +206,20 @@ return function(opts) pos > 1 and img.prev or img.prev_disabled ) end), + queue.random:bind(function(shuffling) + if shuffling then + shuffle_img:set_src(img.shuffle) + else + shuffle_img:set_src(img.shuffle_disabled) + end + end), + queue.repeat_track:bind(function(en) + if en then + repeat_img:set_src(img.repeat_enabled) + else + repeat_img:set_src(img.repeat_disabled) + end + end), queue.size:bind(function(num) if not num then return end playlist_total:set { text = tostring(num) } diff --git a/luals-stubs/queue.lua b/luals-stubs/queue.lua index ece99c69..08247799 100644 --- a/luals-stubs/queue.lua +++ b/luals-stubs/queue.lua @@ -5,6 +5,7 @@ --- @field position Property The index in the queue of the currently playing track. This may be zero if the queue is empty. Writeable. --- @field size Property The total number of tracks in the queue, including tracks which have already been played. --- @field replay Property Whether or not the queue will be restarted after the final track is played. Writeable. +--- @field repeat_track Property Whether or not the current track will repeat indefinitely. Writeable. --- @field random Property Determines whether, when progressing to the next track in the queue, the next track will be chosen randomly. The random selection algorithm used is a Miller Shuffle, which guarantees that no repeat selections will be made until every item in the queue has been played. Writeable. local queue = {} diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index bb0aef6d..95abfa2a 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -357,7 +357,7 @@ void Playback::react(const internal::InputFileClosed& ev) {} void Playback::react(const internal::InputFileFinished& ev) { ESP_LOGI(kTag, "finished playing file"); - sServices->track_queue().next(); + sServices->track_queue().finish(); if (!sServices->track_queue().current()) { transit(); } diff --git a/src/audio/include/track_queue.hpp b/src/audio/include/track_queue.hpp index 5b14fd4a..fd6061a7 100644 --- a/src/audio/include/track_queue.hpp +++ b/src/audio/include/track_queue.hpp @@ -33,13 +33,13 @@ class RandomIterator { // Note resizing has the side-effect of restarting iteration. auto resize(size_t) -> void; - auto repeat(bool) -> void; + auto replay(bool) -> void; private: size_t seed_; size_t pos_; size_t size_; - bool repeat_; + bool replay_; }; /* @@ -85,6 +85,12 @@ class TrackQueue { auto next() -> void; auto previous() -> void; + /* + * Called when the current track finishes + */ + auto finish() -> void; + + auto skipTo(database::TrackId) -> void; /* @@ -98,6 +104,9 @@ class TrackQueue { auto repeat(bool) -> void; auto repeat() const -> bool; + auto replay(bool) -> void; + auto replay() const -> bool; + auto serialise() -> std::string; auto deserialise(const std::string&) -> void; @@ -115,6 +124,7 @@ class TrackQueue { std::optional shuffle_; bool repeat_; + bool replay_; }; } // namespace audio diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index d68f2821..c4c101f6 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -34,12 +34,12 @@ namespace audio { [[maybe_unused]] static constexpr char kTag[] = "tracks"; RandomIterator::RandomIterator(size_t size) - : seed_(), pos_(0), size_(size), repeat_(false) { + : seed_(), pos_(0), size_(size), replay_(false) { esp_fill_random(&seed_, sizeof(seed_)); } auto RandomIterator::current() const -> size_t { - if (pos_ < size_ || repeat_) { + if (pos_ < size_ || replay_) { return MillerShuffle(pos_, seed_, size_); } return size_; @@ -65,8 +65,8 @@ auto RandomIterator::resize(size_t s) -> void { pos_ = 0; } -auto RandomIterator::repeat(bool r) -> void { - repeat_ = r; +auto RandomIterator::replay(bool r) -> void { + replay_ = r; } auto notifyChanged(bool current_changed) -> void { @@ -81,7 +81,8 @@ TrackQueue::TrackQueue(tasks::WorkerPool& bg_worker) pos_(0), tracks_(&memory::kSpiRamResource), shuffle_(), - repeat_(false) {} + repeat_(false), + replay_(false) {} auto TrackQueue::current() const -> std::optional { const std::shared_lock lock(mutex_); @@ -202,7 +203,7 @@ auto TrackQueue::next() -> void { pos_ = shuffle_->current(); } else { if (pos_ + 1 >= tracks_.size()) { - if (repeat_) { + if (replay_) { pos_ = 0; } } else { @@ -231,6 +232,14 @@ auto TrackQueue::previous() -> void { notifyChanged(true); } +auto TrackQueue::finish() -> void { + if (repeat_) { + notifyChanged(true); + } else { + next(); + } +} + auto TrackQueue::skipTo(database::TrackId id) -> void { // Defer this work to the background not because it's particularly // long-running (although it could be), but because we want to ensure we only @@ -279,7 +288,7 @@ auto TrackQueue::random(bool en) -> void { // repeated calls with en == true will re-shuffle. if (en) { shuffle_.emplace(tracks_.size()); - shuffle_->repeat(repeat_); + shuffle_->replay(replay_); } else { shuffle_.reset(); } @@ -298,9 +307,6 @@ auto TrackQueue::repeat(bool en) -> void { { const std::unique_lock lock(mutex_); repeat_ = en; - if (shuffle_) { - shuffle_->repeat(en); - } } notifyChanged(false); @@ -311,6 +317,22 @@ auto TrackQueue::repeat() const -> bool { return repeat_; } +auto TrackQueue::replay(bool en) -> void { + { + const std::unique_lock lock(mutex_); + replay_ = en; + if (shuffle_) { + shuffle_->replay(en); + } + } + notifyChanged(false); +} + +auto TrackQueue::replay() const -> bool { + const std::shared_lock lock(mutex_); + return replay_; +} + auto TrackQueue::serialise() -> std::string { cppbor::Array tracks{}; for (database::TrackId track : tracks_) { diff --git a/src/tasks/tasks.hpp b/src/tasks/tasks.hpp index 1623a8d8..47f26837 100644 --- a/src/tasks/tasks.hpp +++ b/src/tasks/tasks.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "esp_heap_caps.h" diff --git a/src/ui/include/themes.hpp b/src/ui/include/themes.hpp index 576ea42e..11680c0d 100644 --- a/src/ui/include/themes.hpp +++ b/src/ui/include/themes.hpp @@ -32,6 +32,7 @@ class Theme { lv_style_t button_style_; lv_style_t bar_style_; lv_style_t dropdown_style_; + lv_style_t dropdown_list_style_; lv_style_t slider_indicator_style_; lv_style_t slider_knob_style_; diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index c097e764..52ab77a5 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -116,6 +116,7 @@ class UiState : public tinyfsm::Fsm { static lua::Property sQueuePosition; static lua::Property sQueueSize; + static lua::Property sQueueReplay; static lua::Property sQueueRepeat; static lua::Property sQueueRandom; @@ -165,6 +166,7 @@ class Lua : public UiState { auto SetPlaying(const lua::LuaValue&) -> bool; auto SetRandom(const lua::LuaValue&) -> bool; auto SetRepeat(const lua::LuaValue&) -> bool; + auto SetReplay(const lua::LuaValue&) -> bool; auto QueueNext(lua_State*) -> int; auto QueuePrevious(lua_State*) -> int; diff --git a/src/ui/themes.cpp b/src/ui/themes.cpp index bad73ee6..f8390570 100644 --- a/src/ui/themes.cpp +++ b/src/ui/themes.cpp @@ -89,6 +89,14 @@ Theme::Theme() { lv_style_set_border_color(&dropdown_style_, lv_palette_main(LV_PALETTE_BLUE)); lv_style_set_border_side(&dropdown_style_, LV_BORDER_SIDE_FULL); + lv_style_init(&dropdown_list_style_); + lv_style_set_radius(&dropdown_list_style_, 2); + lv_style_set_border_width(&dropdown_list_style_, 1); + lv_style_set_border_color(&dropdown_list_style_, lv_palette_main(LV_PALETTE_BLUE_GREY)); + lv_style_set_bg_opa(&dropdown_list_style_, LV_OPA_COVER); + lv_style_set_bg_color(&dropdown_list_style_, lv_color_white()); + lv_style_set_pad_all(&dropdown_list_style_, 2); + lv_theme_t* parent_theme = lv_disp_get_theme(NULL); theme_ = *parent_theme; theme_.user_data = this; @@ -124,6 +132,8 @@ void Theme::Callback(lv_obj_t* obj) { lv_obj_add_style(obj, &switch_knob_style_, LV_PART_KNOB); } else if (lv_obj_check_type(obj, &lv_dropdown_class)) { lv_obj_add_style(obj, &dropdown_style_, LV_PART_MAIN); + } else if (lv_obj_check_type(obj, &lv_dropdownlist_class)) { + lv_obj_add_style(obj, &dropdown_list_style_, LV_PART_MAIN); } } diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 630238e7..728c9756 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -127,8 +127,30 @@ lua::Property UiState::sPlaybackPosition{0}; lua::Property UiState::sQueuePosition{0}; lua::Property UiState::sQueueSize{0}; -lua::Property UiState::sQueueRepeat{false}; -lua::Property UiState::sQueueRandom{false}; +lua::Property UiState::sQueueRepeat{false, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + bool new_val = std::get(val); + sServices->track_queue().repeat(new_val); + return true; +}}; +lua::Property UiState::sQueueReplay{false, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + bool new_val = std::get(val); + sServices->track_queue().replay(new_val); + return true; +}}; +lua::Property UiState::sQueueRandom{false, [](const lua::LuaValue& val) { + if (!std::holds_alternative(val)) { + return false; + } + bool new_val = std::get(val); + sServices->track_queue().random(new_val); + return true; +}}; lua::Property UiState::sVolumeCurrentPct{ 0, [](const lua::LuaValue& val) { @@ -296,6 +318,7 @@ void UiState::react(const audio::QueueUpdate&) { sQueuePosition.Update(current_pos); sQueueRandom.Update(queue.random()); sQueueRepeat.Update(queue.repeat()); + sQueueReplay.Update(queue.replay()); } void UiState::react(const audio::PlaybackStarted& ev) { @@ -423,7 +446,8 @@ void Lua::entry() { {"previous", [&](lua_State* s) { return QueuePrevious(s); }}, {"position", &sQueuePosition}, {"size", &sQueueSize}, - {"replay", &sQueueRepeat}, + {"replay", &sQueueReplay}, + {"repeat_track", &sQueueRepeat}, {"random", &sQueueRandom}, }); sLua->bridge().AddPropertyModule("volume", @@ -567,6 +591,15 @@ auto Lua::SetRepeat(const lua::LuaValue& val) -> bool { return true; } +auto Lua::SetReplay(const lua::LuaValue& val) -> bool { + if (!std::holds_alternative(val)) { + return false; + } + bool b = std::get(val); + sServices->track_queue().replay(b); + return true; +} + void Lua::exit() { lv_group_set_default(NULL); }