From f09ba5ffd53bf7d28e0dc516c00a8f69ca7efae9 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Thu, 28 Sep 2023 08:29:55 +1000 Subject: Use bindey for databinding instead of hand rolling ui updates --- src/app_console/app_console.cpp | 20 ++-- src/audio/audio_decoder.cpp | 1 + src/audio/fatfs_audio_input.cpp | 7 +- src/audio/track_queue.cpp | 5 + src/battery/include/battery.hpp | 4 + src/database/database.cpp | 104 +++++++++--------- src/database/include/database.hpp | 24 ++-- src/database/include/records.hpp | 2 +- src/database/include/tag_parser.hpp | 18 +-- src/database/include/track.hpp | 43 ++++---- src/database/records.cpp | 4 +- src/database/tag_parser.cpp | 50 +++++---- src/database/track.cpp | 6 - src/playlist/source.cpp | 4 +- src/system_fsm/booting.cpp | 1 + src/ui/CMakeLists.txt | 3 +- src/ui/event_binding.cpp | 23 ++++ src/ui/include/event_binding.hpp | 30 +++++ src/ui/include/model_playback.hpp | 26 +++++ src/ui/include/screen.hpp | 14 +++ src/ui/include/screen_playing.hpp | 33 ++---- src/ui/include/ui_fsm.hpp | 23 ++-- src/ui/screen_playing.cpp | 214 ++++++++++++++++++------------------ src/ui/screen_track_browser.cpp | 4 +- src/ui/ui_fsm.cpp | 63 ++++++----- 25 files changed, 420 insertions(+), 306 deletions(-) create mode 100644 src/ui/event_binding.cpp create mode 100644 src/ui/include/event_binding.hpp create mode 100644 src/ui/include/model_playback.hpp (limited to 'src') diff --git a/src/app_console/app_console.cpp b/src/app_console/app_console.cpp index 6573ee49..83406650 100644 --- a/src/app_console/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -187,8 +187,8 @@ int CmdDbTracks(int argc, char** argv) { std::unique_ptr> res( db->GetTracks(20).get()); while (true) { - for (database::Track s : res->values()) { - std::cout << s.tags()[database::Tag::kTitle].value_or("[BLANK]") + for (const auto& s : res->values()) { + std::cout << s->tags()[database::Tag::kTitle].value_or("[BLANK]") << std::endl; } if (res->next_page()) { @@ -256,12 +256,12 @@ int CmdDbIndex(int argc, char** argv) { std::cout << "choice out of range" << std::endl; return -1; } - if (res->values().at(choice).track()) { + if (res->values().at(choice)->track()) { AppConsole::sServices->track_queue().IncludeLast( std::make_shared( AppConsole::sServices->database(), res, 0, res, choice)); } - auto cont = res->values().at(choice).Expand(20); + auto cont = res->values().at(choice)->Expand(20); if (!cont) { std::cout << "more choices than levels" << std::endl; return 0; @@ -270,10 +270,10 @@ int CmdDbIndex(int argc, char** argv) { choice_index++; } - for (database::IndexRecord r : res->values()) { - std::cout << r.text().value_or(""); - if (r.track()) { - std::cout << "\t(id:" << *r.track() << ")"; + for (const auto& r : res->values()) { + std::cout << r->text().value_or(""); + if (r->track()) { + std::cout << "\t(id:" << *r->track() << ")"; } std::cout << std::endl; } @@ -311,8 +311,8 @@ int CmdDbDump(int argc, char** argv) { std::unique_ptr> res(db->GetDump(5).get()); while (true) { - for (const std::pmr::string& s : res->values()) { - std::cout << s << std::endl; + for (const auto& s : res->values()) { + std::cout << *s << std::endl; } if (res->next_page()) { auto continuation = res->next_page().value(); diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 7751bf37..86394a37 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -103,6 +103,7 @@ void Decoder::Main() { for (;;) { if (source_->HasNewStream() || !stream_) { std::shared_ptr new_stream = source_->NextStream(); + ESP_LOGI(kTag, "decoder has new stream"); if (new_stream && BeginDecoding(new_stream)) { stream_ = new_stream; } else { diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index f71f0463..6039ff9d 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -34,6 +34,7 @@ #include "future_fetcher.hpp" #include "tag_parser.hpp" #include "tasks.hpp" +#include "track.hpp" #include "types.hpp" static const char* kTag = "SRC"; @@ -118,13 +119,13 @@ auto FatfsAudioInput::NextStream() -> std::shared_ptr { auto FatfsAudioInput::OpenFile(const std::pmr::string& path) -> bool { ESP_LOGI(kTag, "opening file %s", path.c_str()); - database::TrackTags tags; - if (!tag_parser_.ReadAndParseTags(path, &tags)) { + auto tags = tag_parser_.ReadAndParseTags(path); + if (!tags) { ESP_LOGE(kTag, "failed to read tags"); return false; } - auto stream_type = ContainerToStreamType(tags.encoding()); + auto stream_type = ContainerToStreamType(tags->encoding()); if (!stream_type.has_value()) { ESP_LOGE(kTag, "couldn't match container to stream"); return false; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index 6f17ad33..b1cacc00 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -19,6 +19,8 @@ namespace audio { +static constexpr char kTag[] = "tracks"; + TrackQueue::TrackQueue() {} auto TrackQueue::GetCurrent() const -> std::optional { @@ -202,6 +204,9 @@ auto TrackQueue::Previous() -> void { auto TrackQueue::Clear() -> void { const std::lock_guard lock(mutex_); + if (enqueued_.empty() && played_.empty()) { + return; + } QueueUpdate ev{.current_changed = !enqueued_.empty()}; played_.clear(); enqueued_.clear(); diff --git a/src/battery/include/battery.hpp b/src/battery/include/battery.hpp index 63a8a47b..32e02347 100644 --- a/src/battery/include/battery.hpp +++ b/src/battery/include/battery.hpp @@ -26,6 +26,10 @@ class Battery { struct BatteryState { uint_fast8_t percent; bool is_charging; + + bool operator==(const BatteryState& other) const { + return percent == other.percent && is_charging == other.is_charging; + } }; auto State() -> std::optional; diff --git a/src/database/database.cpp b/src/database/database.cpp index fd0e50c1..1ecd72e0 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -144,7 +144,7 @@ auto Database::Update() -> std::future { OwningSlice prefix = EncodeDataPrefix(); it->Seek(prefix.slice); while (it->Valid() && it->key().starts_with(prefix.slice)) { - std::optional track = ParseDataValue(it->value()); + std::shared_ptr track = ParseDataValue(it->value()); if (!track) { // The value was malformed. Drop this record. ESP_LOGW(kTag, "dropping malformed metadata"); @@ -159,9 +159,9 @@ auto Database::Update() -> std::future { continue; } - TrackTags tags{}; - if (!tag_parser_.ReadAndParseTags(track->filepath(), &tags) || - tags.encoding() == Container::kUnsupported) { + std::shared_ptr tags = + tag_parser_.ReadAndParseTags(track->filepath()); + if (!tags || tags->encoding() == Container::kUnsupported) { // We couldn't read the tags for this track. Either they were // malformed, or perhaps the file is missing. Either way, tombstone // this record. @@ -174,7 +174,7 @@ auto Database::Update() -> std::future { // At this point, we know that the track still exists in its original // location. All that's left to do is update any metadata about it. - uint64_t new_hash = tags.Hash(); + uint64_t new_hash = tags->Hash(); if (new_hash != track->tags_hash()) { // This track's tags have changed. Since the filepath is exactly the // same, we assume this is a legitimate correction. Update the @@ -185,7 +185,9 @@ auto Database::Update() -> std::future { dbPutHash(new_hash, track->id()); } - dbCreateIndexesForTrack({*track, tags}); + Track t{track, tags}; + + dbCreateIndexesForTrack(t); it->Next(); } @@ -197,15 +199,14 @@ auto Database::Update() -> std::future { .stage = event::UpdateProgress::Stage::kScanningForNewTracks, }); file_gatherer_.FindFiles("", [&](const std::pmr::string& path) { - TrackTags tags; - if (!tag_parser_.ReadAndParseTags(path, &tags) || - tags.encoding() == Container::kUnsupported) { + std::shared_ptr tags = tag_parser_.ReadAndParseTags(path); + if (!tags || tags->encoding() == Container::kUnsupported) { // No parseable tags; skip this fiile. return; } // Check for any existing record with the same hash. - uint64_t hash = tags.Hash(); + uint64_t hash = tags->Hash(); OwningSlice key = EncodeHashKey(hash); std::optional existing_hash; std::string raw_entry; @@ -219,33 +220,36 @@ auto Database::Update() -> std::future { TrackId id = dbMintNewTrackId(); ESP_LOGI(kTag, "recording new 0x%lx", id); - TrackData data(id, path, hash); - dbPutTrackData(data); + auto data = std::make_shared(id, path, hash); + dbPutTrackData(*data); dbPutHash(hash, id); - dbCreateIndexesForTrack({data, tags}); + auto t = std::make_shared(data, tags); + dbCreateIndexesForTrack(*t); return; } - std::optional existing_data = dbGetTrackData(*existing_hash); + std::shared_ptr existing_data = dbGetTrackData(*existing_hash); if (!existing_data) { // We found a hash that matches, but there's no data record? Weird. - TrackData new_data(*existing_hash, path, hash); - dbPutTrackData(new_data); - dbCreateIndexesForTrack({*existing_data, tags}); + auto new_data = std::make_shared(*existing_hash, path, hash); + dbPutTrackData(*new_data); + auto t = std::make_shared(new_data, tags); + dbCreateIndexesForTrack(*t); return; } if (existing_data->is_tombstoned()) { ESP_LOGI(kTag, "exhuming track %lu", existing_data->id()); dbPutTrackData(existing_data->Exhume(path)); - dbCreateIndexesForTrack({*existing_data, tags}); + auto t = std::make_shared(existing_data, tags); + dbCreateIndexesForTrack(*t); } else if (existing_data->filepath() != path) { ESP_LOGW(kTag, "tag hash collision for %s and %s", existing_data->filepath().c_str(), path.c_str()); ESP_LOGI(kTag, "hash components: %s, %s, %s", - tags.at(Tag::kTitle).value_or("no title").c_str(), - tags.at(Tag::kArtist).value_or("no artist").c_str(), - tags.at(Tag::kAlbum).value_or("no album").c_str()); + tags->at(Tag::kTitle).value_or("no title").c_str(), + tags->at(Tag::kArtist).value_or("no artist").c_str(), + tags->at(Tag::kAlbum).value_or("no album").c_str()); } }); events::Ui().Dispatch(event::UpdateFinished{}); @@ -264,26 +268,27 @@ auto Database::GetTrackPath(TrackId id) }); } -auto Database::GetTrack(TrackId id) -> std::future> { - return worker_task_->Dispatch>( - [=, this]() -> std::optional { - std::optional data = dbGetTrackData(id); +auto Database::GetTrack(TrackId id) -> std::future> { + return worker_task_->Dispatch>( + [=, this]() -> std::shared_ptr { + std::shared_ptr data = dbGetTrackData(id); if (!data || data->is_tombstoned()) { return {}; } - TrackTags tags; - if (!tag_parser_.ReadAndParseTags(data->filepath(), &tags)) { + std::shared_ptr tags = + tag_parser_.ReadAndParseTags(data->filepath()); + if (!tags) { return {}; } - return Track(*data, tags); + return std::make_shared(data, tags); }); } auto Database::GetBulkTracks(std::vector ids) - -> std::future>> { - return worker_task_->Dispatch>>( - [=, this]() -> std::vector> { - std::map id_to_track{}; + -> std::future>> { + return worker_task_->Dispatch>>( + [=, this]() -> std::vector> { + std::map> id_to_track{}; // Sort the list of ids so that we can retrieve them all in a single // iteration through the database, without re-seeking. @@ -299,16 +304,16 @@ auto Database::GetBulkTracks(std::vector ids) // This id wasn't found at all. Skip it. continue; } - std::optional track = + std::shared_ptr track = ParseRecord(it->key(), it->value()); if (track) { - id_to_track.insert({id, *track}); + id_to_track.insert({id, track}); } } // We've fetched all of the ids in the request, so now just put them // back into the order they were asked for in. - std::vector> results; + std::vector> results; for (const TrackId& id : ids) { if (id_to_track.contains(id)) { results.push_back(id_to_track.at(id)); @@ -426,7 +431,7 @@ auto Database::dbPutTrackData(const TrackData& s) -> void { } } -auto Database::dbGetTrackData(TrackId id) -> std::optional { +auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr { OwningSlice key = EncodeDataKey(id); std::string raw_val; if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) { @@ -454,7 +459,7 @@ auto Database::dbGetHash(const uint64_t& hash) -> std::optional { return ParseHashValue(raw_val); } -auto Database::dbCreateIndexesForTrack(Track track) -> void { +auto Database::dbCreateIndexesForTrack(const Track& track) -> void { for (const IndexInfo& index : GetIndexes()) { leveldb::WriteBatch writes; if (Index(index, track, &writes)) { @@ -481,7 +486,7 @@ auto Database::dbGetPage(const Continuation& c) -> Result* { // Grab results. std::optional first_key; - std::vector records; + std::vector> records; while (records.size() < c.page_size && it->Valid()) { if (!it->key().starts_with({c.prefix.data(), c.prefix.size()})) { break; @@ -489,9 +494,9 @@ auto Database::dbGetPage(const Continuation& c) -> Result* { if (!first_key) { first_key = it->key().ToString(); } - std::optional parsed = ParseRecord(it->key(), it->value()); + std::shared_ptr parsed = ParseRecord(it->key(), it->value()); if (parsed) { - records.push_back(*parsed); + records.push_back(parsed); } if (c.forward) { it->Next(); @@ -577,7 +582,7 @@ template auto Database::dbGetPage( template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional { + -> std::shared_ptr { std::optional data = ParseIndexKey(key); if (!data) { return {}; @@ -588,28 +593,29 @@ auto Database::ParseRecord(const leveldb::Slice& key, title = val.ToString(); } - return IndexRecord(*data, title, data->track); + return std::make_shared(*data, title, data->track); } template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional { - std::optional data = ParseDataValue(val); + -> std::shared_ptr { + std::shared_ptr data = ParseDataValue(val); if (!data || data->is_tombstoned()) { return {}; } - TrackTags tags; - if (!tag_parser_.ReadAndParseTags(data->filepath(), &tags)) { + std::shared_ptr tags = + tag_parser_.ReadAndParseTags(data->filepath()); + if (!tags) { return {}; } - return Track(*data, tags); + return std::make_shared(data, tags); } template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional { + -> std::shared_ptr { std::ostringstream stream; stream << "key: "; if (key.size() < 3 || key.data()[1] != '\0') { @@ -634,7 +640,7 @@ auto Database::ParseRecord(const leveldb::Slice& key, } } std::pmr::string res{stream.str(), &memory::kSpiRamResource}; - return res; + return std::make_shared(res); } IndexRecord::IndexRecord(const IndexKey& key, diff --git a/src/database/include/database.hpp b/src/database/include/database.hpp index 98540f41..6ad8d318 100644 --- a/src/database/include/database.hpp +++ b/src/database/include/database.hpp @@ -48,12 +48,14 @@ struct Continuation { template class Result { public: - auto values() const -> const std::vector& { return values_; } + auto values() const -> const std::vector>& { + return values_; + } auto next_page() -> std::optional>& { return next_page_; } auto prev_page() -> std::optional>& { return prev_page_; } - Result(const std::vector&& values, + Result(const std::vector>&& values, std::optional> next, std::optional> prev) : values_(values), next_page_(next), prev_page_(prev) {} @@ -62,7 +64,7 @@ class Result { Result& operator=(const Result&) = delete; private: - std::vector values_; + std::vector> values_; std::optional> next_page_; std::optional> prev_page_; }; @@ -102,14 +104,14 @@ class Database { auto GetTrackPath(TrackId id) -> std::future>; - auto GetTrack(TrackId id) -> std::future>; + auto GetTrack(TrackId id) -> std::future>; /* * Fetches data for multiple tracks more efficiently than multiple calls to * GetTrack. */ auto GetBulkTracks(std::vector id) - -> std::future>>; + -> std::future>>; auto GetIndexes() -> std::vector; auto GetTracksByIndex(const IndexInfo& index, std::size_t page_size) @@ -145,30 +147,30 @@ class Database { auto dbEntomb(TrackId track, uint64_t hash) -> void; auto dbPutTrackData(const TrackData& s) -> void; - auto dbGetTrackData(TrackId id) -> std::optional; + auto dbGetTrackData(TrackId id) -> std::shared_ptr; auto dbPutHash(const uint64_t& hash, TrackId i) -> void; auto dbGetHash(const uint64_t& hash) -> std::optional; - auto dbCreateIndexesForTrack(Track track) -> void; + auto dbCreateIndexesForTrack(const Track& track) -> void; template auto dbGetPage(const Continuation& c) -> Result*; template auto ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional; + -> std::shared_ptr; }; template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional; + -> std::shared_ptr; template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional; + -> std::shared_ptr; template <> auto Database::ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) - -> std::optional; + -> std::shared_ptr; } // namespace database diff --git a/src/database/include/records.hpp b/src/database/include/records.hpp index b144dece..e7d7738c 100644 --- a/src/database/include/records.hpp +++ b/src/database/include/records.hpp @@ -53,7 +53,7 @@ auto EncodeDataValue(const TrackData& track) -> OwningSlice; * Parses bytes previously encoded via EncodeDataValue back into a TrackData. * May return nullopt if parsing fails. */ -auto ParseDataValue(const leveldb::Slice& slice) -> std::optional; +auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr; /* Encodes a hash key for the specified hash. */ auto EncodeHashKey(const uint64_t& hash) -> OwningSlice; diff --git a/src/database/include/tag_parser.hpp b/src/database/include/tag_parser.hpp index d77967d8..04817c59 100644 --- a/src/database/include/tag_parser.hpp +++ b/src/database/include/tag_parser.hpp @@ -16,21 +16,21 @@ namespace database { class ITagParser { public: virtual ~ITagParser() {} - virtual auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) - -> bool = 0; + virtual auto ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr = 0; }; class GenericTagParser : public ITagParser { public: - auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) - -> bool override; + auto ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr override; }; class TagParserImpl : public ITagParser { public: TagParserImpl(); - auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) - -> bool override; + auto ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr override; private: std::map> extension_to_parser_; @@ -41,7 +41,7 @@ class TagParserImpl : public ITagParser { * cache should be slightly larger than any page sizes in the UI. */ std::mutex cache_mutex_; - util::LruCache<16, std::pmr::string, TrackTags> cache_; + util::LruCache<16, std::pmr::string, std::shared_ptr> cache_; // We could also consider keeping caches of artist name -> std::pmr::string // and similar. This hasn't been done yet, as this isn't a common workload in @@ -50,8 +50,8 @@ class TagParserImpl : public ITagParser { class OpusTagParser : public ITagParser { public: - auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) - -> bool override; + auto ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr override; }; } // namespace database diff --git a/src/database/include/track.hpp b/src/database/include/track.hpp index 1c11ddea..3c7b20fa 100644 --- a/src/database/include/track.hpp +++ b/src/database/include/track.hpp @@ -61,12 +61,17 @@ enum class Tag { */ class TrackTags { public: - auto encoding() const -> Container { return encoding_; }; - auto encoding(Container e) -> void { encoding_ = e; }; - TrackTags() : encoding_(Container::kUnsupported), tags_(&memory::kSpiRamResource) {} + TrackTags(const TrackTags& other) = delete; + TrackTags& operator=(TrackTags& other) = delete; + + bool operator==(const TrackTags&) const = default; + + auto encoding() const -> Container { return encoding_; }; + auto encoding(Container e) -> void { encoding_ = e; }; + std::optional channels; std::optional sample_rate; std::optional bits_per_sample; @@ -85,10 +90,6 @@ class TrackTags { */ auto Hash() const -> uint64_t; - bool operator==(const TrackTags&) const = default; - TrackTags& operator=(const TrackTags&) = default; - TrackTags(const TrackTags&) = default; - private: Container encoding_; std::pmr::unordered_map tags_; @@ -139,6 +140,11 @@ class TrackData { play_count_(play_count), is_tombstoned_(is_tombstoned) {} + TrackData(TrackData&& other) = delete; + TrackData& operator=(TrackData& other) = delete; + + bool operator==(const TrackData&) const = default; + auto id() const -> TrackId { return id_; } auto filepath() const -> std::pmr::string { return filepath_; } auto play_count() const -> uint32_t { return play_count_; } @@ -158,8 +164,6 @@ class TrackData { * new location. */ auto Exhume(const std::pmr::string& new_path) const -> TrackData; - - bool operator==(const TrackData&) const = default; }; /* @@ -172,23 +176,22 @@ class TrackData { */ class Track { public: - Track(const TrackData& data, const TrackTags& tags) + Track(std::shared_ptr& data, std::shared_ptr tags) : data_(data), tags_(tags) {} - Track(const Track& other) = default; - auto data() const -> const TrackData& { return data_; } - auto tags() const -> const TrackTags& { return tags_; } - - auto TitleOrFilename() const -> std::pmr::string; + Track(Track& other) = delete; + Track& operator=(Track& other) = delete; bool operator==(const Track&) const = default; - Track operator=(const Track& other) const { return Track(other); } + + auto data() const -> const TrackData& { return *data_; } + auto tags() const -> const TrackTags& { return *tags_; } + + auto TitleOrFilename() const -> std::pmr::string; private: - const TrackData data_; - const TrackTags tags_; + std::shared_ptr data_; + std::shared_ptr tags_; }; -void swap(Track& first, Track& second); - } // namespace database diff --git a/src/database/records.cpp b/src/database/records.cpp index f493500c..103b3547 100644 --- a/src/database/records.cpp +++ b/src/database/records.cpp @@ -149,7 +149,7 @@ auto EncodeDataValue(const TrackData& track) -> OwningSlice { return OwningSlice(as_str); } -auto ParseDataValue(const leveldb::Slice& slice) -> std::optional { +auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr { CborParser parser; CborValue container; CborError err; @@ -211,7 +211,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional { return {}; } - return TrackData(id, path, hash, play_count, is_tombstoned); + return std::make_shared(id, path, hash, play_count, is_tombstoned); } /* 'H/ 0xBEEF' */ diff --git a/src/database/tag_parser.cpp b/src/database/tag_parser.cpp index 8912690b..fe71089d 100644 --- a/src/database/tag_parser.cpp +++ b/src/database/tag_parser.cpp @@ -130,14 +130,13 @@ TagParserImpl::TagParserImpl() { extension_to_parser_["opus"] = std::make_unique(); } -auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path, - TrackTags* out) -> bool { +auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr { { std::lock_guard lock{cache_mutex_}; - std::optional cached = cache_.Get(path); + std::optional> cached = cache_.Get(path); if (cached) { - *out = *cached; - return true; + return *cached; } } @@ -152,41 +151,43 @@ auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path, } } - if (!parser->ReadAndParseTags(path, out)) { - return false; + std::shared_ptr tags = parser->ReadAndParseTags(path); + if (!tags) { + return {}; } // There wasn't a track number found in the track's tags. Try to synthesize // one from the filename, which will sometimes have a track number at the // start. - if (!out->at(Tag::kAlbumTrack)) { + if (!tags->at(Tag::kAlbumTrack)) { auto slash_pos = path.find_last_of("/"); if (slash_pos != std::pmr::string::npos && path.size() - slash_pos > 1) { - out->set(Tag::kAlbumTrack, path.substr(slash_pos + 1)); + tags->set(Tag::kAlbumTrack, path.substr(slash_pos + 1)); } } // Normalise track numbers; they're usually treated as strings, but we would // like to sort them lexicographically. - out->set(Tag::kAlbumTrack, - convert_track_number(out->at(Tag::kAlbumTrack).value_or("0"))); + tags->set(Tag::kAlbumTrack, + convert_track_number(tags->at(Tag::kAlbumTrack).value_or("0"))); { std::lock_guard lock{cache_mutex_}; - cache_.Put(path, *out); + cache_.Put(path, tags); } - return true; + return tags; } -auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path, - TrackTags* out) -> bool { +auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr { libtags::Aux aux; - aux.tags = out; + auto out = std::make_shared(); + aux.tags = out.get(); if (f_stat(path.c_str(), &aux.info) != FR_OK || f_open(&aux.file, path.c_str(), FA_READ) != FR_OK) { ESP_LOGW(kTag, "failed to open file %s", path.c_str()); - return false; + return {}; } // Fine to have this on the stack; this is only called on tasks with large // stacks anyway, due to all the string handling. @@ -205,7 +206,7 @@ auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path, if (res != 0) { // Parsing failed. ESP_LOGE(kTag, "tag parsing for %s failed, reason %d", path.c_str(), res); - return false; + return {}; } switch (ctx.format) { @@ -240,25 +241,26 @@ auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path, if (ctx.duration > 0) { out->duration = ctx.duration; } - return true; + return out; } -auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path, - TrackTags* out) -> bool { +auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path) + -> std::shared_ptr { std::pmr::string vfs_path = "/sdcard" + path; int err; OggOpusFile* f = op_test_file(vfs_path.c_str(), &err); if (f == NULL) { ESP_LOGE(kTag, "opusfile tag parsing failed: %d", err); - return false; + return {}; } const OpusTags* tags = op_tags(f, -1); if (tags == NULL) { ESP_LOGE(kTag, "no tags in opusfile"); op_free(f); - return false; + return {}; } + auto out = std::make_shared(); out->encoding(Container::kOpus); for (const auto& pair : kVorbisIdToTag) { const char* tag = opus_tags_query(tags, pair.first, 0); @@ -268,7 +270,7 @@ auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path, } op_free(f); - return true; + return out; } } // namespace database diff --git a/src/database/track.cpp b/src/database/track.cpp index a3c7dc99..f48bb8ed 100644 --- a/src/database/track.cpp +++ b/src/database/track.cpp @@ -64,12 +64,6 @@ auto TrackData::Exhume(const std::pmr::string& new_path) const -> TrackData { return TrackData(id_, new_path, tags_hash_, play_count_, false); } -void swap(Track& first, Track& second) { - Track temp = first; - first = second; - second = temp; -} - auto Track::TitleOrFilename() const -> std::pmr::string { auto title = tags().at(Tag::kTitle); if (title) { diff --git a/src/playlist/source.cpp b/src/playlist/source.cpp index 0df514e4..cf60b1c1 100644 --- a/src/playlist/source.cpp +++ b/src/playlist/source.cpp @@ -51,7 +51,7 @@ auto IndexRecordSource::Current() -> std::optional { return {}; } - return current_page_->values().at(current_item_).track(); + return current_page_->values().at(current_item_)->track(); } auto IndexRecordSource::Advance() -> std::optional { @@ -128,7 +128,7 @@ auto IndexRecordSource::Peek(std::size_t n, std::vector* out) working_item = 0; } - out->push_back(working_page->values().at(working_item).track().value()); + out->push_back(working_page->values().at(working_item)->track().value()); n--; items_added++; working_item++; diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index c4be715b..7914a5c3 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -66,6 +66,7 @@ auto Booting::entry() -> void { ESP_LOGI(kTag, "installing remaining drivers"); sServices->samd(std::unique_ptr(drivers::Samd::Create())); + vTaskDelay(pdMS_TO_TICKS(1000)); sServices->nvs( std::unique_ptr(drivers::NvsStorage::OpenSync())); sServices->touchwheel( diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 906e9e1f..e331d96f 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -7,10 +7,11 @@ idf_component_register( "wheel_encoder.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" "splash.c" "font_fusion.c" "font_symbols.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") + REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "bindey") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/ui/event_binding.cpp b/src/ui/event_binding.cpp new file mode 100644 index 00000000..ed15ccfb --- /dev/null +++ b/src/ui/event_binding.cpp @@ -0,0 +1,23 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "event_binding.hpp" + +#include "core/lv_event.h" + +namespace ui { + +static auto event_cb(lv_event_t* ev) -> void { + EventBinding* binding = + static_cast(lv_event_get_user_data(ev)); + binding->signal()(lv_event_get_target(ev)); +} + +EventBinding::EventBinding(lv_obj_t* obj, lv_event_code_t ev) { + lv_obj_add_event_cb(obj, event_cb, ev, this); +} + +} // namespace ui diff --git a/src/ui/include/event_binding.hpp b/src/ui/include/event_binding.hpp new file mode 100644 index 00000000..19514db4 --- /dev/null +++ b/src/ui/include/event_binding.hpp @@ -0,0 +1,30 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +#include "lvgl.h" + +#include "core/lv_event.h" +#include "core/lv_obj.h" +#include "nod/nod.hpp" + +namespace ui { + +class EventBinding { + public: + EventBinding(lv_obj_t* obj, lv_event_code_t ev); + + auto signal() -> nod::signal& { return signal_; } + + private: + lv_obj_t* obj_; + nod::signal signal_; +}; + +} // namespace ui diff --git a/src/ui/include/model_playback.hpp b/src/ui/include/model_playback.hpp new file mode 100644 index 00000000..f932dcfd --- /dev/null +++ b/src/ui/include/model_playback.hpp @@ -0,0 +1,26 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "bindey/property.h" + +#include "track.hpp" + +namespace ui { +namespace models { + +struct Playback { + bindey::property is_playing; + bindey::property> current_track; + bindey::property> upcoming_tracks; + + bindey::property current_track_position; + bindey::property current_track_duration; +}; + +} // namespace models +} // namespace ui \ No newline at end of file diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp index 76251a72..ac7b19f8 100644 --- a/src/ui/include/screen.hpp +++ b/src/ui/include/screen.hpp @@ -8,11 +8,15 @@ #include #include +#include +#include "bindey/binding.h" #include "core/lv_group.h" #include "core/lv_obj.h" #include "core/lv_obj_tree.h" +#include "event_binding.hpp" #include "lvgl.h" +#include "nod/nod.hpp" #include "widget_top_bar.hpp" namespace ui { @@ -51,6 +55,16 @@ class Screen { auto CreateTopBar(lv_obj_t* parent, const widgets::TopBar::Configuration&) -> widgets::TopBar*; + std::pmr::vector data_bindings_; + std::pmr::vector> event_bindings_; + + template + auto lv_bind(lv_obj_t* obj, lv_event_code_t ev, T fn) -> void { + auto binding = std::make_unique(obj, ev); + binding->signal().connect(fn); + event_bindings_.push_back(std::move(binding)); + } + lv_obj_t* const root_; lv_obj_t* content_; lv_obj_t* modal_content_; diff --git a/src/ui/include/screen_playing.hpp b/src/ui/include/screen_playing.hpp index 2e29130c..fff9cc35 100644 --- a/src/ui/include/screen_playing.hpp +++ b/src/ui/include/screen_playing.hpp @@ -11,10 +11,13 @@ #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 "screen.hpp" #include "track.hpp" #include "track_queue.hpp" @@ -28,48 +31,36 @@ namespace screens { */ class Playing : public Screen { public: - explicit Playing(std::weak_ptr db, + explicit Playing(models::Playback& playback_model, + std::weak_ptr db, audio::TrackQueue& queue); ~Playing(); auto Tick() -> void override; - // Callbacks invoked by the UI state machine in response to audio events. - - auto OnTrackUpdate() -> void; - auto OnPlaybackUpdate(uint32_t, uint32_t) -> void; - auto OnQueueUpdate() -> void; - 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*; - auto BindTrack(const database::Track& track) -> void; - auto ApplyNextUp(const std::vector& tracks) -> void; - std::weak_ptr db_; audio::TrackQueue& queue_; - std::optional track_; - std::vector next_tracks_; + bindey::property> current_track_; + bindey::property>> next_tracks_; - std::unique_ptr>> + std::unique_ptr>> new_track_; std::unique_ptr< - database::FutureFetcher>>> + database::FutureFetcher>>> new_next_tracks_; - lv_obj_t* artist_label_; - lv_obj_t* album_label_; - lv_obj_t* title_label_; - - lv_obj_t* scrubber_; - lv_obj_t* play_pause_control_; - lv_obj_t* next_up_header_; lv_obj_t* next_up_label_; lv_obj_t* next_up_hint_; diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 9980dac6..cb3e651c 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -7,13 +7,16 @@ #pragma once #include +#include #include #include #include "audio_events.hpp" #include "battery.hpp" +#include "bindey/property.h" #include "gpios.hpp" #include "lvgl_task.hpp" +#include "model_playback.hpp" #include "nvs.hpp" #include "relative_wheel.hpp" #include "screen_playing.hpp" @@ -27,6 +30,7 @@ #include "storage.hpp" #include "system_events.hpp" #include "touchwheel.hpp" +#include "track.hpp" #include "track_queue.hpp" #include "ui_events.hpp" #include "wheel_encoder.hpp" @@ -49,11 +53,11 @@ class UiState : public tinyfsm::Fsm { /* Fallback event handler. Does nothing. */ void react(const tinyfsm::Event& ev) {} - virtual void react(const system_fsm::BatteryStateChanged&); - virtual void react(const audio::PlaybackStarted&); - virtual void react(const audio::PlaybackFinished&); - virtual void react(const audio::PlaybackUpdate&) {} - virtual void react(const audio::QueueUpdate&) {} + void react(const system_fsm::BatteryStateChanged&); + void react(const audio::PlaybackStarted&); + void react(const audio::PlaybackFinished&); + void react(const audio::PlaybackUpdate&); + void react(const audio::QueueUpdate&); virtual void react(const system_fsm::KeyLockChanged&); @@ -88,6 +92,10 @@ class UiState : public tinyfsm::Fsm { static std::stack> sScreens; static std::shared_ptr sCurrentScreen; static std::shared_ptr sCurrentModal; + + static models::Playback sPlaybackModel; + + static bindey::property sPropBatteryState; }; namespace states { @@ -96,7 +104,6 @@ class Splash : public UiState { public: void exit() override; void react(const system_fsm::BootComplete&) override; - void react(const system_fsm::BatteryStateChanged&) override{}; using UiState::react; }; @@ -140,10 +147,6 @@ class Playing : public UiState { void react(const internal::BackPressed&) override; - void react(const audio::PlaybackStarted&) override; - void react(const audio::PlaybackUpdate&) override; - void react(const audio::PlaybackFinished&) override; - void react(const audio::QueueUpdate&) override; using UiState::react; }; diff --git a/src/ui/screen_playing.cpp b/src/ui/screen_playing.cpp index bd55924d..547bcf98 100644 --- a/src/ui/screen_playing.cpp +++ b/src/ui/screen_playing.cpp @@ -9,6 +9,7 @@ #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" @@ -35,6 +36,7 @@ #include "misc/lv_area.h" #include "misc/lv_color.h" #include "misc/lv_txt.h" +#include "model_playback.hpp" #include "track.hpp" #include "ui_events.hpp" #include "ui_fsm.hpp" @@ -46,8 +48,6 @@ namespace ui { namespace screens { -static constexpr std::size_t kMaxUpcoming = 10; - static void above_fold_focus_cb(lv_event_t* ev) { if (ev->user_data == NULL) { return; @@ -64,10 +64,6 @@ static void below_fold_focus_cb(lv_event_t* ev) { instance->OnFocusBelowFold(); } -static void play_pause_cb(lv_event_t* ev) { - events::Audio().Dispatch(audio::TogglePlayPause{}); -} - static lv_style_t scrubber_style; auto info_label(lv_obj_t* parent) -> lv_obj_t* { @@ -105,13 +101,42 @@ auto Playing::next_up_label(lv_obj_t* parent, const std::pmr::string& text) return button; } -Playing::Playing(std::weak_ptr db, audio::TrackQueue& queue) +Playing::Playing(models::Playback& playback_model, + std::weak_ptr db, + audio::TrackQueue& queue) : db_(db), queue_(queue), - track_(), + current_track_(), next_tracks_(), new_track_(), new_next_tracks_() { + 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; + } + new_track_.reset( + new database::FutureFetcher>( + db->GetTrack(*id))); + })); + data_bindings_.emplace_back(playback_model.upcoming_tracks.onChangedAndNow( + [=, this](const std::vector& ids) { + auto db = db_.lock(); + if (!db) { + return; + } + new_next_tracks_.reset(new database::FutureFetcher< + std::vector>>( + db->GetBulkTracks(ids))); + })); + lv_obj_set_layout(content_, LV_LAYOUT_FLEX); lv_group_set_wrap(group_, false); @@ -143,20 +168,40 @@ Playing::Playing(std::weak_ptr db, audio::TrackQueue& queue) lv_obj_set_flex_align(info_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - artist_label_ = info_label(info_container); - album_label_ = info_label(info_container); - title_label_ = info_label(info_container); + 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); - scrubber_ = lv_slider_create(above_fold_container); - lv_obj_set_size(scrubber_, lv_pct(100), 5); - lv_slider_set_range(scrubber_, 0, 100); - lv_slider_set_value(scrubber_, 0, LV_ANIM_OFF); + 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()); + })); + + lv_obj_t* scrubber = lv_slider_create(above_fold_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_obj_add_style(scrubber, &scrubber_style, LV_PART_INDICATOR); - lv_group_add_obj(group_, scrubber_); + lv_group_add_obj(group_, scrubber); + + 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* controls_container = lv_obj_create(above_fold_container); lv_obj_set_size(controls_container, lv_pct(100), 20); @@ -164,15 +209,25 @@ Playing::Playing(std::weak_ptr db, audio::TrackQueue& queue) lv_obj_set_flex_align(controls_container, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - play_pause_control_ = control_button(controls_container, LV_SYMBOL_PLAY); - lv_obj_add_event_cb(play_pause_control_, play_pause_cb, LV_EVENT_CLICKED, - NULL); - lv_group_add_obj(group_, play_pause_control_); + 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_PREV)); - lv_group_add_obj(group_, control_button(controls_container, LV_SYMBOL_NEXT)); - lv_group_add_obj(group_, - control_button(controls_container, LV_SYMBOL_SHUFFLE)); lv_group_add_obj(group_, control_button(controls_container, LV_SYMBOL_LOOP)); next_up_header_ = lv_obj_create(above_fold_container); @@ -198,111 +253,56 @@ Playing::Playing(std::weak_ptr db, audio::TrackQueue& queue) lv_obj_set_flex_align(next_up_container_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - OnTrackUpdate(); - OnQueueUpdate(); -} - -Playing::~Playing() {} + 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--; + } -auto Playing::OnTrackUpdate() -> void { - auto current = queue_.GetCurrent(); - if (!current) { - return; - } - if (track_ && track_->data().id() == *current) { - return; - } - auto db = db_.lock(); - if (!db) { - return; - } - new_track_.reset(new database::FutureFetcher>( - db->GetTrack(*current))); -} + 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_, ""); + } -auto Playing::OnPlaybackUpdate(uint32_t pos_seconds, uint32_t new_duration) - -> void { - if (!track_) { - return; - } - lv_slider_set_range(scrubber_, 0, new_duration); - lv_slider_set_value(scrubber_, pos_seconds, LV_ANIM_ON); + for (const auto& track : tracks) { + lv_group_add_obj(group_, next_up_label(next_up_container_, + track->TitleOrFilename())); + } + })); } -auto Playing::OnQueueUpdate() -> void { - OnTrackUpdate(); - auto current = queue_.GetUpcoming(kMaxUpcoming); - auto db = db_.lock(); - if (!db) { - return; - } - new_next_tracks_.reset( - new database::FutureFetcher>>( - db->GetBulkTracks(current))); -} +Playing::~Playing() {} auto Playing::Tick() -> void { if (new_track_ && new_track_->Finished()) { auto res = new_track_->Result(); new_track_.reset(); - if (res && *res) { - BindTrack(**res); + 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; + std::vector> filtered; for (const auto& t : *res) { if (t) { - filtered.push_back(*t); + filtered.push_back(t); } } - ApplyNextUp(filtered); + next_tracks_.set(filtered); } } } -auto Playing::BindTrack(const database::Track& t) -> void { - track_ = t; - - 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()); - - std::optional duration = t.tags().duration; - lv_slider_set_range(scrubber_, 0, duration.value_or(1)); - lv_slider_set_value(scrubber_, 0, LV_ANIM_OFF); -} - -auto Playing::ApplyNextUp(const std::vector& tracks) -> void { - // 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--; - } - - next_tracks_ = tracks; - - if (next_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 : next_tracks_) { - lv_group_add_obj( - group_, next_up_label(next_up_container_, track.TitleOrFilename())); - } -} - auto Playing::OnFocusAboveFold() -> void { lv_obj_scroll_to_y(content_, 0, LV_ANIM_ON); } diff --git a/src/ui/screen_track_browser.cpp b/src/ui/screen_track_browser.cpp index 6cd92a04..8d1fe653 100644 --- a/src/ui/screen_track_browser.cpp +++ b/src/ui/screen_track_browser.cpp @@ -170,8 +170,8 @@ auto TrackBrowser::AddResults( initial_page_ = results; } - auto fn = [&](const database::IndexRecord& record) { - auto text = record.text(); + auto fn = [&](const std::shared_ptr& record) { + auto text = record->text(); if (!text) { // TODO(jacqueline): Display category-specific text. text = "[ no data ]"; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index fa4939f3..d7bb9bb7 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -19,6 +19,7 @@ #include "gpios.hpp" #include "lvgl_task.hpp" #include "modal_confirm.hpp" +#include "model_playback.hpp" #include "nvs.hpp" #include "relative_wheel.hpp" #include "screen.hpp" @@ -52,6 +53,10 @@ std::stack> UiState::sScreens; std::shared_ptr UiState::sCurrentScreen; std::shared_ptr UiState::sCurrentModal; +models::Playback UiState::sPlaybackModel; + +bindey::property UiState::sPropBatteryState; + auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { // Init LVGL first, since the display driver registers itself with LVGL. lv_init(); @@ -89,15 +94,33 @@ void UiState::react(const system_fsm::KeyLockChanged& ev) { } void UiState::react(const system_fsm::BatteryStateChanged&) { - UpdateTopBar(); + if (!sServices) { + return; + } + auto state = sServices->battery().State(); + if (state) { + sPropBatteryState.set(*state); + } } void UiState::react(const audio::PlaybackStarted&) { - UpdateTopBar(); + sPlaybackModel.is_playing.set(true); } void UiState::react(const audio::PlaybackFinished&) { - UpdateTopBar(); + sPlaybackModel.is_playing.set(false); +} + +void UiState::react(const audio::PlaybackUpdate& ev) { + sPlaybackModel.current_track_duration.set(ev.seconds_total); + sPlaybackModel.current_track_position.set(ev.seconds_elapsed); +} + +void UiState::react(const audio::QueueUpdate&) { + ESP_LOGI(kTag, "current changed!"); + auto& queue = sServices->track_queue(); + sPlaybackModel.current_track.set(queue.GetCurrent()); + sPlaybackModel.upcoming_tracks.set(queue.GetUpcoming(10)); } void UiState::UpdateTopBar() { @@ -283,21 +306,22 @@ void Browse::react(const internal::RecordSelected& ev) { } auto record = ev.page->values().at(ev.record); - if (record.track()) { - ESP_LOGI(kTag, "selected track '%s'", record.text()->c_str()); + if (record->track()) { + ESP_LOGI(kTag, "selected track '%s'", record->text()->c_str()); auto& queue = sServices->track_queue(); queue.Clear(); queue.IncludeLast(std::make_shared( sServices->database(), ev.initial_page, 0, ev.page, ev.record)); + ESP_LOGI(kTag, "transit to playing"); transit(); } else { - ESP_LOGI(kTag, "selected record '%s'", record.text()->c_str()); - auto cont = record.Expand(kRecordsPerPage); + ESP_LOGI(kTag, "selected record '%s'", record->text()->c_str()); + auto cont = record->Expand(kRecordsPerPage); if (!cont) { return; } auto query = db->GetPage(&cont.value()); - std::pmr::string title = record.text().value_or("TODO"); + std::pmr::string title = record->text().value_or("TODO"); PushScreen(std::make_shared( sServices->database(), title, std::move(query))); } @@ -329,8 +353,9 @@ void Browse::react(const system_fsm::BluetoothDevicesChanged&) { static std::shared_ptr sPlayingScreen; void Playing::entry() { - sPlayingScreen.reset( - new screens::Playing(sServices->database(), sServices->track_queue())); + ESP_LOGI(kTag, "push playing screen"); + sPlayingScreen.reset(new screens::Playing( + sPlaybackModel, sServices->database(), sServices->track_queue())); PushScreen(sPlayingScreen); } @@ -339,24 +364,6 @@ void Playing::exit() { PopScreen(); } -void Playing::react(const audio::PlaybackStarted& ev) { - UpdateTopBar(); - sPlayingScreen->OnTrackUpdate(); -} - -void Playing::react(const audio::PlaybackFinished& ev) { - UpdateTopBar(); - sPlayingScreen->OnTrackUpdate(); -} - -void Playing::react(const audio::PlaybackUpdate& ev) { - sPlayingScreen->OnPlaybackUpdate(ev.seconds_elapsed, ev.seconds_total); -} - -void Playing::react(const audio::QueueUpdate& ev) { - sPlayingScreen->OnQueueUpdate(); -} - void Playing::react(const internal::BackPressed& ev) { transit(); } -- cgit v1.2.3