Use bindey for databinding instead of hand rolling ui updates
This commit is contained in:
@@ -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
|
||||||
Vendored
+44
@@ -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>;
|
||||||
|
|||||||
+55
-49
@@ -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;
|
||||||
|
|
||||||
|
bool operator==(const Track&) const = default;
|
||||||
|
|
||||||
|
auto data() const -> const TrackData& { return *data_; }
|
||||||
|
auto tags() const -> const TrackTags& { return *tags_; }
|
||||||
|
|
||||||
auto TitleOrFilename() const -> std::pmr::string;
|
auto TitleOrFilename() const -> std::pmr::string;
|
||||||
|
|
||||||
bool operator==(const Track&) const = default;
|
|
||||||
Track operator=(const Track& other) const { return Track(other); }
|
|
||||||
|
|
||||||
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' */
|
||||||
|
|||||||
+26
-24
@@ -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_;
|
||||||
|
|||||||
+13
-10
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+112
-112
@@ -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,87 +253,8 @@ 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) {
|
||||||
}
|
|
||||||
|
|
||||||
Playing::~Playing() {}
|
|
||||||
|
|
||||||
auto Playing::OnTrackUpdate() -> void {
|
|
||||||
auto current = queue_.GetCurrent();
|
|
||||||
if (!current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (track_ && track_->data().id() == *current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto db = db_.lock();
|
|
||||||
if (!db) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
new_track_.reset(new database::FutureFetcher<std::optional<database::Track>>(
|
|
||||||
db->GetTrack(*current)));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Playing::OnPlaybackUpdate(uint32_t pos_seconds, uint32_t new_duration)
|
|
||||||
-> void {
|
|
||||||
if (!track_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lv_slider_set_range(scrubber_, 0, new_duration);
|
|
||||||
lv_slider_set_value(scrubber_, pos_seconds, LV_ANIM_ON);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Playing::OnQueueUpdate() -> void {
|
|
||||||
OnTrackUpdate();
|
|
||||||
auto current = queue_.GetUpcoming(kMaxUpcoming);
|
|
||||||
auto db = db_.lock();
|
|
||||||
if (!db) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
new_next_tracks_.reset(
|
|
||||||
new database::FutureFetcher<std::vector<std::optional<database::Track>>>(
|
|
||||||
db->GetBulkTracks(current)));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto Playing::Tick() -> void {
|
|
||||||
if (new_track_ && new_track_->Finished()) {
|
|
||||||
auto res = new_track_->Result();
|
|
||||||
new_track_.reset();
|
|
||||||
if (res && *res) {
|
|
||||||
BindTrack(**res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (new_next_tracks_ && new_next_tracks_->Finished()) {
|
|
||||||
auto res = new_next_tracks_->Result();
|
|
||||||
new_next_tracks_.reset();
|
|
||||||
if (res) {
|
|
||||||
std::vector<database::Track> filtered;
|
|
||||||
for (const auto& t : *res) {
|
|
||||||
if (t) {
|
|
||||||
filtered.push_back(*t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ApplyNextUp(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.
|
// TODO(jacqueline): Do a proper diff to maintain selection.
|
||||||
int children = lv_obj_get_child_cnt(next_up_container_);
|
int children = lv_obj_get_child_cnt(next_up_container_);
|
||||||
while (children > 0) {
|
while (children > 0) {
|
||||||
@@ -286,9 +262,7 @@ auto Playing::ApplyNextUp(const std::vector<database::Track>& tracks) -> void {
|
|||||||
children--;
|
children--;
|
||||||
}
|
}
|
||||||
|
|
||||||
next_tracks_ = tracks;
|
if (tracks.empty()) {
|
||||||
|
|
||||||
if (next_tracks_.empty()) {
|
|
||||||
lv_label_set_text(next_up_label_, "Nothing queued");
|
lv_label_set_text(next_up_label_, "Nothing queued");
|
||||||
lv_label_set_text(next_up_hint_, "");
|
lv_label_set_text(next_up_hint_, "");
|
||||||
return;
|
return;
|
||||||
@@ -297,9 +271,35 @@ auto Playing::ApplyNextUp(const std::vector<database::Track>& tracks) -> void {
|
|||||||
lv_label_set_text(next_up_hint_, "");
|
lv_label_set_text(next_up_hint_, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& track : next_tracks_) {
|
for (const auto& track : tracks) {
|
||||||
lv_group_add_obj(
|
lv_group_add_obj(group_, next_up_label(next_up_container_,
|
||||||
group_, next_up_label(next_up_container_, track.TitleOrFilename()));
|
track->TitleOrFilename()));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Playing::~Playing() {}
|
||||||
|
|
||||||
|
auto Playing::Tick() -> void {
|
||||||
|
if (new_track_ && new_track_->Finished()) {
|
||||||
|
auto res = new_track_->Result();
|
||||||
|
new_track_.reset();
|
||||||
|
if (res) {
|
||||||
|
current_track_(*res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (new_next_tracks_ && new_next_tracks_->Finished()) {
|
||||||
|
auto res = new_next_tracks_->Result();
|
||||||
|
new_next_tracks_.reset();
|
||||||
|
if (res) {
|
||||||
|
std::vector<std::shared_ptr<database::Track>> filtered;
|
||||||
|
for (const auto& t : *res) {
|
||||||
|
if (t) {
|
||||||
|
filtered.push_back(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next_tracks_.set(filtered);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 ]";
|
||||||
|
|||||||
+35
-28
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user