summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2023-09-28 08:29:55 +1000
committerjacqueline <me@jacqueline.id.au>2023-09-28 08:29:55 +1000
commitf09ba5ffd53bf7d28e0dc516c00a8f69ca7efae9 (patch)
treeaffce5567186d8944686afd824bf4ee4f7ee4d2d
parentf168bfab7698f28492c7693263525945a26cbcc8 (diff)
downloadtangara-fw-f09ba5ffd53bf7d28e0dc516c00a8f69ca7efae9.tar.gz
Use bindey for databinding instead of hand rolling ui updates
-rw-r--r--lib/bindey/.clang-format72
-rw-r--r--lib/bindey/.github/workflows/ci.yml44
-rw-r--r--lib/bindey/.gitignore5
-rw-r--r--lib/bindey/.gitmodules6
-rw-r--r--lib/bindey/CMakeLists.txt6
-rw-r--r--lib/bindey/LICENSE.md21
-rw-r--r--lib/bindey/README.md116
-rw-r--r--lib/bindey/include/bindey/binding.h47
-rw-r--r--lib/bindey/include/bindey/property.h137
-rw-r--r--lib/bindey/include/nod/nod.hpp681
-rw-r--r--src/app_console/app_console.cpp20
-rw-r--r--src/audio/audio_decoder.cpp1
-rw-r--r--src/audio/fatfs_audio_input.cpp7
-rw-r--r--src/audio/track_queue.cpp5
-rw-r--r--src/battery/include/battery.hpp4
-rw-r--r--src/database/database.cpp104
-rw-r--r--src/database/include/database.hpp24
-rw-r--r--src/database/include/records.hpp2
-rw-r--r--src/database/include/tag_parser.hpp18
-rw-r--r--src/database/include/track.hpp43
-rw-r--r--src/database/records.cpp4
-rw-r--r--src/database/tag_parser.cpp50
-rw-r--r--src/database/track.cpp6
-rw-r--r--src/playlist/source.cpp4
-rw-r--r--src/system_fsm/booting.cpp1
-rw-r--r--src/ui/CMakeLists.txt3
-rw-r--r--src/ui/event_binding.cpp23
-rw-r--r--src/ui/include/event_binding.hpp30
-rw-r--r--src/ui/include/model_playback.hpp26
-rw-r--r--src/ui/include/screen.hpp14
-rw-r--r--src/ui/include/screen_playing.hpp33
-rw-r--r--src/ui/include/ui_fsm.hpp23
-rw-r--r--src/ui/screen_playing.cpp214
-rw-r--r--src/ui/screen_track_browser.cpp4
-rw-r--r--src/ui/ui_fsm.cpp63
-rw-r--r--tools/cmake/common.cmake1
36 files changed, 1556 insertions, 306 deletions
diff --git a/lib/bindey/.clang-format b/lib/bindey/.clang-format
new file mode 100644
index 00000000..c3b36785
--- /dev/null
+++ b/lib/bindey/.clang-format
@@ -0,0 +1,72 @@
+AccessModifierOffset: -4
+AlignAfterOpenBracket: Align
+AlignConsecutiveAssignments: true
+AlignConsecutiveDeclarations: true
+AlignEscapedNewlinesLeft: true
+AlignOperands: true
+AlignTrailingComments: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortBlocksOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: true
+BinPackArguments: false
+BinPackParameters: false
+BraceWrapping:
+ AfterClass: true
+ AfterControlStatement: true
+ AfterEnum: true
+ AfterFunction: true
+ AfterNamespace: true
+ AfterObjCDeclaration: true
+ AfterStruct: true
+ AfterUnion: true
+ BeforeCatch: true
+ BeforeElse: true
+ IndentBraces: false
+BreakBeforeBinaryOperators: NonAssignment
+BreakBeforeBraces: Custom
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializersBeforeComma: true
+ColumnLimit: 120
+CommentPragmas: '^!'
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 0
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+DisableFormat: false
+ExperimentalAutoDetectBinPacking: false
+ForEachMacros: [ foreach, BOOST_FOREACH ]
+IndentCaseLabels: true
+IndentFunctionDeclarationAfterType: false
+IndentWidth: 4
+IndentWrappedFunctionNames: false
+KeepEmptyLinesAtTheStartOfBlocks: true
+Language: Cpp
+MaxEmptyLinesToKeep: 2
+NamespaceIndentation: None
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 600
+PointerAlignment: Left
+SpaceAfterCStyleCast: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeParens: ControlStatements
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: false
+SpacesInContainerLiterals: true
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: true
+SpacesInSquareBrackets: false
+Standard: Cpp11
+TabWidth: 4
+UseTab: Never
diff --git a/lib/bindey/.github/workflows/ci.yml b/lib/bindey/.github/workflows/ci.yml
new file mode 100644
index 00000000..060f14e4
--- /dev/null
+++ b/lib/bindey/.github/workflows/ci.yml
@@ -0,0 +1,44 @@
+name: ci
+
+on: [pull_request]
+
+env:
+ # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
+ BUILD_TYPE: Release
+
+jobs:
+ Build-And-Test:
+ runs-on: macos-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ submodules: true
+
+ - name: Create Build Environment
+ # Some projects don't allow in-source building, so create a separate build directory
+ # We'll use this as our working directory for all subsequent commands
+ run: cmake -E make_directory ${{runner.workspace}}/build
+
+ - name: Configure
+ shell: bash
+ working-directory: ${{runner.workspace}}/build
+ run: cmake $GITHUB_WORKSPACE -GXcode -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DBINDEY_BUILD_TESTS=ON
+ env:
+ CC: clang
+ CXX: clang
+
+ - name: Build
+ working-directory: ${{runner.workspace}}/build
+ shell: bash
+ run: cmake --build . --config $BUILD_TYPE
+ env:
+ CC: clang
+ CXX: clang
+
+ - name: Test
+ working-directory: ${{runner.workspace}}/build
+ shell: bash
+ # Execute tests defined by the CMake configuration.
+ # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail
+ run: ctest -C $BUILD_TYPE
diff --git a/lib/bindey/.gitignore b/lib/bindey/.gitignore
new file mode 100644
index 00000000..92b4043c
--- /dev/null
+++ b/lib/bindey/.gitignore
@@ -0,0 +1,5 @@
+*.a
+*.lib
+*.o
+*.pdb
+.DS_Store
diff --git a/lib/bindey/.gitmodules b/lib/bindey/.gitmodules
new file mode 100644
index 00000000..67f59dd4
--- /dev/null
+++ b/lib/bindey/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "lib/Catch2"]
+ path = lib/Catch2
+ url = git@github.com:catchorg/Catch2.git
+[submodule "lib/nod"]
+ path = lib/nod
+ url = git@github.com:fr00b0/nod.git
diff --git a/lib/bindey/CMakeLists.txt b/lib/bindey/CMakeLists.txt
new file mode 100644
index 00000000..f71c176a
--- /dev/null
+++ b/lib/bindey/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright 2023 jacqueline <me@jacqueline.id.au>
+#
+# SPDX-License-Identifier: GPL-3.0-only
+idf_component_register(
+ INCLUDE_DIRS "include"
+)
diff --git a/lib/bindey/LICENSE.md b/lib/bindey/LICENSE.md
new file mode 100644
index 00000000..5ba17155
--- /dev/null
+++ b/lib/bindey/LICENSE.md
@@ -0,0 +1,21 @@
+## The MIT License (MIT)
+
+Copyright (c) 2021 Kevin Dixon
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/lib/bindey/README.md b/lib/bindey/README.md
new file mode 100644
index 00000000..0ef0e62b
--- /dev/null
+++ b/lib/bindey/README.md
@@ -0,0 +1,116 @@
+# bindey
+
+Everyone knows Model-View-ViewModel is the best architecture, but how can we realize it in C++ applications with minimal overhead, and no complicated framework impositions?
+
+`bindey` provides the basic building block of MVVM -- an observable "Property" and a databinding mechanism.
+
+## Property Usage
+
+At minimum, `bindey::property` can allow you to avoid writing getters and setters. Consider this example:
+
+```
+#include <bindey/property.h>
+
+using namespace bindey;
+
+class Person
+{
+public:
+ property<std::string> name;
+ property<int> age;
+};
+```
+Then we can use it like this:
+```
+Person p;
+p.name("Kevin");
+p.age(666);
+
+auto thatDudesName = p.name();
+auto ageIsJustANumber = p.age();
+```
+
+`property` default initializes its value with `{}`, and of course allows initialization.
+```
+Person::Person()
+: name("Default Name")
+, age(0)
+{}
+```
+## Data Binding
+`bindey` provides a simple binding mechanism to connect a "source" `property` to an arbitrary object. This base signature is
+```
+template <typename T, typename To>
+binding bind( property<T>& from, To& to );
+```
+And a specialization for `property` to `property` binding of the same type is provided.
+```
+template<typename T>
+binding bind( property<T>& from, property<T>& to )
+{
+ return from.onChanged( [&]( const auto& newValue ) { to( newValue ); } );
+}
+```
+
+### Writing Your Own Bindings
+Where this becomes fun is when you get to reduce boilerplate. For example, assume a `Button` class from some UI Framework.
+```
+struct Button
+{
+ void setText(const std::string& text)
+ {
+ this->text = text;
+ }
+
+ std::string text;
+};
+```
+To make your life better, simply implement a template speciailization in the `bindey` namespace.
+```
+namespace bindey
+{
+template <>
+binding bind( property<std::string>& from, Button& to )
+{
+ return from.onChanged( [&]( const auto& newValue ){ to.setText( newValue ); } );
+}
+} // namespace bindey
+```
+Then, bind your property to the button as needed:
+```
+bindey::property<std::string> name;
+...
+Button someButton;
+...
+bindey::bind( name, someButton );
+```
+
+### Binding Lifetimes
+The result of a call to `bind` is a `bindey::binding` object. If this return value is discarded, then the binding's lifetime is coupled to the `property`'s.
+
+Otherwise, this token can be used to disconnect the binding as needed, the easiest way is to capture it in a `scoped_binding` object.
+
+For example, if your binding involves objects who's lifetime you do not control, you should certainly capture the binding to avoid crashes.
+```
+struct GreatObj
+{
+ GreatObj(Button* b)
+ {
+ mSomeButton = b;
+ mButtonBinding = bindey::bind( name, *mSomeButton );
+ }
+
+ void updateButton(Button* newB)
+ {
+ mSomeButton = nullptr;
+ mButtonBinding = {}; // disconnect from old button
+ if( newB != nullptr )
+ {
+ mSomeButton = newB;
+ mButtonBinding = bindey::bind( name, *mSomeButton );
+ }
+ }
+
+ bindey::scoped_binding mButtonBinding;
+};
+```
diff --git a/lib/bindey/include/bindey/binding.h b/lib/bindey/include/bindey/binding.h
new file mode 100644
index 00000000..69baa5cf
--- /dev/null
+++ b/lib/bindey/include/bindey/binding.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "property.h"
+
+#include <nod/nod.hpp>
+
+#include <functional>
+#include <type_traits>
+
+namespace bindey
+{
+
+using binding = nod::connection;
+using scoped_binding = nod::scoped_connection;
+
+/**
+ * base binding signature
+ */
+template <typename T, typename To>
+binding bind( property<T>& from, To& to );
+
+/**
+ * binds two properties of the same type
+ */
+template <typename T>
+binding bind( property<T>& from, property<T>& to )
+{
+ return from.onChanged( [&]( const auto& newValue ) { to( newValue ); } );
+}
+
+/**
+ * binds two properties of differing types using a Converter callable
+ * @param from property to observe
+ * @param to property to write to
+ * @param bindingConverter a callable to invoke to convert between the types
+ */
+template <typename TFrom, typename TTo, typename Converter>
+binding bind( property<TFrom>& from, property<TTo>& to, Converter&& bindingConverter )
+{
+ static_assert( std::is_convertible<Converter&&, std::function<TTo( const TFrom& )>>::value,
+ "Wrong Signature for binding converter!" );
+
+ return from.onChanged(
+ [&to, converter = bindingConverter]( const auto& newValue ) { to( converter( newValue ) ); } );
+}
+
+} // namespace bindey
diff --git a/lib/bindey/include/bindey/property.h b/lib/bindey/include/bindey/property.h
new file mode 100644
index 00000000..369f01f0
--- /dev/null
+++ b/lib/bindey/include/bindey/property.h
@@ -0,0 +1,137 @@
+#pragma once
+
+#include <nod/nod.hpp>
+
+#include <functional>
+
+namespace bindey
+{
+
+/**
+ * Optional always_update policy to notify subscribers everytime the property value is set, not just when it changes
+ */
+class always_update
+{
+public:
+ template <typename T>
+ bool operator()( const T&, const T& ) const
+ {
+ return true;
+ }
+};
+
+template <typename T,
+ typename UpdatePolicy = std::not_equal_to<T>,
+ typename Signal = nod::unsafe_signal<void( const T& )>>
+class property
+{
+public:
+ property()
+ {
+ }
+ property( T&& value )
+ : mStorage( std::move( value ) )
+ {
+ }
+ property( const property& ) = delete;
+ property& operator=(const property&) = delete;
+
+
+ /**
+ * gets the current value
+ * @return const reference to the value
+ */
+ const T& get() const
+ {
+ return mStorage;
+ }
+
+ /**
+ * gets the current value
+ * @return mutable reference to the value
+ */
+ T& get()
+ {
+ return mStorage;
+ }
+
+ const T& operator()() const
+ {
+ return get();
+ }
+
+ T& operator()()
+ {
+ return get();
+ }
+
+ /**
+ * sets the value of the property.
+ * @param value the new value
+ * @discussion the value will only be updated if the UpdatePolicy's critera is met.
+ * if the value is changed, then the @ref changed event will be fired.
+ */
+ void set( const T& value )
+ {
+ if ( UpdatePolicy{}( mStorage, value ) )
+ {
+ mStorage = value;
+ changed( mStorage );
+ }
+ }
+
+ void set( T&& value )
+ {
+ if ( UpdatePolicy{}( mStorage, value ) )
+ {
+ mStorage = std::move( value );
+ changed( mStorage );
+ }
+ }
+
+ void operator()( const T& value )
+ {
+ set( value );
+ }
+
+ void operator()( T&& value )
+ {
+ set( std::move( value ) );
+ }
+
+ /**
+ * this signal is invoked whenever the the value changes per the UpdatePolicy
+ * @discussion nod::unsafe_signal is used here for speed. Take care of your own threading.
+ */
+ Signal changed;
+
+ /**
+ * convience function to attach a change listener to this property
+ */
+ auto onChanged( typename decltype( changed )::slot_type&& c )
+ {
+ return changed.connect( std::move( c ) );
+ }
+
+ /**
+ * convience function to attach a change listener to this property and call it right away
+ */
+ auto onChangedAndNow( typename decltype( changed )::slot_type&& c )
+ {
+ auto connection = onChanged( std::move( c ) );
+ changed( mStorage );
+ return connection;
+ }
+
+
+private:
+ T mStorage{};
+};
+
+/**
+ * thread safe property type based on nod::signal
+ */
+template <typename T, typename UpdatePolicy = std::not_equal_to<T>>
+using safe_property = property<T, UpdatePolicy, nod::signal<void( const T& )>>;
+
+} // namespace bindey
diff --git a/lib/bindey/include/nod/nod.hpp b/lib/bindey/include/nod/nod.hpp
new file mode 100644
index 00000000..68e7b8d4
--- /dev/null
+++ b/lib/bindey/include/nod/nod.hpp
@@ -0,0 +1,681 @@
+#ifndef IG_NOD_INCLUDE_NOD_HPP
+#define IG_NOD_INCLUDE_NOD_HPP
+
+#include <vector> // std::vector
+#include <functional> // std::function
+#include <mutex> // std::mutex, std::lock_guard
+#include <memory> // std::shared_ptr, std::weak_ptr
+#include <algorithm> // std::find_if()
+#include <cassert> // assert()
+#include <thread> // std::this_thread::yield()
+#include <type_traits> // std::is_same
+#include <iterator> // std::back_inserter
+
+namespace nod {
+ // implementational details
+ namespace detail {
+ /// Interface for type erasure when disconnecting slots
+ struct disconnector {
+ virtual ~disconnector() {}
+ virtual void operator()( std::size_t index ) const = 0;
+ };
+ /// Deleter that doesn't delete
+ inline void no_delete(disconnector*){
+ };
+ } // namespace detail
+
+ /// Base template for the signal class
+ template <class P, class T>
+ class signal_type;
+
+
+ /// Connection class.
+ ///
+ /// This is used to be able to disconnect slots after they have been connected.
+ /// Used as return type for the connect method of the signals.
+ ///
+ /// Connections are default constructible.
+ /// Connections are not copy constructible or copy assignable.
+ /// Connections are move constructible and move assignable.
+ ///
+ class connection {
+ public:
+ /// Default constructor
+ connection() :
+ _index()
+ {}
+
+ // Connection are not copy constructible or copy assignable
+ connection( connection const& ) = delete;
+ connection& operator=( connection const& ) = delete;
+
+ /// Move constructor
+ /// @param other The instance to move from.
+ connection( connection&& other ) :
+ _weak_disconnector( std::move(other._weak_disconnector) ),
+ _index( other._index )
+ {}
+
+ /// Move assign operator.
+ /// @param other The instance to move from.
+ connection& operator=( connection&& other ) {
+ _weak_disconnector = std::move( other._weak_disconnector );
+ _index = other._index;
+ return *this;
+ }
+
+ /// @returns `true` if the connection is connected to a signal object,
+ /// and `false` otherwise.
+ bool connected() const {
+ return !_weak_disconnector.expired();
+ }
+
+ /// Disconnect the slot from the connection.
+ ///
+ /// If the connection represents a slot that is connected to a signal object, calling
+ /// this method will disconnect the slot from that object. The result of this operation
+ /// is that the slot will stop receiving calls when the signal is invoked.
+ void disconnect();
+
+ private:
+ /// The signal template is a friend of the connection, since it is the
+ /// only one allowed to create instances using the meaningful constructor.
+ template<class P,class T> friend class signal_type;
+
+ /// Create a connection.
+ /// @param shared_disconnector Disconnector instance that will be used to disconnect
+ /// the connection when the time comes. A weak pointer
+ /// to the disconnector will be held within the connection
+ /// object.
+ /// @param index The slot index of the connection.
+ connection( std::shared_ptr<detail::disconnector> const& shared_disconnector, std::size_t index ) :
+ _weak_disconnector( shared_disconnector ),
+ _index( index )
+ {}
+
+ /// Weak pointer to the current disconnector functor.
+ std::weak_ptr<detail::disconnector> _weak_disconnector;
+ /// Slot index of the connected slot.
+ std::size_t _index;
+ };
+
+ /// Scoped connection class.
+ ///
+ /// This type of connection is automatically disconnected when
+ /// the connection object is destructed.
+ ///
+ class scoped_connection
+ {
+ public:
+ /// Scoped are default constructible
+ scoped_connection() = default;
+ /// Scoped connections are not copy constructible
+ scoped_connection( scoped_connection const& ) = delete;
+ /// Scoped connections are not copy assingable
+ scoped_connection& operator=( scoped_connection const& ) = delete;
+
+ /// Move constructor
+ scoped_connection( scoped_connection&& other ) :
+ _connection( std::move(other._connection) )
+ {}
+
+ /// Move assign operator.
+ /// @param other The instance to move from.
+ scoped_connection& operator=( scoped_connection&& other ) {
+ reset( std::move( other._connection ) );
+ return *this;
+ }
+
+ /// Construct a scoped connection from a connection object
+ /// @param connection The connection object to manage
+ scoped_connection( connection&& c ) :
+ _connection( std::forward<connection>(c) )
+ {}
+
+ /// destructor
+ ~scoped_connection() {
+ disconnect();
+ }
+
+ /// Assignment operator moving a new connection into the instance.
+ /// @note If the scoped_connection instance already contains a
+ /// connection, that connection will be disconnected as if
+ /// the scoped_connection was destroyed.
+ /// @param c New connection to manage
+ scoped_connection& operator=( connection&& c ) {
+ reset( std::forward<connection>(c) );
+ return *this;
+ }
+
+ /// Reset the underlying connection to another connection.
+ /// @note The connection currently managed by the scoped_connection
+ /// instance will be disconnected when resetting.
+ /// @param c New connection to manage
+ void reset( connection&& c = {} ) {
+ disconnect();
+ _connection = std::move(c);
+ }
+
+ /// Release the underlying connection, without disconnecting it.
+ /// @returns The newly released connection instance is returned.
+ connection release() {
+ connection c = std::move(_connection);
+ _connection = connection{};
+ return c;
+ }
+
+ ///
+ /// @returns `true` if the connection is connected to a signal object,
+ /// and `false` otherwise.
+ bool connected() const {
+ return _connection.connected();
+ }
+
+ /// Disconnect the slot from the connection.
+ ///
+ /// If the connection represents a slot that is connected to a signal object, calling
+ /// this method will disconnect the slot from that object. The result of this operation
+ /// is that the slot will stop receiving calls when the signal is invoked.
+ void disconnect() {
+ _connection.disconnect();
+ }
+
+ private:
+ /// Underlying connection object
+ connection _connection;
+ };
+
+ /// Policy for multi threaded use of signals.
+ ///
+ /// This policy provides mutex and lock types for use in
+ /// a multithreaded environment, where signals and slots
+ /// may exists in different threads.
+ ///
+ /// This policy is used in the `nod::signal` type provided
+ /// by the library.
+ struct multithread_policy
+ {
+ using mutex_type = std::mutex;
+ using mutex_lock_type = std::unique_lock<mutex_type>;
+ /// Function that yields the current thread, allowing
+ /// the OS to reschedule.
+ static void yield_thread() {
+ std::this_thread::yield();
+ }
+ /// Function that defers a lock to a lock function that prevents deadlock
+ static mutex_lock_type defer_lock(mutex_type & m){
+ return mutex_lock_type{m, std::defer_lock};
+ }
+ /// Function that locks two mutexes and prevents deadlock
+ static void lock(mutex_lock_type & a,mutex_lock_type & b) {
+ std::lock(a,b);
+ }
+ };
+
+ /// Policy for single threaded use of signals.
+ ///
+ /// This policy provides dummy implementations for mutex
+ /// and lock types, resulting in that no synchronization
+ /// will take place.
+ ///
+ /// This policy is used in the `nod::unsafe_signal` type
+ /// provided by the library.
+ struct singlethread_policy
+ {
+ /// Dummy mutex type that doesn't do anything
+ struct mutex_type{};
+ /// Dummy lock type, that doesn't do any locking.
+ struct mutex_lock_type
+ {
+ /// A lock type must be constructible from a
+ /// mutex type from the same thread policy.
+ explicit mutex_lock_type( mutex_type const& ) {
+ }
+ };
+ /// Dummy implementation of thread yielding, that
+ /// doesn't do any actual yielding.
+ static void yield_thread() {
+ }
+ /// Dummy implemention of defer_lock that doesn't
+ /// do anything
+ static mutex_lock_type defer_lock(mutex_type &m){
+ return mutex_lock_type{m};
+ }
+ /// Dummy implemention of lock that doesn't
+ /// do anything
+ static void lock(mutex_lock_type &,mutex_lock_type &) {
+ }
+ };
+
+ /// Signal accumulator class template.
+ ///
+ /// This acts sort of as a proxy for triggering a signal and
+ /// accumulating the slot return values.
+ ///
+ /// This class is not really intended to instantiate by client code.
+ /// Instances are aquired as return values of the method `accumulate()`
+ /// called on signals.
+ ///
+ /// @tparam S Type of signal. The signal_accumulator acts
+ /// as a type of proxy for a signal instance of
+ /// this type.
+ /// @tparam T Type of initial value of the accumulate algorithm.
+ /// This type must meet the requirements of `CopyAssignable`
+ /// and `CopyConstructible`
+ /// @tparam F Type of accumulation function.
+ /// @tparam A... Argument types of the underlying signal type.
+ ///
+ template <class S, class T, class F, class...A>
+ class signal_accumulator
+ {
+ public:
+ /// Result type when calling the accumulating function operator.
+ using result_type = typename std::result_of<F(T, typename S::slot_type::result_type)>::type;
+
+ /// Construct a signal_accumulator as a proxy to a given signal
+ //
+ /// @param signal Signal instance.
+ /// @param init Initial value of the accumulate algorithm.
+ /// @param func Binary operation function object that will be
+ /// applied to all slot return values.
+ /// The signature of the function should be
+ /// equivalent of the following:
+ /// `R func( T1 const& a, T2 const& b )`
+ /// - The signature does not need to have `const&`.
+ /// - The initial value, type `T`, must be implicitly
+ /// convertible to `R`
+ /// - The return type `R` must be implicitly convertible
+ /// to type `T1`.
+ /// - The type `R` must be `CopyAssignable`.
+ /// - The type `S::slot_type::result_type` (return type of
+ /// the signals slots) must be implicitly convertible to
+ /// type `T2`.
+ signal_accumulator( S const& signal, T init, F func ) :
+ _signal( signal ),
+ _init( init ),
+ _func( func )
+ {}
+
+ /// Function call operator.
+ ///
+ /// Calling this will trigger the underlying signal and accumulate
+ /// all of the connected slots return values with the current
+ /// initial value and accumulator function.
+ ///
+ /// When called, this will invoke the accumulator function will
+ /// be called for each return value of the slots. The semantics
+ /// are similar to the `std::accumulate` algorithm.
+ ///
+ /// @param args Arguments to propagate to the slots of the
+ /// underlying when triggering the signal.
+ result_type operator()( A const& ... args ) const {
+ return _signal.trigger_with_accumulator( _init, _func, args... );
+ }
+
+ private:
+
+ /// Reference to the underlying signal to proxy.
+ S const& _signal;
+ /// Initial value of the accumulate algorithm.
+ T _init;
+ /// Accumulator function.
+ F _func;
+
+ };
+
+ /// Signal template specialization.
+ ///
+ /// This is the main signal implementation, and it is used to
+ /// implement the observer pattern whithout the overhead
+ /// boilerplate code that typically comes with it.
+ ///
+ /// Any function or function object is considered a slot, and
+ /// can be connected to a signal instance, as long as the signature
+ /// of the slot matches the signature of the signal.
+ ///
+ /// @tparam P Threading policy for the signal.
+ /// A threading policy must provide two type definitions:
+ /// - P::mutex_type, this type will be used as a mutex
+ /// in the signal_type class template.
+ /// - P::mutex_lock_type, this type must implement a
+ /// constructor that takes a P::mutex_type as a parameter,
+ /// and it must have the semantics of a scoped mutex lock
+ /// like std::lock_guard, i.e. locking in the constructor
+ /// and unlocking in the destructor.
+ ///
+ /// @tparam R Return value type of the slots connected to the signal.
+ /// @tparam A... Argument types of the slots connected to the signal.
+ template <class P, class R, class... A >
+ class signal_type<P,R(A...)>
+ {
+ public:
+ /// signals are not copy constructible
+ signal_type( signal_type const& ) = delete;
+ /// signals are not copy assignable
+ signal_type& operator=( signal_type const& ) = delete;
+ /// signals are move constructible
+ signal_type(signal_type&& other)
+ {
+ mutex_lock_type lock{other._mutex};
+ _slot_count = std::move(other._slot_count);
+ _slots = std::move(other._slots);
+ if(other._shared_disconnector != nullptr)
+ {
+ _disconnector = disconnector{ this };
+ _shared_disconnector = std::move(other._shared_disconnector);
+ // replace the disconnector with our own disconnector
+ *static_cast<disconnector*>(_shared_disconnector.get()) = _disconnector;
+ }
+ }
+ /// signals are move assignable
+ signal_type& operator=(signal_type&& other)
+ {
+ auto lock = thread_policy::defer_lock(_mutex);
+ auto other_lock = thread_policy::defer_lock(other._mutex);
+ thread_policy::lock(lock,other_lock);
+
+ _slot_count = std::move(other._slot_count);
+ _slots = std::move(other._slots);
+ if(other._shared_disconnector != nullptr)
+ {
+ _disconnector = disconnector{ this };
+ _shared_disconnector = std::move(other._shared_disconnector);
+ // replace the disconnector with our own disconnector
+ *static_cast<disconnector*>(_shared_disconnector.get()) = _disconnector;
+ }
+ return *this;
+ }
+
+ /// signals are default constructible
+ signal_type() :
+ _slot_count(0)
+ {}
+
+ // Destruct the signal object.
+ ~signal_type() {
+ invalidate_disconnector();
+ }
+
+ /// Type that will be used to store the slots for this signal type.
+ using slot_type = std::function<R(A...)>;
+ /// Type that is used for counting the slots connected to this signal.
+ using size_type = typename std::vector<slot_type>::size_type;
+
+
+ /// Connect a new slot to the signal.
+ ///
+ /// The connected slot will be called every time the signal
+ /// is triggered.
+ /// @param slot The slot to connect. This must be a callable with
+ /// the same signature as the signal itself.
+ /// @return A connection object is returned, and can be used to
+ /// disconnect the slot.
+ template <class T>
+ connection connect( T&& slot ) {
+ mutex_lock_type lock{ _mutex };
+ _slots.push_back( std::forward<T>(slot) );
+ std::size_t index = _slots.size()-1;
+ if( _shared_disconnector == nullptr ) {
+ _disconnector = disconnector{ this };
+ _shared_disconnector = std::shared_ptr<detail::disconnector>{&_disconnector, detail::no_delete};
+ }
+ ++_slot_count;
+ return connection{ _shared_disconnector, index };
+ }
+
+ /// Function call operator.
+ ///
+ /// Calling this is how the signal is triggered and the
+ /// connected slots are called.
+ ///
+ /// @note The slots will be called in the order they were
+ /// connected to the signal.
+ ///
+ /// @param args Arguments that will be propagated to the
+ /// connected slots when they are called.
+ void operator()( A const&... args ) const {
+ for( auto const& slot : copy_slots() ) {
+ if( slot ) {
+ slot( args... );
+ }
+ }
+ }
+
+ /// Construct a accumulator proxy object for the signal.
+ ///
+ /// The intended purpose of this function is to create a function
+ /// object that can be used to trigger the signal and accumulate
+ /// all the slot return values.
+ ///
+ /// The algorithm used to accumulate slot return values is similar
+ /// to `std::accumulate`. A given binary function is called for
+ /// each return value with the parameters consisting of the
+ /// return value of the accumulator function applied to the
+ /// previous slots return value, and the current slots return value.
+ /// A initial value must be provided for the first slot return type.
+ ///
+ /// @note This can only be used on signals that have slots with
+ /// non-void return types, since we can't accumulate void
+ /// values.
+ ///
+ /// @tparam T The type of the initial value given to the accumulator.
+ /// @tparam F The accumulator function type.
+ /// @param init Initial value given to the accumulator.
+ /// @param op Binary operator function object to apply by the accumulator.
+ /// The signature of the function should be
+ /// equivalent of the following:
+ /// `R func( T1 const& a, T2 const& b )`
+ /// - The signature does not need to have `const&`.
+ /// - The initial value, type `T`, must be implicitly
+ /// convertible to `R`
+ /// - The return type `R` must be implicitly convertible
+ /// to type `T1`.
+ /// - The type `R` must be `CopyAssignable`.
+ /// - The type `S::slot_type::result_type` (return type of
+ /// the signals slots) must be implicitly convertible to
+ /// type `T2`.
+ template <class T, class F>
+ signal_accumulator<signal_type, T, F, A...> accumulate( T init, F op ) const {
+ static_assert( std::is_same<R,void>::value == false, "Unable to accumulate slot return values with 'void' as return type." );
+ return { *this, init, op };
+ }
+
+
+ /// Trigger the signal, calling the slots and aggregate all
+ /// the slot return values into a container.
+ ///
+ /// @tparam C The type of container. This type must be
+ /// `DefaultConstructible`, and usable with
+ /// `std::back_insert_iterator`. Additionally it
+ /// must be either copyable or moveable.
+ /// @param args The arguments to propagate to the slots.
+ template <class C>
+ C aggregate( A const&... args ) const {
+ static_assert( std::is_same<R,void>::value == false, "Unable to aggregate slot return values with 'void' as return type." );
+ C container;
+ auto iterator = std::back_inserter( container );
+ for( auto const& slot : copy_slots() ) {
+ if( slot ) {
+ (*iterator) = slot( args... );
+ }
+ }
+ return container;
+ }
+
+ /// Count the number of slots connected to this signal
+ /// @returns The number of connected slots
+ size_type slot_count() const {
+ return _slot_count;
+ }
+
+ /// Determine if the signal is empty, i.e. no slots are connected
+ /// to it.
+ /// @returns `true` is returned if the signal has no connected
+ /// slots, and `false` otherwise.
+ bool empty() const {
+ return slot_count() == 0;
+ }
+
+ /// Disconnects all slots
+ /// @note This operation invalidates all scoped_connection objects
+ void disconnect_all_slots() {
+ mutex_lock_type lock{ _mutex };
+ _slots.clear();
+ _slot_count = 0;
+ invalidate_disconnector();
+ }
+
+ private:
+ template<class, class, class, class...> friend class signal_accumulator;
+ /// Thread policy currently in use
+ using thread_policy = P;
+ /// Type of mutex, provided by threading policy
+ using mutex_type = typename thread_policy::mutex_type;
+ /// Type of mutex lock, provided by threading policy
+ using mutex_lock_type = typename thread_policy::mutex_lock_type;
+
+ /// Invalidate the internal disconnector object in a way
+ /// that is safe according to the current thread policy.
+ ///
+ /// This will effectively make all current connection objects to
+ /// to this signal incapable of disconnecting, since they keep a
+ /// weak pointer to the shared disconnector object.
+ void invalidate_disconnector() {
+ // If we are unlucky, some of the connected slots
+ // might be in the process of disconnecting from other threads.
+ // If this happens, we are risking to destruct the disconnector
+ // object managed by our shared pointer before they are done
+ // disconnecting. This would be bad. To solve this problem, we
+ // discard the shared pointer (that is pointing to the disconnector
+ // object within our own instance), but keep a weak pointer to that
+ // instance. We then stall the destruction until all other weak
+ // pointers have released their "lock" (indicated by the fact that
+ // we will get a nullptr when locking our weak pointer).
+ std::weak_ptr<detail::disconnector> weak{_shared_disconnector};
+ _shared_disconnector.reset();
+ while( weak.lock() != nullptr ) {
+ // we just yield here, allowing the OS to reschedule. We do
+ // this until all threads has released the disconnector object.
+ thread_policy::yield_thread();
+ }
+ }
+
+ /// Retrieve a copy of the current slots
+ ///
+ /// It's useful and necessary to copy the slots so we don't need
+ /// to hold the lock while calling the slots. If we hold the lock
+ /// we prevent the called slots from modifying the slots vector.
+ /// This simple "double buffering" will allow slots to disconnect
+ /// themself or other slots and connect new slots.
+ std::vector<slot_type> copy_slots() const
+ {
+ mutex_lock_type lock{ _mutex };
+ return _slots;
+ }
+
+ /// Implementation of the signal accumulator function call
+ template <class T, class F>
+ typename signal_accumulator<signal_type, T, F, A...>::result_type trigger_with_accumulator( T value, F& func, A const&... args ) const {
+ for( auto const& slot : copy_slots() ) {
+ if( slot ) {
+ value = func( value, slot( args... ) );
+ }
+ }
+ return value;
+ }
+
+ /// Implementation of the disconnection operation.
+ ///
+ /// This is private, and only called by the connection
+ /// objects created when connecting slots to this signal.
+ /// @param index The slot index of the slot that should
+ /// be disconnected.
+ void disconnect( std::size_t index ) {
+ mutex_lock_type lock( _mutex );
+ assert( _slots.size() > index );
+ if( _slots[ index ] != nullptr ) {
+ --_slot_count;
+ }
+ _slots[ index ] = slot_type{};
+ while( _slots.size()>0 && !_slots.back() ) {
+ _slots.pop_back();
+ }
+ }
+
+ /// Implementation of the shared disconnection state
+ /// used by all connection created by signal instances.
+ ///
+ /// This inherits the @ref detail::disconnector interface
+ /// for type erasure.
+ struct disconnector :
+ detail::disconnector
+ {
+ /// Default constructor, resulting in a no-op disconnector.
+ disconnector() :
+ _ptr(nullptr)
+ {}
+
+ /// Create a disconnector that works with a given signal instance.
+ /// @param ptr Pointer to the signal instance that the disconnector
+ /// should work with.
+ disconnector( signal_type<P,R(A...)>* ptr ) :
+ _ptr( ptr )
+ {}
+
+ /// Disconnect a given slot on the current signal instance.
+ /// @note If the instance is default constructed, or created
+ /// with `nullptr` as signal pointer this operation will
+ /// effectively be a no-op.
+ /// @param index The index of the slot to disconnect.
+ void operator()( std::size_t index ) const override {
+ if( _ptr ) {
+ _ptr->disconnect( index );
+ }
+ }
+
+ /// Pointer to the current signal.
+ signal_type<P,R(A...)>* _ptr;
+ };
+
+ /// Mutex to synchronize access to the slot vector
+ mutable mutex_type _mutex;
+ /// Vector of all connected slots
+ std::vector<slot_type> _slots;
+ /// Number of connected slots
+ size_type _slot_count;
+ /// Disconnector operation, used for executing disconnection in a
+ /// type erased manner.
+ disconnector _disconnector;
+ /// Shared pointer to the disconnector. All connection objects has a
+ /// weak pointer to this pointer for performing disconnections.
+ std::shared_ptr<detail::disconnector> _shared_disconnector;
+ };
+
+ // Implementation of the disconnect operation of the connection class
+ inline void connection::disconnect() {
+ auto ptr = _weak_disconnector.lock();
+ if( ptr ) {
+ (*ptr)( _index );
+ }
+ _weak_disconnector.reset();
+ }
+
+ /// Signal type that is safe to use in multithreaded environments,
+ /// where the signal and slots exists in different threads.
+ /// The multithreaded policy provides mutexes and locks to synchronize
+ /// access to the signals internals.
+ ///
+ /// This is the recommended signal type, even for single threaded
+ /// environments.
+ template <class T> using signal = signal_type<multithread_policy, T>;
+
+ /// Signal type that is unsafe in multithreaded environments.
+ /// No synchronizations are provided to the signal_type for accessing
+ /// the internals.
+ ///
+ /// Only use this signal type if you are sure that your environment is
+ /// single threaded and performance is of importance.
+ template <class T> using unsafe_signal = signal_type<singlethread_policy, T>;
+} // namespace nod
+
+#endif // IG_NOD_INCLUDE_NOD_HPP
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<database::Result<database::Track>> 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<playlist::IndexRecordSource>(
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("<unknown>");
- if (r.track()) {
- std::cout << "\t(id:" << *r.track() << ")";
+ for (const auto& r : res->values()) {
+ std::cout << r->text().value_or("<unknown>");
+ 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<database::Result<std::pmr::string>> 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<codecs::IStream> 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<codecs::IStream> {
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<database::TrackId> {
@@ -202,6 +204,9 @@ auto TrackQueue::Previous() -> void {
auto TrackQueue::Clear() -> void {
const std::lock_guard<std::mutex> 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<BatteryState>;
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<void> {
OwningSlice prefix = EncodeDataPrefix();
it->Seek(prefix.slice);
while (it->Valid() && it->key().starts_with(prefix.slice)) {
- std::optional<TrackData> track = ParseDataValue(it->value());
+ std::shared_ptr<TrackData> 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<void> {
continue;
}
- TrackTags tags{};
- if (!tag_parser_.ReadAndParseTags(track->filepath(), &tags) ||
- tags.encoding() == Container::kUnsupported) {
+ std::shared_ptr<TrackTags> 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<void> {
// 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<void> {
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<void> {
.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<TrackTags> 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<TrackId> existing_hash;
std::string raw_entry;
@@ -219,33 +220,36 @@ auto Database::Update() -> std::future<void> {
TrackId id = dbMintNewTrackId();
ESP_LOGI(kTag, "recording new 0x%lx", id);
- TrackData data(id, path, hash);
- dbPutTrackData(data);
+ auto data = std::make_shared<TrackData>(id, path, hash);
+ dbPutTrackData(*data);
dbPutHash(hash, id);
- dbCreateIndexesForTrack({data, tags});
+ auto t = std::make_shared<Track>(data, tags);
+ dbCreateIndexesForTrack(*t);
return;
}
- std::optional<TrackData> existing_data = dbGetTrackData(*existing_hash);
+ std::shared_ptr<TrackData> 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<TrackData>(*existing_hash, path, hash);
+ dbPutTrackData(*new_data);
+ auto t = std::make_shared<Track>(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<Track>(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<std::optional<Track>> {
- return worker_task_->Dispatch<std::optional<Track>>(
- [=, this]() -> std::optional<Track> {
- std::optional<TrackData> data = dbGetTrackData(id);
+auto Database::GetTrack(TrackId id) -> std::future<std::shared_ptr<Track>> {
+ return worker_task_->Dispatch<std::shared_ptr<Track>>(
+ [=, this]() -> std::shared_ptr<Track> {
+ std::shared_ptr<TrackData> data = dbGetTrackData(id);
if (!data || data->is_tombstoned()) {
return {};
}
- TrackTags tags;
- if (!tag_parser_.ReadAndParseTags(data->filepath(), &tags)) {
+ std::shared_ptr<TrackTags> tags =
+ tag_parser_.ReadAndParseTags(data->filepath());
+ if (!tags) {
return {};
}
- return Track(*data, tags);
+ return std::make_shared<Track>(data, tags);
});
}
auto Database::GetBulkTracks(std::vector<TrackId> ids)
- -> std::future<std::vector<std::optional<Track>>> {
- return worker_task_->Dispatch<std::vector<std::optional<Track>>>(
- [=, this]() -> std::vector<std::optional<Track>> {
- std::map<TrackId, Track> id_to_track{};
+ -> std::future<std::vector<std::shared_ptr<Track>>> {
+ return worker_task_->Dispatch<std::vector<std::shared_ptr<Track>>>(
+ [=, this]() -> std::vector<std::shared_ptr<Track>> {
+ std::map<TrackId, std::shared_ptr<Track>> 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<TrackId> ids)
// This id wasn't found at all. Skip it.
continue;
}
- std::optional<Track> track =
+ std::shared_ptr<Track> track =
ParseRecord<Track>(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<std::optional<Track>> results;
+ std::vector<std::shared_ptr<Track>> 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<TrackData> {
+auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr<TrackData> {
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<TrackId> {
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<T>& c) -> Result<T>* {
// Grab results.
std::optional<std::pmr::string> first_key;
- std::vector<T> records;
+ std::vector<std::shared_ptr<T>> 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<T>& c) -> Result<T>* {
if (!first_key) {
first_key = it->key().ToString();
}
- std::optional<T> parsed = ParseRecord<T>(it->key(), it->value());
+ std::shared_ptr<T> parsed = ParseRecord<T>(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<std::pmr::string>(
template <>
auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key,
const leveldb::Slice& val)
- -> std::optional<IndexRecord> {
+ -> std::shared_ptr<IndexRecord> {
std::optional<IndexKey> data = ParseIndexKey(key);
if (!data) {
return {};
@@ -588,28 +593,29 @@ auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key,
title = val.ToString();
}
- return IndexRecord(*data, title, data->track);
+ return std::make_shared<IndexRecord>(*data, title, data->track);
}
template <>
auto Database::ParseRecord<Track>(const leveldb::Slice& key,
const leveldb::Slice& val)
- -> std::optional<Track> {
- std::optional<TrackData> data = ParseDataValue(val);
+ -> std::shared_ptr<Track> {
+ std::shared_ptr<TrackData> data = ParseDataValue(val);
if (!data || data->is_tombstoned()) {
return {};
}
- TrackTags tags;
- if (!tag_parser_.ReadAndParseTags(data->filepath(), &tags)) {
+ std::shared_ptr<TrackTags> tags =
+ tag_parser_.ReadAndParseTags(data->filepath());
+ if (!tags) {
return {};
}
- return Track(*data, tags);
+ return std::make_shared<Track>(data, tags);
}
template <>
auto Database::ParseRecord<std::pmr::string>(const leveldb::Slice& key,
const leveldb::Slice& val)
- -> std::optional<std::pmr::string> {
+ -> std::shared_ptr<std::pmr::string> {
std::ostringstream stream;
stream << "key: ";
if (key.size() < 3 || key.data()[1] != '\0') {
@@ -634,7 +640,7 @@ auto Database::ParseRecord<std::pmr::string>(const leveldb::Slice& key,
}
}
std::pmr::string res{stream.str(), &memory::kSpiRamResource};
- return res;
+ return std::make_shared<std::pmr::string>(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 <typename T>
class Result {
public:
- auto values() const -> const std::vector<T>& { return values_; }
+ auto values() const -> const std::vector<std::shared_ptr<T>>& {
+ return values_;
+ }
auto next_page() -> std::optional<Continuation<T>>& { return next_page_; }
auto prev_page() -> std::optional<Continuation<T>>& { return prev_page_; }
- Result(const std::vector<T>&& values,
+ Result(const std::vector<std::shared_ptr<T>>&& values,
std::optional<Continuation<T>> next,
std::optional<Continuation<T>> prev)
: values_(values), next_page_(next), prev_page_(prev) {}
@@ -62,7 +64,7 @@ class Result {
Result& operator=(const Result&) = delete;
private:
- std::vector<T> values_;
+ std::vector<std::shared_ptr<T>> values_;
std::optional<Continuation<T>> next_page_;
std::optional<Continuation<T>> prev_page_;
};
@@ -102,14 +104,14 @@ class Database {
auto GetTrackPath(TrackId id) -> std::future<std::optional<std::pmr::string>>;
- auto GetTrack(TrackId id) -> std::future<std::optional<Track>>;
+ auto GetTrack(TrackId id) -> std::future<std::shared_ptr<Track>>;
/*
* Fetches data for multiple tracks more efficiently than multiple calls to
* GetTrack.
*/
auto GetBulkTracks(std::vector<TrackId> id)
- -> std::future<std::vector<std::optional<Track>>>;
+ -> std::future<std::vector<std::shared_ptr<Track>>>;
auto GetIndexes() -> std::vector<IndexInfo>;
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<TrackData>;
+ auto dbGetTrackData(TrackId id) -> std::shared_ptr<TrackData>;
auto dbPutHash(const uint64_t& hash, TrackId i) -> void;
auto dbGetHash(const uint64_t& hash) -> std::optional<TrackId>;
- auto dbCreateIndexesForTrack(Track track) -> void;
+ auto dbCreateIndexesForTrack(const Track& track) -> void;
template <typename T>
auto dbGetPage(const Continuation<T>& c) -> Result<T>*;
template <typename T>
auto ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val)
- -> std::optional<T>;
+ -> std::shared_ptr<T>;
};
template <>
auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key,
const leveldb::Slice& val)
- -> std::optional<IndexRecord>;
+ -> std::shared_ptr<IndexRecord>;
template <>
auto Database::ParseRecord<Track>(const leveldb::Slice& key,
const leveldb::Slice& val)
- -> std::optional<Track>;
+ -> std::shared_ptr<Track>;
template <>
auto Database::ParseRecord<std::pmr::string>(const leveldb::Slice& key,
const leveldb::Slice& val)
- -> std::optional<std::pmr::string>;
+ -> std::shared_ptr<std::pmr::string>;
} // 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<TrackData>;
+auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr<TrackData>;
/* 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<TrackTags> = 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<TrackTags> 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<TrackTags> override;
private:
std::map<std::pmr::string, std::unique_ptr<ITagParser>> 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<TrackTags>> 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<TrackTags> 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<int> channels;
std::optional<int> sample_rate;
std::optional<int> 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<Tag, std::pmr::string> 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<TrackData>& data, std::shared_ptr<TrackTags> 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<TrackData> data_;
+ std::shared_ptr<TrackTags> 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<TrackData> {
+auto ParseDataValue(const leveldb::Slice& slice) -> std::shared_ptr<TrackData> {
CborParser parser;
CborValue container;
CborError err;
@@ -211,7 +211,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<TrackData> {
return {};
}
- return TrackData(id, path, hash, play_count, is_tombstoned);
+ return std::make_shared<TrackData>(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<OpusTagParser>();
}
-auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path,
- TrackTags* out) -> bool {
+auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path)
+ -> std::shared_ptr<TrackTags> {
{
std::lock_guard<std::mutex> lock{cache_mutex_};
- std::optional<TrackTags> cached = cache_.Get(path);
+ std::optional<std::shared_ptr<TrackTags>> 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<TrackTags> 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<std::mutex> 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<TrackTags> {
libtags::Aux aux;
- aux.tags = out;
+ auto out = std::make_shared<TrackTags>();
+ 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<TrackTags> {
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<TrackTags>();
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<database::TrackId> {
return {};
}
- return current_page_->values().at(current_item_).track();
+ return current_page_->values().at(current_item_)->track();
}
auto IndexRecordSource::Advance() -> std::optional<database::TrackId> {
@@ -128,7 +128,7 @@ auto IndexRecordSource::Peek(std::size_t n, std::vector<database::TrackId>* 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>(drivers::Samd::Create()));
+ vTaskDelay(pdMS_TO_TICKS(1000));
sServices->nvs(
std::unique_ptr<drivers::NvsStorage>(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 <me@jacqueline.id.au>
+ *
+ * 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<EventBinding*>(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 <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include <cstdint>
+
+#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<void(lv_obj_t*)>& { return signal_; }
+
+ private:
+ lv_obj_t* obj_;
+ nod::signal<void(lv_obj_t*)> 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 <me@jacqueline.id.au>
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+#pragma once
+
+#include "bindey/property.h"
+
+#include "track.hpp"
+
+namespace ui {
+namespace models {
+
+struct Playback {
+ bindey::property<bool> is_playing;
+ bindey::property<std::optional<database::TrackId>> current_track;
+ bindey::property<std::vector<database::TrackId>> upcoming_tracks;
+
+ bindey::property<uint32_t> current_track_position;
+ bindey::property<uint32_t> 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 <memory>
#include <optional>
+#include <vector>
+#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<bindey::scoped_binding> data_bindings_;
+ std::pmr::vector<std::unique_ptr<EventBinding>> event_bindings_;
+
+ template <typename T>
+ auto lv_bind(lv_obj_t* obj, lv_event_code_t ev, T fn) -> void {
+ auto binding = std::make_unique<EventBinding>(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 <memory>
#include <vector>
+#include "bindey/property.h"
+#include "esp_log.h"
#include "lvgl.h"
#include "database.hpp"
#include "future_fetcher.hpp"
+#include "model_playback.hpp"
#include "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<database::Database> db,
+ explicit Playing(models::Playback& playback_model,
+ std::weak_ptr<database::Database> 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<database::Track>& tracks) -> void;
-
std::weak_ptr<database::Database> db_;
audio::TrackQueue& queue_;
- std::optional<database::Track> track_;
- std::vector<database::Track> next_tracks_;
+ bindey::property<std::shared_ptr<database::Track>> current_track_;
+ bindey::property<std::vector<std::shared_ptr<database::Track>>> next_tracks_;
- std::unique_ptr<database::FutureFetcher<std::optional<database::Track>>>
+ std::unique_ptr<database::FutureFetcher<std::shared_ptr<database::Track>>>
new_track_;
std::unique_ptr<
- database::FutureFetcher<std::vector<std::optional<database::Track>>>>
+ database::FutureFetcher<std::vector<std::shared_ptr<database::Track>>>>
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 <stdint.h>
+#include <sys/_stdint.h>
#include <memory>
#include <stack>
#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<UiState> {
/* 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<UiState> {
static std::stack<std::shared_ptr<Screen>> sScreens;
static std::shared_ptr<Screen> sCurrentScreen;
static std::shared_ptr<Modal> sCurrentModal;
+
+ static models::Playback sPlaybackModel;
+
+ static bindey::property<battery::Battery::BatteryState> 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 <memory>
#include "audio_events.hpp"
+#include "bindey/binding.h"
#include "core/lv_event.h"
#include "core/lv_obj.h"
#include "core/lv_obj_scroll.h"
@@ -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<database::Database> db, audio::TrackQueue& queue)
+Playing::Playing(models::Playback& playback_model,
+ std::weak_ptr<database::Database> 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<database::TrackId>& 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<std::shared_ptr<database::Track>>(
+ db->GetTrack(*id)));
+ }));
+ data_bindings_.emplace_back(playback_model.upcoming_tracks.onChangedAndNow(
+ [=, this](const std::vector<database::TrackId>& ids) {
+ auto db = db_.lock();
+ if (!db) {
+ return;
+ }
+ new_next_tracks_.reset(new database::FutureFetcher<
+ std::vector<std::shared_ptr<database::Track>>>(
+ 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<database::Database> 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<database::Track>& t) {
+ if (!t) {
+ return;
+ }
+ lv_label_set_text(
+ artist_label,
+ t->tags().at(database::Tag::kArtist).value_or("").c_str());
+ lv_label_set_text(
+ album_label,
+ t->tags().at(database::Tag::kAlbum).value_or("").c_str());
+ lv_label_set_text(title_label, t->TitleOrFilename().c_str());
+ }));
+
+ 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<uint32_t>(1, d));
+ }));
+ data_bindings_.emplace_back(
+ playback_model.current_track_position.onChangedAndNow(
+ [=](uint32_t p) { lv_slider_set_value(scrubber, p, LV_ANIM_OFF); }));
lv_obj_t* 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<database::Database> 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<database::Database> 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<std::shared_ptr<database::Track>>& tracks) {
+ // TODO(jacqueline): Do a proper diff to maintain selection.
+ int children = lv_obj_get_child_cnt(next_up_container_);
+ while (children > 0) {
+ lv_obj_del(lv_obj_get_child(next_up_container_, 0));
+ children--;
+ }
-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<std::optional<database::Track>>(
- 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<std::vector<std::optional<database::Track>>>(
- 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<database::Track> filtered;
+ std::vector<std::shared_ptr<database::Track>> 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<int> 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<database::Track>& 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<database::IndexRecord>& 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<std::shared_ptr<Screen>> UiState::sScreens;
std::shared_ptr<Screen> UiState::sCurrentScreen;
std::shared_ptr<Modal> UiState::sCurrentModal;
+models::Playback UiState::sPlaybackModel;
+
+bindey::property<battery::Battery::BatteryState> 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<playlist::IndexRecordSource>(
sServices->database(), ev.initial_page, 0, ev.page, ev.record));
+ ESP_LOGI(kTag, "transit to playing");
transit<Playing>();
} 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<screens::TrackBrowser>(
sServices->database(), title, std::move(query)));
}
@@ -329,8 +353,9 @@ void Browse::react(const system_fsm::BluetoothDevicesChanged&) {
static std::shared_ptr<screens::Playing> 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<Browse>();
}
diff --git a/tools/cmake/common.cmake b/tools/cmake/common.cmake
index 313961a3..34bd1226 100644
--- a/tools/cmake/common.cmake
+++ b/tools/cmake/common.cmake
@@ -25,6 +25,7 @@ list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/result")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/shared_string")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/span")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/tinyfsm")
+list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/bindey")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)