Use bindey for databinding instead of hand rolling ui updates

custom
jacqueline 2 years ago
parent f168bfab76
commit f09ba5ffd5
  1. 72
      lib/bindey/.clang-format
  2. 44
      lib/bindey/.github/workflows/ci.yml
  3. 5
      lib/bindey/.gitignore
  4. 6
      lib/bindey/.gitmodules
  5. 6
      lib/bindey/CMakeLists.txt
  6. 21
      lib/bindey/LICENSE.md
  7. 116
      lib/bindey/README.md
  8. 47
      lib/bindey/include/bindey/binding.h
  9. 137
      lib/bindey/include/bindey/property.h
  10. 681
      lib/bindey/include/nod/nod.hpp
  11. 20
      src/app_console/app_console.cpp
  12. 1
      src/audio/audio_decoder.cpp
  13. 7
      src/audio/fatfs_audio_input.cpp
  14. 5
      src/audio/track_queue.cpp
  15. 4
      src/battery/include/battery.hpp
  16. 104
      src/database/database.cpp
  17. 24
      src/database/include/database.hpp
  18. 2
      src/database/include/records.hpp
  19. 18
      src/database/include/tag_parser.hpp
  20. 43
      src/database/include/track.hpp
  21. 4
      src/database/records.cpp
  22. 50
      src/database/tag_parser.cpp
  23. 6
      src/database/track.cpp
  24. 4
      src/playlist/source.cpp
  25. 1
      src/system_fsm/booting.cpp
  26. 3
      src/ui/CMakeLists.txt
  27. 23
      src/ui/event_binding.cpp
  28. 30
      src/ui/include/event_binding.hpp
  29. 26
      src/ui/include/model_playback.hpp
  30. 14
      src/ui/include/screen.hpp
  31. 33
      src/ui/include/screen_playing.hpp
  32. 23
      src/ui/include/ui_fsm.hpp
  33. 214
      src/ui/screen_playing.cpp
  34. 4
      src/ui/screen_track_browser.cpp
  35. 63
      src/ui/ui_fsm.cpp
  36. 1
      tools/cmake/common.cmake

@ -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

@ -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

@ -0,0 +1,5 @@
*.a
*.lib
*.o
*.pdb
.DS_Store

@ -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

@ -0,0 +1,6 @@
# Copyright 2023 jacqueline <me@jacqueline.id.au>
#
# SPDX-License-Identifier: GPL-3.0-only
idf_component_register(
INCLUDE_DIRS "include"
)

@ -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.

@ -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;
};
```

@ -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

@ -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

@ -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

@ -187,8 +187,8 @@ int CmdDbTracks(int argc, char** argv) {
std::unique_ptr<database::Result<database::Track>> res( std::unique_ptr<database::Result<database::Track>> res(
db->GetTracks(20).get()); db->GetTracks(20).get());
while (true) { while (true) {
for (database::Track s : res->values()) { for (const auto& s : res->values()) {
std::cout << s.tags()[database::Tag::kTitle].value_or("[BLANK]") std::cout << s->tags()[database::Tag::kTitle].value_or("[BLANK]")
<< std::endl; << std::endl;
} }
if (res->next_page()) { if (res->next_page()) {
@ -256,12 +256,12 @@ int CmdDbIndex(int argc, char** argv) {
std::cout << "choice out of range" << std::endl; std::cout << "choice out of range" << std::endl;
return -1; return -1;
} }
if (res->values().at(choice).track()) { if (res->values().at(choice)->track()) {
AppConsole::sServices->track_queue().IncludeLast( AppConsole::sServices->track_queue().IncludeLast(
std::make_shared<playlist::IndexRecordSource>( std::make_shared<playlist::IndexRecordSource>(
AppConsole::sServices->database(), res, 0, res, choice)); 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) { if (!cont) {
std::cout << "more choices than levels" << std::endl; std::cout << "more choices than levels" << std::endl;
return 0; return 0;
@ -270,10 +270,10 @@ int CmdDbIndex(int argc, char** argv) {
choice_index++; choice_index++;
} }
for (database::IndexRecord r : res->values()) { for (const auto& r : res->values()) {
std::cout << r.text().value_or("<unknown>"); std::cout << r->text().value_or("<unknown>");
if (r.track()) { if (r->track()) {
std::cout << "\t(id:" << *r.track() << ")"; std::cout << "\t(id:" << *r->track() << ")";
} }
std::cout << std::endl; 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()); std::unique_ptr<database::Result<std::pmr::string>> res(db->GetDump(5).get());
while (true) { while (true) {
for (const std::pmr::string& s : res->values()) { for (const auto& s : res->values()) {
std::cout << s << std::endl; std::cout << *s << std::endl;
} }
if (res->next_page()) { if (res->next_page()) {
auto continuation = res->next_page().value(); auto continuation = res->next_page().value();

@ -103,6 +103,7 @@ void Decoder::Main() {
for (;;) { for (;;) {
if (source_->HasNewStream() || !stream_) { if (source_->HasNewStream() || !stream_) {
std::shared_ptr<codecs::IStream> new_stream = source_->NextStream(); std::shared_ptr<codecs::IStream> new_stream = source_->NextStream();
ESP_LOGI(kTag, "decoder has new stream");
if (new_stream && BeginDecoding(new_stream)) { if (new_stream && BeginDecoding(new_stream)) {
stream_ = new_stream; stream_ = new_stream;
} else { } else {

@ -34,6 +34,7 @@
#include "future_fetcher.hpp" #include "future_fetcher.hpp"
#include "tag_parser.hpp" #include "tag_parser.hpp"
#include "tasks.hpp" #include "tasks.hpp"
#include "track.hpp"
#include "types.hpp" #include "types.hpp"
static const char* kTag = "SRC"; 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 { auto FatfsAudioInput::OpenFile(const std::pmr::string& path) -> bool {
ESP_LOGI(kTag, "opening file %s", path.c_str()); ESP_LOGI(kTag, "opening file %s", path.c_str());
database::TrackTags tags; auto tags = tag_parser_.ReadAndParseTags(path);
if (!tag_parser_.ReadAndParseTags(path, &tags)) { if (!tags) {
ESP_LOGE(kTag, "failed to read tags"); ESP_LOGE(kTag, "failed to read tags");
return false; return false;
} }
auto stream_type = ContainerToStreamType(tags.encoding()); auto stream_type = ContainerToStreamType(tags->encoding());
if (!stream_type.has_value()) { if (!stream_type.has_value()) {
ESP_LOGE(kTag, "couldn't match container to stream"); ESP_LOGE(kTag, "couldn't match container to stream");
return false; return false;

@ -19,6 +19,8 @@
namespace audio { namespace audio {
static constexpr char kTag[] = "tracks";
TrackQueue::TrackQueue() {} TrackQueue::TrackQueue() {}
auto TrackQueue::GetCurrent() const -> std::optional<database::TrackId> { auto TrackQueue::GetCurrent() const -> std::optional<database::TrackId> {
@ -202,6 +204,9 @@ auto TrackQueue::Previous() -> void {
auto TrackQueue::Clear() -> void { auto TrackQueue::Clear() -> void {
const std::lock_guard<std::mutex> lock(mutex_); const std::lock_guard<std::mutex> lock(mutex_);
if (enqueued_.empty() && played_.empty()) {
return;
}
QueueUpdate ev{.current_changed = !enqueued_.empty()}; QueueUpdate ev{.current_changed = !enqueued_.empty()};
played_.clear(); played_.clear();
enqueued_.clear(); enqueued_.clear();

@ -26,6 +26,10 @@ class Battery {
struct BatteryState { struct BatteryState {
uint_fast8_t percent; uint_fast8_t percent;
bool is_charging; bool is_charging;
bool operator==(const BatteryState& other) const {
return percent == other.percent && is_charging == other.is_charging;
}
}; };
auto State() -> std::optional<BatteryState>; auto State() -> std::optional<BatteryState>;

@ -144,7 +144,7 @@ auto Database::Update() -> std::future<void> {
OwningSlice prefix = EncodeDataPrefix(); OwningSlice prefix = EncodeDataPrefix();
it->Seek(prefix.slice); it->Seek(prefix.slice);
while (it->Valid() && it->key().starts_with(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) { if (!track) {
// The value was malformed. Drop this record. // The value was malformed. Drop this record.
ESP_LOGW(kTag, "dropping malformed metadata"); ESP_LOGW(kTag, "dropping malformed metadata");
@ -159,9 +159,9 @@ auto Database::Update() -> std::future<void> {
continue; continue;
} }
TrackTags tags{}; std::shared_ptr<TrackTags> tags =
if (!tag_parser_.ReadAndParseTags(track->filepath(), &tags) || tag_parser_.ReadAndParseTags(track->filepath());
tags.encoding() == Container::kUnsupported) { if (!tags || tags->encoding() == Container::kUnsupported) {
// We couldn't read the tags for this track. Either they were // We couldn't read the tags for this track. Either they were
// malformed, or perhaps the file is missing. Either way, tombstone // malformed, or perhaps the file is missing. Either way, tombstone
// this record. // 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 // 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. // 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()) { if (new_hash != track->tags_hash()) {
// This track's tags have changed. Since the filepath is exactly the // This track's tags have changed. Since the filepath is exactly the
// same, we assume this is a legitimate correction. Update 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()); dbPutHash(new_hash, track->id());
} }
dbCreateIndexesForTrack({*track, tags}); Track t{track, tags};
dbCreateIndexesForTrack(t);
it->Next(); it->Next();
} }
@ -197,15 +199,14 @@ auto Database::Update() -> std::future<void> {
.stage = event::UpdateProgress::Stage::kScanningForNewTracks, .stage = event::UpdateProgress::Stage::kScanningForNewTracks,
}); });
file_gatherer_.FindFiles("", [&](const std::pmr::string& path) { file_gatherer_.FindFiles("", [&](const std::pmr::string& path) {
TrackTags tags; std::shared_ptr<TrackTags> tags = tag_parser_.ReadAndParseTags(path);
if (!tag_parser_.ReadAndParseTags(path, &tags) || if (!tags || tags->encoding() == Container::kUnsupported) {
tags.encoding() == Container::kUnsupported) {
// No parseable tags; skip this fiile. // No parseable tags; skip this fiile.
return; return;
} }
// Check for any existing record with the same hash. // Check for any existing record with the same hash.
uint64_t hash = tags.Hash(); uint64_t hash = tags->Hash();
OwningSlice key = EncodeHashKey(hash); OwningSlice key = EncodeHashKey(hash);
std::optional<TrackId> existing_hash; std::optional<TrackId> existing_hash;
std::string raw_entry; std::string raw_entry;
@ -219,33 +220,36 @@ auto Database::Update() -> std::future<void> {
TrackId id = dbMintNewTrackId(); TrackId id = dbMintNewTrackId();
ESP_LOGI(kTag, "recording new 0x%lx", id); ESP_LOGI(kTag, "recording new 0x%lx", id);
TrackData data(id, path, hash); auto data = std::make_shared<TrackData>(id, path, hash);
dbPutTrackData(data); dbPutTrackData(*data);
dbPutHash(hash, id); dbPutHash(hash, id);
dbCreateIndexesForTrack({data, tags}); auto t = std::make_shared<Track>(data, tags);
dbCreateIndexesForTrack(*t);
return; return;
} }
std::optional<TrackData> existing_data = dbGetTrackData(*existing_hash); std::shared_ptr<TrackData> existing_data = dbGetTrackData(*existing_hash);
if (!existing_data) { if (!existing_data) {
// We found a hash that matches, but there's no data record? Weird. // We found a hash that matches, but there's no data record? Weird.
TrackData new_data(*existing_hash, path, hash); auto new_data = std::make_shared<TrackData>(*existing_hash, path, hash);
dbPutTrackData(new_data); dbPutTrackData(*new_data);
dbCreateIndexesForTrack({*existing_data, tags}); auto t = std::make_shared<Track>(new_data, tags);
dbCreateIndexesForTrack(*t);
return; return;
} }
if (existing_data->is_tombstoned()) { if (existing_data->is_tombstoned()) {
ESP_LOGI(kTag, "exhuming track %lu", existing_data->id()); ESP_LOGI(kTag, "exhuming track %lu", existing_data->id());
dbPutTrackData(existing_data->Exhume(path)); 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) { } else if (existing_data->filepath() != path) {
ESP_LOGW(kTag, "tag hash collision for %s and %s", ESP_LOGW(kTag, "tag hash collision for %s and %s",
existing_data->filepath().c_str(), path.c_str()); existing_data->filepath().c_str(), path.c_str());
ESP_LOGI(kTag, "hash components: %s, %s, %s", ESP_LOGI(kTag, "hash components: %s, %s, %s",
tags.at(Tag::kTitle).value_or("no title").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::kArtist).value_or("no artist").c_str(),
tags.at(Tag::kAlbum).value_or("no album").c_str()); tags->at(Tag::kAlbum).value_or("no album").c_str());
} }
}); });
events::Ui().Dispatch(event::UpdateFinished{}); events::Ui().Dispatch(event::UpdateFinished{});
@ -264,26 +268,27 @@ auto Database::GetTrackPath(TrackId id)
}); });
} }
auto Database::GetTrack(TrackId id) -> std::future<std::optional<Track>> { auto Database::GetTrack(TrackId id) -> std::future<std::shared_ptr<Track>> {
return worker_task_->Dispatch<std::optional<Track>>( return worker_task_->Dispatch<std::shared_ptr<Track>>(
[=, this]() -> std::optional<Track> { [=, this]() -> std::shared_ptr<Track> {
std::optional<TrackData> data = dbGetTrackData(id); std::shared_ptr<TrackData> data = dbGetTrackData(id);
if (!data || data->is_tombstoned()) { if (!data || data->is_tombstoned()) {
return {}; return {};
} }
TrackTags tags; std::shared_ptr<TrackTags> tags =
if (!tag_parser_.ReadAndParseTags(data->filepath(), &tags)) { tag_parser_.ReadAndParseTags(data->filepath());
if (!tags) {
return {}; return {};
} }
return Track(*data, tags); return std::make_shared<Track>(data, tags);
}); });
} }
auto Database::GetBulkTracks(std::vector<TrackId> ids) auto Database::GetBulkTracks(std::vector<TrackId> ids)
-> std::future<std::vector<std::optional<Track>>> { -> std::future<std::vector<std::shared_ptr<Track>>> {
return worker_task_->Dispatch<std::vector<std::optional<Track>>>( return worker_task_->Dispatch<std::vector<std::shared_ptr<Track>>>(
[=, this]() -> std::vector<std::optional<Track>> { [=, this]() -> std::vector<std::shared_ptr<Track>> {
std::map<TrackId, Track> id_to_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 // Sort the list of ids so that we can retrieve them all in a single
// iteration through the database, without re-seeking. // 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. // This id wasn't found at all. Skip it.
continue; continue;
} }
std::optional<Track> track = std::shared_ptr<Track> track =
ParseRecord<Track>(it->key(), it->value()); ParseRecord<Track>(it->key(), it->value());
if (track) { 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 // We've fetched all of the ids in the request, so now just put them
// back into the order they were asked for in. // 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) { for (const TrackId& id : ids) {
if (id_to_track.contains(id)) { if (id_to_track.contains(id)) {
results.push_back(id_to_track.at(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); OwningSlice key = EncodeDataKey(id);
std::string raw_val; std::string raw_val;
if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) { 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); return ParseHashValue(raw_val);
} }
auto Database::dbCreateIndexesForTrack(Track track) -> void { auto Database::dbCreateIndexesForTrack(const Track& track) -> void {
for (const IndexInfo& index : GetIndexes()) { for (const IndexInfo& index : GetIndexes()) {
leveldb::WriteBatch writes; leveldb::WriteBatch writes;
if (Index(index, track, &writes)) { if (Index(index, track, &writes)) {
@ -481,7 +486,7 @@ auto Database::dbGetPage(const Continuation<T>& c) -> Result<T>* {
// Grab results. // Grab results.
std::optional<std::pmr::string> first_key; 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()) { while (records.size() < c.page_size && it->Valid()) {
if (!it->key().starts_with({c.prefix.data(), c.prefix.size()})) { if (!it->key().starts_with({c.prefix.data(), c.prefix.size()})) {
break; break;
@ -489,9 +494,9 @@ auto Database::dbGetPage(const Continuation<T>& c) -> Result<T>* {
if (!first_key) { if (!first_key) {
first_key = it->key().ToString(); 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) { if (parsed) {
records.push_back(*parsed); records.push_back(parsed);
} }
if (c.forward) { if (c.forward) {
it->Next(); it->Next();
@ -577,7 +582,7 @@ template auto Database::dbGetPage<std::pmr::string>(
template <> template <>
auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key, auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key,
const leveldb::Slice& val) const leveldb::Slice& val)
-> std::optional<IndexRecord> { -> std::shared_ptr<IndexRecord> {
std::optional<IndexKey> data = ParseIndexKey(key); std::optional<IndexKey> data = ParseIndexKey(key);
if (!data) { if (!data) {
return {}; return {};
@ -588,28 +593,29 @@ auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key,
title = val.ToString(); title = val.ToString();
} }
return IndexRecord(*data, title, data->track); return std::make_shared<IndexRecord>(*data, title, data->track);
} }
template <> template <>
auto Database::ParseRecord<Track>(const leveldb::Slice& key, auto Database::ParseRecord<Track>(const leveldb::Slice& key,
const leveldb::Slice& val) const leveldb::Slice& val)
-> std::optional<Track> { -> std::shared_ptr<Track> {
std::optional<TrackData> data = ParseDataValue(val); std::shared_ptr<TrackData> data = ParseDataValue(val);
if (!data || data->is_tombstoned()) { if (!data || data->is_tombstoned()) {
return {}; return {};
} }
TrackTags tags; std::shared_ptr<TrackTags> tags =
if (!tag_parser_.ReadAndParseTags(data->filepath(), &tags)) { tag_parser_.ReadAndParseTags(data->filepath());
if (!tags) {
return {}; return {};
} }
return Track(*data, tags); return std::make_shared<Track>(data, tags);
} }
template <> template <>
auto Database::ParseRecord<std::pmr::string>(const leveldb::Slice& key, auto Database::ParseRecord<std::pmr::string>(const leveldb::Slice& key,
const leveldb::Slice& val) const leveldb::Slice& val)
-> std::optional<std::pmr::string> { -> std::shared_ptr<std::pmr::string> {
std::ostringstream stream; std::ostringstream stream;
stream << "key: "; stream << "key: ";
if (key.size() < 3 || key.data()[1] != '\0') { 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}; std::pmr::string res{stream.str(), &memory::kSpiRamResource};
return res; return std::make_shared<std::pmr::string>(res);
} }
IndexRecord::IndexRecord(const IndexKey& key, IndexRecord::IndexRecord(const IndexKey& key,

@ -48,12 +48,14 @@ struct Continuation {
template <typename T> template <typename T>
class Result { class Result {
public: 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 next_page() -> std::optional<Continuation<T>>& { return next_page_; }
auto prev_page() -> std::optional<Continuation<T>>& { return prev_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>> next,
std::optional<Continuation<T>> prev) std::optional<Continuation<T>> prev)
: values_(values), next_page_(next), prev_page_(prev) {} : values_(values), next_page_(next), prev_page_(prev) {}
@ -62,7 +64,7 @@ class Result {
Result& operator=(const Result&) = delete; Result& operator=(const Result&) = delete;
private: private:
std::vector<T> values_; std::vector<std::shared_ptr<T>> values_;
std::optional<Continuation<T>> next_page_; std::optional<Continuation<T>> next_page_;
std::optional<Continuation<T>> prev_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 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 * Fetches data for multiple tracks more efficiently than multiple calls to
* GetTrack. * GetTrack.
*/ */
auto GetBulkTracks(std::vector<TrackId> id) 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 GetIndexes() -> std::vector<IndexInfo>;
auto GetTracksByIndex(const IndexInfo& index, std::size_t page_size) 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 dbEntomb(TrackId track, uint64_t hash) -> void;
auto dbPutTrackData(const TrackData& s) -> 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 dbPutHash(const uint64_t& hash, TrackId i) -> void;
auto dbGetHash(const uint64_t& hash) -> std::optional<TrackId>; auto dbGetHash(const uint64_t& hash) -> std::optional<TrackId>;
auto dbCreateIndexesForTrack(Track track) -> void; auto dbCreateIndexesForTrack(const Track& track) -> void;
template <typename T> template <typename T>
auto dbGetPage(const Continuation<T>& c) -> Result<T>*; auto dbGetPage(const Continuation<T>& c) -> Result<T>*;
template <typename T> template <typename T>
auto ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val) auto ParseRecord(const leveldb::Slice& key, const leveldb::Slice& val)
-> std::optional<T>; -> std::shared_ptr<T>;
}; };
template <> template <>
auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key, auto Database::ParseRecord<IndexRecord>(const leveldb::Slice& key,
const leveldb::Slice& val) const leveldb::Slice& val)
-> std::optional<IndexRecord>; -> std::shared_ptr<IndexRecord>;
template <> template <>
auto Database::ParseRecord<Track>(const leveldb::Slice& key, auto Database::ParseRecord<Track>(const leveldb::Slice& key,
const leveldb::Slice& val) const leveldb::Slice& val)
-> std::optional<Track>; -> std::shared_ptr<Track>;
template <> template <>
auto Database::ParseRecord<std::pmr::string>(const leveldb::Slice& key, auto Database::ParseRecord<std::pmr::string>(const leveldb::Slice& key,
const leveldb::Slice& val) const leveldb::Slice& val)
-> std::optional<std::pmr::string>; -> std::shared_ptr<std::pmr::string>;
} // namespace database } // namespace database

@ -53,7 +53,7 @@ auto EncodeDataValue(const TrackData& track) -> OwningSlice;
* Parses bytes previously encoded via EncodeDataValue back into a TrackData. * Parses bytes previously encoded via EncodeDataValue back into a TrackData.
* May return nullopt if parsing fails. * 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. */ /* Encodes a hash key for the specified hash. */
auto EncodeHashKey(const uint64_t& hash) -> OwningSlice; auto EncodeHashKey(const uint64_t& hash) -> OwningSlice;

@ -16,21 +16,21 @@ namespace database {
class ITagParser { class ITagParser {
public: public:
virtual ~ITagParser() {} virtual ~ITagParser() {}
virtual auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) virtual auto ReadAndParseTags(const std::pmr::string& path)
-> bool = 0; -> std::shared_ptr<TrackTags> = 0;
}; };
class GenericTagParser : public ITagParser { class GenericTagParser : public ITagParser {
public: public:
auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) auto ReadAndParseTags(const std::pmr::string& path)
-> bool override; -> std::shared_ptr<TrackTags> override;
}; };
class TagParserImpl : public ITagParser { class TagParserImpl : public ITagParser {
public: public:
TagParserImpl(); TagParserImpl();
auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) auto ReadAndParseTags(const std::pmr::string& path)
-> bool override; -> std::shared_ptr<TrackTags> override;
private: private:
std::map<std::pmr::string, std::unique_ptr<ITagParser>> extension_to_parser_; 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. * cache should be slightly larger than any page sizes in the UI.
*/ */
std::mutex cache_mutex_; 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 // 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 // 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 { class OpusTagParser : public ITagParser {
public: public:
auto ReadAndParseTags(const std::pmr::string& path, TrackTags* out) auto ReadAndParseTags(const std::pmr::string& path)
-> bool override; -> std::shared_ptr<TrackTags> override;
}; };
} // namespace database } // namespace database

@ -61,12 +61,17 @@ enum class Tag {
*/ */
class TrackTags { class TrackTags {
public: public:
auto encoding() const -> Container { return encoding_; };
auto encoding(Container e) -> void { encoding_ = e; };
TrackTags() TrackTags()
: encoding_(Container::kUnsupported), tags_(&memory::kSpiRamResource) {} : 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> channels;
std::optional<int> sample_rate; std::optional<int> sample_rate;
std::optional<int> bits_per_sample; std::optional<int> bits_per_sample;
@ -85,10 +90,6 @@ class TrackTags {
*/ */
auto Hash() const -> uint64_t; auto Hash() const -> uint64_t;
bool operator==(const TrackTags&) const = default;
TrackTags& operator=(const TrackTags&) = default;
TrackTags(const TrackTags&) = default;
private: private:
Container encoding_; Container encoding_;
std::pmr::unordered_map<Tag, std::pmr::string> tags_; std::pmr::unordered_map<Tag, std::pmr::string> tags_;
@ -139,6 +140,11 @@ class TrackData {
play_count_(play_count), play_count_(play_count),
is_tombstoned_(is_tombstoned) {} 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 id() const -> TrackId { return id_; }
auto filepath() const -> std::pmr::string { return filepath_; } auto filepath() const -> std::pmr::string { return filepath_; }
auto play_count() const -> uint32_t { return play_count_; } auto play_count() const -> uint32_t { return play_count_; }
@ -158,8 +164,6 @@ class TrackData {
* new location. * new location.
*/ */
auto Exhume(const std::pmr::string& new_path) const -> TrackData; auto Exhume(const std::pmr::string& new_path) const -> TrackData;
bool operator==(const TrackData&) const = default;
}; };
/* /*
@ -172,23 +176,22 @@ class TrackData {
*/ */
class Track { class Track {
public: public:
Track(const TrackData& data, const TrackTags& tags) Track(std::shared_ptr<TrackData>& data, std::shared_ptr<TrackTags> tags)
: data_(data), tags_(tags) {} : data_(data), tags_(tags) {}
Track(const Track& other) = default;
auto data() const -> const TrackData& { return data_; } Track(Track& other) = delete;
auto tags() const -> const TrackTags& { return tags_; } Track& operator=(Track& other) = delete;
auto TitleOrFilename() const -> std::pmr::string;
bool operator==(const Track&) const = default; 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: private:
const TrackData data_; std::shared_ptr<TrackData> data_;
const TrackTags tags_; std::shared_ptr<TrackTags> tags_;
}; };
void swap(Track& first, Track& second);
} // namespace database } // namespace database

@ -149,7 +149,7 @@ auto EncodeDataValue(const TrackData& track) -> OwningSlice {
return OwningSlice(as_str); 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; CborParser parser;
CborValue container; CborValue container;
CborError err; CborError err;
@ -211,7 +211,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<TrackData> {
return {}; return {};
} }
return TrackData(id, path, hash, play_count, is_tombstoned); return std::make_shared<TrackData>(id, path, hash, play_count, is_tombstoned);
} }
/* 'H/ 0xBEEF' */ /* 'H/ 0xBEEF' */

@ -130,14 +130,13 @@ TagParserImpl::TagParserImpl() {
extension_to_parser_["opus"] = std::make_unique<OpusTagParser>(); extension_to_parser_["opus"] = std::make_unique<OpusTagParser>();
} }
auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path, auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path)
TrackTags* out) -> bool { -> std::shared_ptr<TrackTags> {
{ {
std::lock_guard<std::mutex> lock{cache_mutex_}; 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) { if (cached) {
*out = *cached; return *cached;
return true;
} }
} }
@ -152,41 +151,43 @@ auto TagParserImpl::ReadAndParseTags(const std::pmr::string& path,
} }
} }
if (!parser->ReadAndParseTags(path, out)) { std::shared_ptr<TrackTags> tags = parser->ReadAndParseTags(path);
return false; if (!tags) {
return {};
} }
// There wasn't a track number found in the track's tags. Try to synthesize // 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 // one from the filename, which will sometimes have a track number at the
// start. // start.
if (!out->at(Tag::kAlbumTrack)) { if (!tags->at(Tag::kAlbumTrack)) {
auto slash_pos = path.find_last_of("/"); auto slash_pos = path.find_last_of("/");
if (slash_pos != std::pmr::string::npos && path.size() - slash_pos > 1) { 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 // Normalise track numbers; they're usually treated as strings, but we would
// like to sort them lexicographically. // like to sort them lexicographically.
out->set(Tag::kAlbumTrack, tags->set(Tag::kAlbumTrack,
convert_track_number(out->at(Tag::kAlbumTrack).value_or("0"))); convert_track_number(tags->at(Tag::kAlbumTrack).value_or("0")));
{ {
std::lock_guard<std::mutex> lock{cache_mutex_}; 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, auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path)
TrackTags* out) -> bool { -> std::shared_ptr<TrackTags> {
libtags::Aux aux; 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 || if (f_stat(path.c_str(), &aux.info) != FR_OK ||
f_open(&aux.file, path.c_str(), FA_READ) != FR_OK) { f_open(&aux.file, path.c_str(), FA_READ) != FR_OK) {
ESP_LOGW(kTag, "failed to open file %s", path.c_str()); 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 // Fine to have this on the stack; this is only called on tasks with large
// stacks anyway, due to all the string handling. // stacks anyway, due to all the string handling.
@ -205,7 +206,7 @@ auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path,
if (res != 0) { if (res != 0) {
// Parsing failed. // Parsing failed.
ESP_LOGE(kTag, "tag parsing for %s failed, reason %d", path.c_str(), res); ESP_LOGE(kTag, "tag parsing for %s failed, reason %d", path.c_str(), res);
return false; return {};
} }
switch (ctx.format) { switch (ctx.format) {
@ -240,25 +241,26 @@ auto GenericTagParser::ReadAndParseTags(const std::pmr::string& path,
if (ctx.duration > 0) { if (ctx.duration > 0) {
out->duration = ctx.duration; out->duration = ctx.duration;
} }
return true; return out;
} }
auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path, auto OpusTagParser::ReadAndParseTags(const std::pmr::string& path)
TrackTags* out) -> bool { -> std::shared_ptr<TrackTags> {
std::pmr::string vfs_path = "/sdcard" + path; std::pmr::string vfs_path = "/sdcard" + path;
int err; int err;
OggOpusFile* f = op_test_file(vfs_path.c_str(), &err); OggOpusFile* f = op_test_file(vfs_path.c_str(), &err);
if (f == NULL) { if (f == NULL) {
ESP_LOGE(kTag, "opusfile tag parsing failed: %d", err); ESP_LOGE(kTag, "opusfile tag parsing failed: %d", err);
return false; return {};
} }
const OpusTags* tags = op_tags(f, -1); const OpusTags* tags = op_tags(f, -1);
if (tags == NULL) { if (tags == NULL) {
ESP_LOGE(kTag, "no tags in opusfile"); ESP_LOGE(kTag, "no tags in opusfile");
op_free(f); op_free(f);
return false; return {};
} }
auto out = std::make_shared<TrackTags>();
out->encoding(Container::kOpus); out->encoding(Container::kOpus);
for (const auto& pair : kVorbisIdToTag) { for (const auto& pair : kVorbisIdToTag) {
const char* tag = opus_tags_query(tags, pair.first, 0); 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); op_free(f);
return true; return out;
} }
} // namespace database } // namespace database

@ -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); 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 Track::TitleOrFilename() const -> std::pmr::string {
auto title = tags().at(Tag::kTitle); auto title = tags().at(Tag::kTitle);
if (title) { if (title) {

@ -51,7 +51,7 @@ auto IndexRecordSource::Current() -> std::optional<database::TrackId> {
return {}; return {};
} }
return current_page_->values().at(current_item_).track(); return current_page_->values().at(current_item_)->track();
} }
auto IndexRecordSource::Advance() -> std::optional<database::TrackId> { 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; 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--; n--;
items_added++; items_added++;
working_item++; working_item++;

@ -66,6 +66,7 @@ auto Booting::entry() -> void {
ESP_LOGI(kTag, "installing remaining drivers"); ESP_LOGI(kTag, "installing remaining drivers");
sServices->samd(std::unique_ptr<drivers::Samd>(drivers::Samd::Create())); sServices->samd(std::unique_ptr<drivers::Samd>(drivers::Samd::Create()));
vTaskDelay(pdMS_TO_TICKS(1000));
sServices->nvs( sServices->nvs(
std::unique_ptr<drivers::NvsStorage>(drivers::NvsStorage::OpenSync())); std::unique_ptr<drivers::NvsStorage>(drivers::NvsStorage::OpenSync()));
sServices->touchwheel( sServices->touchwheel(

@ -7,10 +7,11 @@ idf_component_register(
"wheel_encoder.cpp" "screen_track_browser.cpp" "screen_playing.cpp" "wheel_encoder.cpp" "screen_track_browser.cpp" "screen_playing.cpp"
"themes.cpp" "widget_top_bar.cpp" "screen.cpp" "screen_onboarding.cpp" "themes.cpp" "widget_top_bar.cpp" "screen.cpp" "screen_onboarding.cpp"
"modal_progress.cpp" "modal.cpp" "modal_confirm.cpp" "screen_settings.cpp" "modal_progress.cpp" "modal.cpp" "modal_confirm.cpp" "screen_settings.cpp"
"event_binding.cpp"
"splash.c" "font_fusion.c" "font_symbols.c" "splash.c" "font_fusion.c" "font_symbols.c"
"icons/battery_empty.c" "icons/battery_full.c" "icons/battery_20.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/battery_40.c" "icons/battery_60.c" "icons/battery_80.c" "icons/play.c"
"icons/pause.c" "icons/bluetooth.c" "icons/pause.c" "icons/bluetooth.c"
INCLUDE_DIRS "include" 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}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -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

@ -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

@ -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

@ -8,11 +8,15 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <vector>
#include "bindey/binding.h"
#include "core/lv_group.h" #include "core/lv_group.h"
#include "core/lv_obj.h" #include "core/lv_obj.h"
#include "core/lv_obj_tree.h" #include "core/lv_obj_tree.h"
#include "event_binding.hpp"
#include "lvgl.h" #include "lvgl.h"
#include "nod/nod.hpp"
#include "widget_top_bar.hpp" #include "widget_top_bar.hpp"
namespace ui { namespace ui {
@ -51,6 +55,16 @@ class Screen {
auto CreateTopBar(lv_obj_t* parent, const widgets::TopBar::Configuration&) auto CreateTopBar(lv_obj_t* parent, const widgets::TopBar::Configuration&)
-> widgets::TopBar*; -> 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* const root_;
lv_obj_t* content_; lv_obj_t* content_;
lv_obj_t* modal_content_; lv_obj_t* modal_content_;

@ -11,10 +11,13 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include "bindey/property.h"
#include "esp_log.h"
#include "lvgl.h" #include "lvgl.h"
#include "database.hpp" #include "database.hpp"
#include "future_fetcher.hpp" #include "future_fetcher.hpp"
#include "model_playback.hpp"
#include "screen.hpp" #include "screen.hpp"
#include "track.hpp" #include "track.hpp"
#include "track_queue.hpp" #include "track_queue.hpp"
@ -28,48 +31,36 @@ namespace screens {
*/ */
class Playing : public Screen { class Playing : public Screen {
public: public:
explicit Playing(std::weak_ptr<database::Database> db, explicit Playing(models::Playback& playback_model,
std::weak_ptr<database::Database> db,
audio::TrackQueue& queue); audio::TrackQueue& queue);
~Playing(); ~Playing();
auto Tick() -> void override; 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 OnFocusAboveFold() -> void;
auto OnFocusBelowFold() -> void; auto OnFocusBelowFold() -> void;
Playing(const Playing&) = delete;
Playing& operator=(const Playing&) = delete;
private: private:
auto control_button(lv_obj_t* parent, char* icon) -> lv_obj_t*; 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) auto next_up_label(lv_obj_t* parent, const std::pmr::string& text)
-> lv_obj_t*; -> 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_; std::weak_ptr<database::Database> db_;
audio::TrackQueue& queue_; audio::TrackQueue& queue_;
std::optional<database::Track> track_; bindey::property<std::shared_ptr<database::Track>> current_track_;
std::vector<database::Track> next_tracks_; 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_; new_track_;
std::unique_ptr< std::unique_ptr<
database::FutureFetcher<std::vector<std::optional<database::Track>>>> database::FutureFetcher<std::vector<std::shared_ptr<database::Track>>>>
new_next_tracks_; 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_header_;
lv_obj_t* next_up_label_; lv_obj_t* next_up_label_;
lv_obj_t* next_up_hint_; lv_obj_t* next_up_hint_;

@ -7,13 +7,16 @@
#pragma once #pragma once
#include <stdint.h> #include <stdint.h>
#include <sys/_stdint.h>
#include <memory> #include <memory>
#include <stack> #include <stack>
#include "audio_events.hpp" #include "audio_events.hpp"
#include "battery.hpp" #include "battery.hpp"
#include "bindey/property.h"
#include "gpios.hpp" #include "gpios.hpp"
#include "lvgl_task.hpp" #include "lvgl_task.hpp"
#include "model_playback.hpp"
#include "nvs.hpp" #include "nvs.hpp"
#include "relative_wheel.hpp" #include "relative_wheel.hpp"
#include "screen_playing.hpp" #include "screen_playing.hpp"
@ -27,6 +30,7 @@
#include "storage.hpp" #include "storage.hpp"
#include "system_events.hpp" #include "system_events.hpp"
#include "touchwheel.hpp" #include "touchwheel.hpp"
#include "track.hpp"
#include "track_queue.hpp" #include "track_queue.hpp"
#include "ui_events.hpp" #include "ui_events.hpp"
#include "wheel_encoder.hpp" #include "wheel_encoder.hpp"
@ -49,11 +53,11 @@ class UiState : public tinyfsm::Fsm<UiState> {
/* Fallback event handler. Does nothing. */ /* Fallback event handler. Does nothing. */
void react(const tinyfsm::Event& ev) {} void react(const tinyfsm::Event& ev) {}
virtual void react(const system_fsm::BatteryStateChanged&); void react(const system_fsm::BatteryStateChanged&);
virtual void react(const audio::PlaybackStarted&); void react(const audio::PlaybackStarted&);
virtual void react(const audio::PlaybackFinished&); void react(const audio::PlaybackFinished&);
virtual void react(const audio::PlaybackUpdate&) {} void react(const audio::PlaybackUpdate&);
virtual void react(const audio::QueueUpdate&) {} void react(const audio::QueueUpdate&);
virtual void react(const system_fsm::KeyLockChanged&); 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::stack<std::shared_ptr<Screen>> sScreens;
static std::shared_ptr<Screen> sCurrentScreen; static std::shared_ptr<Screen> sCurrentScreen;
static std::shared_ptr<Modal> sCurrentModal; static std::shared_ptr<Modal> sCurrentModal;
static models::Playback sPlaybackModel;
static bindey::property<battery::Battery::BatteryState> sPropBatteryState;
}; };
namespace states { namespace states {
@ -96,7 +104,6 @@ class Splash : public UiState {
public: public:
void exit() override; void exit() override;
void react(const system_fsm::BootComplete&) override; void react(const system_fsm::BootComplete&) override;
void react(const system_fsm::BatteryStateChanged&) override{};
using UiState::react; using UiState::react;
}; };
@ -140,10 +147,6 @@ class Playing : public UiState {
void react(const internal::BackPressed&) override; 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; using UiState::react;
}; };

@ -9,6 +9,7 @@
#include <memory> #include <memory>
#include "audio_events.hpp" #include "audio_events.hpp"
#include "bindey/binding.h"
#include "core/lv_event.h" #include "core/lv_event.h"
#include "core/lv_obj.h" #include "core/lv_obj.h"
#include "core/lv_obj_scroll.h" #include "core/lv_obj_scroll.h"
@ -35,6 +36,7 @@
#include "misc/lv_area.h" #include "misc/lv_area.h"
#include "misc/lv_color.h" #include "misc/lv_color.h"
#include "misc/lv_txt.h" #include "misc/lv_txt.h"
#include "model_playback.hpp"
#include "track.hpp" #include "track.hpp"
#include "ui_events.hpp" #include "ui_events.hpp"
#include "ui_fsm.hpp" #include "ui_fsm.hpp"
@ -46,8 +48,6 @@
namespace ui { namespace ui {
namespace screens { namespace screens {
static constexpr std::size_t kMaxUpcoming = 10;
static void above_fold_focus_cb(lv_event_t* ev) { static void above_fold_focus_cb(lv_event_t* ev) {
if (ev->user_data == NULL) { if (ev->user_data == NULL) {
return; return;
@ -64,10 +64,6 @@ static void below_fold_focus_cb(lv_event_t* ev) {
instance->OnFocusBelowFold(); instance->OnFocusBelowFold();
} }
static void play_pause_cb(lv_event_t* ev) {
events::Audio().Dispatch(audio::TogglePlayPause{});
}
static lv_style_t scrubber_style; static lv_style_t scrubber_style;
auto info_label(lv_obj_t* parent) -> lv_obj_t* { 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; 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), : db_(db),
queue_(queue), queue_(queue),
track_(), current_track_(),
next_tracks_(), next_tracks_(),
new_track_(), new_track_(),
new_next_tracks_() { 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_obj_set_layout(content_, LV_LAYOUT_FLEX);
lv_group_set_wrap(group_, false); 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_obj_set_flex_align(info_container, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
artist_label_ = info_label(info_container); lv_obj_t* artist_label = info_label(info_container);
album_label_ = info_label(info_container); lv_obj_t* album_label = info_label(info_container);
title_label_ = info_label(info_container); lv_obj_t* title_label = info_label(info_container);
scrubber_ = lv_slider_create(above_fold_container); data_bindings_.emplace_back(current_track_.onChangedAndNow(
lv_obj_set_size(scrubber_, lv_pct(100), 5); [=](const std::shared_ptr<database::Track>& t) {
lv_slider_set_range(scrubber_, 0, 100); if (!t) {
lv_slider_set_value(scrubber_, 0, LV_ANIM_OFF); 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_init(&scrubber_style);
lv_style_set_bg_color(&scrubber_style, lv_color_black()); 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_t* controls_container = lv_obj_create(above_fold_container);
lv_obj_set_size(controls_container, lv_pct(100), 20); 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_obj_set_flex_align(controls_container, LV_FLEX_ALIGN_SPACE_EVENLY,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
play_pause_control_ = control_button(controls_container, LV_SYMBOL_PLAY); lv_obj_t* play_pause_control =
lv_obj_add_event_cb(play_pause_control_, play_pause_cb, LV_EVENT_CLICKED, control_button(controls_container, LV_SYMBOL_PLAY);
NULL); lv_group_add_obj(group_, play_pause_control);
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)); lv_group_add_obj(group_, control_button(controls_container, LV_SYMBOL_LOOP));
next_up_header_ = lv_obj_create(above_fold_container); 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_obj_set_flex_align(next_up_container_, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
OnTrackUpdate(); data_bindings_.emplace_back(next_tracks_.onChangedAndNow(
OnQueueUpdate(); [=](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_);
Playing::~Playing() {} while (children > 0) {
lv_obj_del(lv_obj_get_child(next_up_container_, 0));
children--;
}
auto Playing::OnTrackUpdate() -> void { if (tracks.empty()) {
auto current = queue_.GetCurrent(); lv_label_set_text(next_up_label_, "Nothing queued");
if (!current) { lv_label_set_text(next_up_hint_, "");
return; return;
} } else {
if (track_ && track_->data().id() == *current) { lv_label_set_text(next_up_label_, "Next up");
return; lv_label_set_text(next_up_hint_, "");
} }
auto db = db_.lock();
if (!db) {
return;
}
new_track_.reset(new database::FutureFetcher<std::optional<database::Track>>(
db->GetTrack(*current)));
}
auto Playing::OnPlaybackUpdate(uint32_t pos_seconds, uint32_t new_duration) for (const auto& track : tracks) {
-> void { lv_group_add_obj(group_, next_up_label(next_up_container_,
if (!track_) { track->TitleOrFilename()));
return; }
} }));
lv_slider_set_range(scrubber_, 0, new_duration);
lv_slider_set_value(scrubber_, pos_seconds, LV_ANIM_ON);
} }
auto Playing::OnQueueUpdate() -> void { Playing::~Playing() {}
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)));
}
auto Playing::Tick() -> void { auto Playing::Tick() -> void {
if (new_track_ && new_track_->Finished()) { if (new_track_ && new_track_->Finished()) {
auto res = new_track_->Result(); auto res = new_track_->Result();
new_track_.reset(); new_track_.reset();
if (res && *res) { if (res) {
BindTrack(**res); current_track_(*res);
} }
} }
if (new_next_tracks_ && new_next_tracks_->Finished()) { if (new_next_tracks_ && new_next_tracks_->Finished()) {
auto res = new_next_tracks_->Result(); auto res = new_next_tracks_->Result();
new_next_tracks_.reset(); new_next_tracks_.reset();
if (res) { if (res) {
std::vector<database::Track> filtered; std::vector<std::shared_ptr<database::Track>> filtered;
for (const auto& t : *res) { for (const auto& t : *res) {
if (t) { 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 { auto Playing::OnFocusAboveFold() -> void {
lv_obj_scroll_to_y(content_, 0, LV_ANIM_ON); lv_obj_scroll_to_y(content_, 0, LV_ANIM_ON);
} }

@ -170,8 +170,8 @@ auto TrackBrowser::AddResults(
initial_page_ = results; initial_page_ = results;
} }
auto fn = [&](const database::IndexRecord& record) { auto fn = [&](const std::shared_ptr<database::IndexRecord>& record) {
auto text = record.text(); auto text = record->text();
if (!text) { if (!text) {
// TODO(jacqueline): Display category-specific text. // TODO(jacqueline): Display category-specific text.
text = "[ no data ]"; text = "[ no data ]";

@ -19,6 +19,7 @@
#include "gpios.hpp" #include "gpios.hpp"
#include "lvgl_task.hpp" #include "lvgl_task.hpp"
#include "modal_confirm.hpp" #include "modal_confirm.hpp"
#include "model_playback.hpp"
#include "nvs.hpp" #include "nvs.hpp"
#include "relative_wheel.hpp" #include "relative_wheel.hpp"
#include "screen.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<Screen> UiState::sCurrentScreen;
std::shared_ptr<Modal> UiState::sCurrentModal; std::shared_ptr<Modal> UiState::sCurrentModal;
models::Playback UiState::sPlaybackModel;
bindey::property<battery::Battery::BatteryState> UiState::sPropBatteryState;
auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool {
// Init LVGL first, since the display driver registers itself with LVGL. // Init LVGL first, since the display driver registers itself with LVGL.
lv_init(); lv_init();
@ -89,15 +94,33 @@ void UiState::react(const system_fsm::KeyLockChanged& ev) {
} }
void UiState::react(const system_fsm::BatteryStateChanged&) { 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&) { void UiState::react(const audio::PlaybackStarted&) {
UpdateTopBar(); sPlaybackModel.is_playing.set(true);
} }
void UiState::react(const audio::PlaybackFinished&) { 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() { void UiState::UpdateTopBar() {
@ -283,21 +306,22 @@ void Browse::react(const internal::RecordSelected& ev) {
} }
auto record = ev.page->values().at(ev.record); auto record = ev.page->values().at(ev.record);
if (record.track()) { if (record->track()) {
ESP_LOGI(kTag, "selected track '%s'", record.text()->c_str()); ESP_LOGI(kTag, "selected track '%s'", record->text()->c_str());
auto& queue = sServices->track_queue(); auto& queue = sServices->track_queue();
queue.Clear(); queue.Clear();
queue.IncludeLast(std::make_shared<playlist::IndexRecordSource>( queue.IncludeLast(std::make_shared<playlist::IndexRecordSource>(
sServices->database(), ev.initial_page, 0, ev.page, ev.record)); sServices->database(), ev.initial_page, 0, ev.page, ev.record));
ESP_LOGI(kTag, "transit to playing");
transit<Playing>(); transit<Playing>();
} else { } else {
ESP_LOGI(kTag, "selected record '%s'", record.text()->c_str()); ESP_LOGI(kTag, "selected record '%s'", record->text()->c_str());
auto cont = record.Expand(kRecordsPerPage); auto cont = record->Expand(kRecordsPerPage);
if (!cont) { if (!cont) {
return; return;
} }
auto query = db->GetPage(&cont.value()); 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>( PushScreen(std::make_shared<screens::TrackBrowser>(
sServices->database(), title, std::move(query))); sServices->database(), title, std::move(query)));
} }
@ -329,8 +353,9 @@ void Browse::react(const system_fsm::BluetoothDevicesChanged&) {
static std::shared_ptr<screens::Playing> sPlayingScreen; static std::shared_ptr<screens::Playing> sPlayingScreen;
void Playing::entry() { void Playing::entry() {
sPlayingScreen.reset( ESP_LOGI(kTag, "push playing screen");
new screens::Playing(sServices->database(), sServices->track_queue())); sPlayingScreen.reset(new screens::Playing(
sPlaybackModel, sServices->database(), sServices->track_queue()));
PushScreen(sPlayingScreen); PushScreen(sPlayingScreen);
} }
@ -339,24 +364,6 @@ void Playing::exit() {
PopScreen(); 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) { void Playing::react(const internal::BackPressed& ev) {
transit<Browse>(); transit<Browse>();
} }

@ -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/shared_string")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/span") 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/tinyfsm")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/bindey")
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)

Loading…
Cancel
Save