Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cca7eb4c0c |
No files matched your search
@@ -43,6 +43,7 @@ find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 I18n CoreAddon
|
||||
find_package(pybind11 REQUIRED)
|
||||
find_package(Ytdlp REQUIRED RUNTIME)
|
||||
find_package(YTMusicAPI 0.24.0 REQUIRED RUNTIME)
|
||||
find_package(Desert REQUIRED)
|
||||
|
||||
ki18n_install(po)
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ add_executable(audiotube
|
||||
errorhandler.cpp
|
||||
playerutils.cpp
|
||||
abstractytmusicmodel.cpp
|
||||
autolistmodel.h
|
||||
multiiterableview.h
|
||||
library.cpp
|
||||
resources.qrc
|
||||
@@ -51,6 +52,7 @@ target_link_libraries(audiotube
|
||||
KF5::CoreAddons
|
||||
KF5::Crash
|
||||
KF5::WindowSystem
|
||||
Desert::desert
|
||||
ytm
|
||||
asyncdatabase
|
||||
)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// SPDX-FileCopyrightText: 2023 Jonah Brüchert <[email protected]>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDebug>
|
||||
|
||||
#include <Desert/Reflection>
|
||||
|
||||
#include "updateablemodel.h"
|
||||
|
||||
template <typename T>
|
||||
using AutoListModelBase = UpdateableModel<QAbstractListModel, T>;
|
||||
|
||||
template <typename T>
|
||||
concept IsComparable = requires(T v, T o)
|
||||
{
|
||||
{ v == o } -> std::same_as<bool>;
|
||||
{ v < o } -> std::same_as<bool>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
requires desert::IsDesertObject<T> && IsComparable<T>
|
||||
class AutoListModel : public AutoListModelBase<T>
|
||||
{
|
||||
public:
|
||||
AutoListModel(QObject *parent = nullptr) : AutoListModelBase<T>(parent) {
|
||||
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override {
|
||||
QHash<int, QByteArray> roles;
|
||||
int i = Qt::UserRole + 1;
|
||||
T mock_obj;
|
||||
desert::iterate_struct(mock_obj, [&](auto attribute) {
|
||||
roles.insert(i, attribute.attributeName().toUtf8());
|
||||
i++;
|
||||
});
|
||||
return roles;
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override {
|
||||
return parent.isValid() ? 0 : m_data.size();
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override {
|
||||
int structIndex = role - Qt::UserRole - 1;
|
||||
if (structIndex >= pfr::tuple_size_v<T>) {
|
||||
qDebug() << "Tried to fetch a role that is not defined by this model. Role id:" << role;
|
||||
return {};
|
||||
}
|
||||
return structAttributeAt<0, T>(m_data.at(index.row()), structIndex);
|
||||
}
|
||||
|
||||
void updateData(std::vector<T> &&updatedData) {
|
||||
// allow comparing items. Could also have used an operator< on Data
|
||||
auto lessThan = [](const T &lhs, const T &rhs) { return lhs < rhs; };
|
||||
// check if the item has changed data, but is otherwise the same object. Needs to return the actual changes made.
|
||||
auto itemHasChanged = [](const T &lhs, const T &rhs) {
|
||||
typename AutoListModelBase<T>::DataChanges changes;
|
||||
if (lhs != rhs)
|
||||
changes.changedColumns.append(0);
|
||||
changes.changedRoles = QVector<int>{}; // TODO more specific updates
|
||||
return changes;
|
||||
};
|
||||
|
||||
// call the updateData method to trigger merging in the changes to the model
|
||||
auto changes = AutoListModelBase<T>::updateData(updatedData.cbegin(), updatedData.cend(), m_data, lessThan, itemHasChanged);
|
||||
|
||||
// the method above returns some stats, so lets print them.
|
||||
qDebug() << "changes in model: inserts:" << changes.inserts << " deletes:" << changes.removals
|
||||
<< " updates:" << changes.updates;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<T> m_data;
|
||||
};
|
||||
|
||||
template <size_t i, typename T>
|
||||
requires desert::IsDesertObject<T>
|
||||
QVariant structAttributeAt(const T &object, size_t index) {
|
||||
if constexpr (i >= pfr::tuple_size_v<T>) {
|
||||
Q_UNREACHABLE();
|
||||
} else if (i == index) {
|
||||
return QVariant::fromValue(pfr::get<i>(object).value);
|
||||
} else {
|
||||
return structAttributeAt<i + 1>(object, index);
|
||||
}
|
||||
}
|
||||
@@ -59,8 +59,7 @@ Kirigami.ScrollablePage {
|
||||
delegate: ColumnLayout {
|
||||
id: delegateItem
|
||||
required property string title
|
||||
required property var artists
|
||||
required property string artistsDisplayString
|
||||
required property string artists
|
||||
required property string videoId
|
||||
required property int index
|
||||
|
||||
@@ -183,7 +182,7 @@ Kirigami.ScrollablePage {
|
||||
Layout.maximumWidth: 200
|
||||
leftPadding: 5
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
text: delegateItem.artistsDisplayString
|
||||
text: delegateItem.artists
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
@@ -192,8 +191,8 @@ Kirigami.ScrollablePage {
|
||||
icon.name: "view-more-horizontal-symbolic"
|
||||
onPressed: menu.openForSong(delegateItem.videoId,
|
||||
delegateItem.title,
|
||||
delegateItem.artists,
|
||||
delegateItem.artistsDisplayString)
|
||||
delegateItem.artists, // TODO
|
||||
delegateItem.artists)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -244,8 +243,7 @@ Kirigami.ScrollablePage {
|
||||
delegate: ColumnLayout {
|
||||
id: mpdelegateItem
|
||||
required property string title
|
||||
required property var artists
|
||||
required property string artistsDisplayString
|
||||
required property string artists
|
||||
required property string videoId
|
||||
|
||||
Layout.fillWidth: false
|
||||
@@ -365,7 +363,7 @@ Kirigami.ScrollablePage {
|
||||
Layout.maximumWidth: 200
|
||||
leftPadding: 5
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
text: mpdelegateItem.artistsDisplayString
|
||||
text: mpdelegateItem.artists
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
@@ -374,8 +372,8 @@ Kirigami.ScrollablePage {
|
||||
icon.name: "view-more-horizontal-symbolic"
|
||||
onPressed: menu.openForSong(mpdelegateItem.videoId,
|
||||
mpdelegateItem.title,
|
||||
mpdelegateItem.artists,
|
||||
mpdelegateItem.artistsDisplayString)
|
||||
mpdelegateItem.artists, // TODO
|
||||
mpdelegateItem.artists)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@ Kirigami.ScrollablePage {
|
||||
|
||||
required property string title
|
||||
required property string videoId
|
||||
required property var artists
|
||||
required property string artistsDisplayString
|
||||
required property string artists
|
||||
required property int index
|
||||
|
||||
onClicked: {
|
||||
|
||||
@@ -49,7 +49,7 @@ Item {
|
||||
pageStack.push("qrc:/SearchPage.qml", {
|
||||
"searchQuery": text,
|
||||
objectName: "searchPage"
|
||||
})
|
||||
})
|
||||
} else {
|
||||
wideScreen
|
||||
? pageStack.push("qrc:/LibraryPage.qml")
|
||||
@@ -299,6 +299,8 @@ Item {
|
||||
filterRegularExpression: searchField.filterExpression
|
||||
}
|
||||
delegate: Kirigami.AbstractListItem {
|
||||
required property string query
|
||||
|
||||
id: completionDelegate
|
||||
highlighted: focus
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
@@ -314,7 +316,7 @@ Item {
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
Controls.Label{
|
||||
text: model.display
|
||||
text: query
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Controls.ToolButton {
|
||||
@@ -322,13 +324,13 @@ Item {
|
||||
text: i18n("remove from search history")
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
Library.removeSearch(model.display)
|
||||
Library.removeSearch(query)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
onClicked: {
|
||||
searchField.text = model.display
|
||||
searchField.text = query
|
||||
searchField.accepted()
|
||||
}
|
||||
}
|
||||
|
||||
+33
-106
@@ -40,8 +40,7 @@ Library &Library::instance()
|
||||
|
||||
FavouritesModel *Library::favourites()
|
||||
{
|
||||
auto future = m_database->getResults<Song>("select * from favourites natural join songs order by favourites.rowid desc");
|
||||
return new FavouritesModel(std::move(future), this);
|
||||
return new FavouritesModel(this);
|
||||
}
|
||||
|
||||
void Library::addFavourite(const QString &videoId, const QString &title, const QString &artist, const QString &album)
|
||||
@@ -66,7 +65,7 @@ FavouriteWatcher *Library::favouriteWatcher(const QString &videoId)
|
||||
|
||||
SearchHistoryModel *Library::searches()
|
||||
{
|
||||
return new SearchHistoryModel(m_database->getResults<SingleValue<QString>>("select distinct (search_query) from searches order by search_id desc"), this);
|
||||
return new SearchHistoryModel(this);
|
||||
}
|
||||
|
||||
void Library::addSearch(const QString &text)
|
||||
@@ -81,8 +80,7 @@ void Library::removeSearch(const QString &text)
|
||||
|
||||
PlaybackHistoryModel *Library::playbackHistory()
|
||||
{
|
||||
auto future = m_database->getResults<PlayedSong>("select * from played_songs natural join songs");
|
||||
return new PlaybackHistoryModel(std::move(future), this);
|
||||
return new PlaybackHistoryModel(this, PlaybackHistoryModel::History);
|
||||
}
|
||||
|
||||
void Library::addPlaybackHistoryItem(const QString &videoId, const QString &title, const QString &artist, const QString &album)
|
||||
@@ -110,8 +108,7 @@ WasPlayedWatcher *Library::wasPlayedWatcher(const QString& videoId)
|
||||
|
||||
PlaybackHistoryModel *Library::mostPlayed()
|
||||
{
|
||||
auto future = m_database->getResults<PlayedSong>("select * from played_songs natural join songs order by plays desc limit 10");
|
||||
return new PlaybackHistoryModel(std::move(future), this);
|
||||
return new PlaybackHistoryModel(this, PlaybackHistoryModel::MostPlayed);
|
||||
}
|
||||
|
||||
QNetworkAccessManager &Library::nam()
|
||||
@@ -171,93 +168,35 @@ void ThumbnailSource::setVideoId(const QString &id) {
|
||||
});
|
||||
}
|
||||
|
||||
PlaybackHistoryModel::PlaybackHistoryModel(QFuture<std::vector<PlayedSong> > &&songs, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
PlaybackHistoryModel::PlaybackHistoryModel(Library *library, Type type)
|
||||
: AutoListModel<PlayedSong>(library)
|
||||
{
|
||||
connectFuture(songs, this, [this](const auto songs) {
|
||||
beginResetModel();
|
||||
m_playedSongs = songs;
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> PlaybackHistoryModel::roleNames() const {
|
||||
return {
|
||||
{Roles::VideoId, "videoId"},
|
||||
{Roles::Title, "title"},
|
||||
{Roles::Artists, "artists"},
|
||||
{Roles::ArtistsDisplayString, "artistsDisplayString"},
|
||||
{Roles::Plays, "plays"}
|
||||
};
|
||||
}
|
||||
|
||||
int PlaybackHistoryModel::rowCount(const QModelIndex &parent) const {
|
||||
return parent.isValid() ? 0 : m_playedSongs.size();
|
||||
}
|
||||
|
||||
QVariant PlaybackHistoryModel::data(const QModelIndex &index, int role) const {
|
||||
switch (role) {
|
||||
case Roles::VideoId:
|
||||
return m_playedSongs.at(index.row()).videoId;
|
||||
case Roles::Title:
|
||||
return m_playedSongs.at(index.row()).title;
|
||||
case Roles::Artists:
|
||||
return QVariant::fromValue(std::vector<meta::Artist> {
|
||||
{
|
||||
m_playedSongs.at(index.row()).artist.toStdString(),
|
||||
{}
|
||||
auto update = [=, this]() {
|
||||
auto future = [=, this]() {
|
||||
if (type == MostPlayed) {
|
||||
return library->database().getResults<PlayedSong>("select * from played_songs natural join songs order by plays desc limit 10");
|
||||
} else {
|
||||
return library->database().getResults<PlayedSong>("select * from played_songs natural join songs");
|
||||
}
|
||||
});
|
||||
case Roles::ArtistsDisplayString:
|
||||
return m_playedSongs.at(index.row()).artist;
|
||||
case Roles::Plays:
|
||||
return m_playedSongs.at(index.row()).plays;
|
||||
}
|
||||
}();
|
||||
|
||||
Q_UNREACHABLE();
|
||||
connectFuture(future, this, &PlaybackHistoryModel::updateData);
|
||||
};
|
||||
|
||||
update();
|
||||
connect(library, &Library::playbackHistoryChanged, this, update);
|
||||
}
|
||||
|
||||
FavouritesModel::FavouritesModel(QFuture<std::vector<Song>> &&songs, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
FavouritesModel::FavouritesModel(Library *library)
|
||||
: AutoListModel<Song>(library)
|
||||
{
|
||||
connectFuture(songs, this, [this](const auto songs) {
|
||||
beginResetModel();
|
||||
m_favouriteSongs = songs;
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> FavouritesModel::roleNames() const {
|
||||
return {
|
||||
{Roles::VideoId, "videoId"},
|
||||
{Roles::Title, "title"},
|
||||
{Roles::Artists, "artists"},
|
||||
{Roles::ArtistsDisplayString, "artistsDisplayString"}
|
||||
auto update = [=, this]() {
|
||||
auto future = library->database().getResults<Song>("select * from favourites natural join songs order by favourites.rowid desc");
|
||||
connectFuture(future, this, &FavouritesModel::updateData);
|
||||
};
|
||||
}
|
||||
|
||||
int FavouritesModel::rowCount(const QModelIndex &parent) const {
|
||||
return parent.isValid() ? 0 : m_favouriteSongs.size();
|
||||
}
|
||||
|
||||
QVariant FavouritesModel::data(const QModelIndex &index, int role) const {
|
||||
switch (role) {
|
||||
case Roles::VideoId:
|
||||
return m_favouriteSongs.at(index.row()).videoId;
|
||||
case Roles::Title:
|
||||
return m_favouriteSongs.at(index.row()).title;
|
||||
case Roles::ArtistsDisplayString:
|
||||
return m_favouriteSongs.at(index.row()).artist;
|
||||
case Roles::Artists:
|
||||
return QVariant::fromValue(std::vector<meta::Artist> {
|
||||
{
|
||||
m_favouriteSongs.at(index.row()).artist.toStdString(),
|
||||
{}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
update();
|
||||
connect(library, &Library::favouritesChanged, this, update);
|
||||
}
|
||||
|
||||
FavouriteWatcher::FavouriteWatcher(Library *library, const QString &videoId)
|
||||
@@ -279,27 +218,15 @@ bool FavouriteWatcher::isFavourite() const {
|
||||
return m_isFavourite;
|
||||
}
|
||||
|
||||
SearchHistoryModel::SearchHistoryModel(QFuture<std::vector<SingleValue<QString>>> &&historyFuture, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
SearchHistoryModel::SearchHistoryModel(Library *library)
|
||||
: AutoListModel<SearchQuery>(library)
|
||||
{
|
||||
connectFuture(historyFuture, this, [this](const auto history) {
|
||||
beginResetModel();
|
||||
m_history = history;
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
int SearchHistoryModel::rowCount(const QModelIndex &parent) const {
|
||||
return parent.isValid() ? 0 : m_history.size();
|
||||
}
|
||||
|
||||
QVariant SearchHistoryModel::data(const QModelIndex &index, int role) const {
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
return m_history[index.row()].value;
|
||||
}
|
||||
|
||||
Q_UNREACHABLE();
|
||||
auto update = [=, this]() {
|
||||
auto future = library->database().getResults<SearchQuery>("select distinct (search_query) from searches order by search_id desc");
|
||||
connectFuture(future, this, &SearchHistoryModel::updateData);
|
||||
};
|
||||
update();
|
||||
connect(library, &Library::searchesChanged, update);
|
||||
}
|
||||
|
||||
WasPlayedWatcher::WasPlayedWatcher(Library* library, const QString& videoId)
|
||||
|
||||
+56
-51
@@ -15,10 +15,22 @@
|
||||
#include "asyncytmusic.h"
|
||||
#include "asyncdatabase.h"
|
||||
|
||||
#include "autolistmodel.h"
|
||||
|
||||
using namespace desert;
|
||||
|
||||
class FavouriteWatcher;
|
||||
class WasPlayedWatcher;
|
||||
|
||||
inline std::vector<meta::Artist> metaArtist(const QString &artist) {
|
||||
return { meta::Artist { artist.toStdString(), {} } };
|
||||
}
|
||||
|
||||
struct Song {
|
||||
DESERT_OBJECT
|
||||
|
||||
int operator<=>(const Song &other) const = default;
|
||||
|
||||
using ColumnTypes = std::tuple<QString, QString, QString, QString>;
|
||||
|
||||
static Song fromSql(ColumnTypes tuple) {
|
||||
@@ -26,34 +38,26 @@ struct Song {
|
||||
return Song {videoId, title, artist, album};
|
||||
}
|
||||
|
||||
QString videoId;
|
||||
QString title;
|
||||
QString artist;
|
||||
QString album;
|
||||
Attribute<u"videoId", QString> videoId;
|
||||
Attribute<u"title", QString> title;
|
||||
Attribute<u"artists", QString> artists;
|
||||
Attribute<u"album", QString> album;
|
||||
};
|
||||
|
||||
class FavouritesModel : public QAbstractListModel {
|
||||
class Library;
|
||||
|
||||
class FavouritesModel : public AutoListModel<Song> {
|
||||
Q_OBJECT
|
||||
|
||||
enum Roles {
|
||||
VideoId = Qt::UserRole + 1,
|
||||
Title,
|
||||
Artists,
|
||||
ArtistsDisplayString
|
||||
};
|
||||
|
||||
public:
|
||||
FavouritesModel(QFuture<std::vector<Song>> &&songs, QObject *parent = nullptr);
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
private:
|
||||
std::vector<Song> m_favouriteSongs;
|
||||
FavouritesModel(Library *library);
|
||||
};
|
||||
|
||||
struct PlayedSong {
|
||||
DESERT_OBJECT
|
||||
|
||||
int operator<=>(const PlayedSong &other) const = default;
|
||||
|
||||
using ColumnTypes = std::tuple<QString, int, QString, QString, QString>;
|
||||
|
||||
static PlayedSong fromSql(ColumnTypes tuple) {
|
||||
@@ -61,55 +65,56 @@ struct PlayedSong {
|
||||
return PlayedSong {videoId, title, artist, album, plays};
|
||||
}
|
||||
|
||||
QString videoId;
|
||||
QString title;
|
||||
QString artist;
|
||||
QString album;
|
||||
int plays;
|
||||
Attribute<u"videoId", QString> videoId;
|
||||
Attribute<u"title", QString> title;
|
||||
Attribute<u"artists", QString> artists;
|
||||
Attribute<u"album", QString> album;
|
||||
Attribute<u"album", int> plays;
|
||||
};
|
||||
|
||||
class PlaybackHistoryModel : public QAbstractListModel {
|
||||
class PlaybackHistoryModel : public AutoListModel<PlayedSong> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
VideoId = Qt::UserRole + 1,
|
||||
Title,
|
||||
Artists,
|
||||
ArtistsDisplayString,
|
||||
Plays
|
||||
enum Type {
|
||||
MostPlayed,
|
||||
History
|
||||
};
|
||||
Q_ENUM(Roles);
|
||||
|
||||
PlaybackHistoryModel(QFuture<std::vector<PlayedSong>> &&songs, QObject *parent = nullptr);
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
private:
|
||||
std::vector<PlayedSong> m_playedSongs;
|
||||
PlaybackHistoryModel(Library *library, Type type);
|
||||
};
|
||||
|
||||
class SearchHistoryModel : public QAbstractListModel {
|
||||
class SearchQuery {
|
||||
DESERT_OBJECT
|
||||
|
||||
using ColumnTypes = std::tuple<QString>;
|
||||
|
||||
static SearchQuery fromSql(ColumnTypes tuple) {
|
||||
auto [query] = tuple;
|
||||
return { query };
|
||||
}
|
||||
|
||||
int operator<=>(const SearchQuery &other) const = default;
|
||||
|
||||
Attribute<u"query", QString> query;
|
||||
};
|
||||
|
||||
class SearchHistoryModel : public AutoListModel<SearchQuery> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SearchHistoryModel(QFuture<std::vector<SingleValue<QString>>> &&historyFuture, QObject *parent = nullptr);
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
SearchHistoryModel(Library *library);
|
||||
|
||||
private:
|
||||
std::vector<SingleValue<QString>> m_history;
|
||||
std::vector<SearchQuery> m_history;
|
||||
};
|
||||
|
||||
class Library : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QAbstractListModel *favourites READ favourites NOTIFY favouritesChanged)
|
||||
Q_PROPERTY(QAbstractListModel *searches READ searches NOTIFY searchesChanged)
|
||||
Q_PROPERTY(QAbstractListModel *playbackHistory READ playbackHistory NOTIFY playbackHistoryChanged)
|
||||
Q_PROPERTY(QAbstractListModel *mostPlayed READ mostPlayed NOTIFY playbackHistoryChanged)
|
||||
Q_PROPERTY(QAbstractListModel *favourites READ favourites CONSTANT)
|
||||
Q_PROPERTY(QAbstractListModel *searches READ searches CONSTANT)
|
||||
Q_PROPERTY(QAbstractListModel *playbackHistory READ playbackHistory CONSTANT)
|
||||
Q_PROPERTY(QAbstractListModel *mostPlayed READ mostPlayed CONSTANT)
|
||||
|
||||
public:
|
||||
explicit Library(QObject *parent = nullptr);
|
||||
|
||||
@@ -10,20 +10,20 @@
|
||||
#include <QtGlobal>
|
||||
|
||||
template <typename Tuple, typename Func, std::size_t i>
|
||||
inline constexpr void iterate_impl(Tuple &tup, Func fun)
|
||||
inline constexpr void iterate_tuple_impl(Tuple &tup, Func fun)
|
||||
{
|
||||
if constexpr(i >= std::tuple_size_v<std::decay_t<decltype(tup)>>) {
|
||||
return;
|
||||
} else {
|
||||
fun(std::get<i>(tup));
|
||||
return iterate_impl<Tuple, Func, i + 1>(tup, fun);
|
||||
return iterate_tuple_impl<Tuple, Func, i + 1>(tup, fun);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Tuple, typename Func>
|
||||
inline constexpr void iterate_tuple(Tuple &tup, Func fun)
|
||||
{
|
||||
iterate_impl<Tuple, Func, 0>(tup, fun);
|
||||
iterate_tuple_impl<Tuple, Func, 0>(tup, fun);
|
||||
}
|
||||
|
||||
/// MultiIterableView is a view over multiple contigous memory arrays, like std::vector or std::array.
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
/****************************************************************************
|
||||
** MIT License
|
||||
**
|
||||
** Copyright (C) 2018-2023 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
|
||||
** Author: André Somers <[email protected]>
|
||||
**
|
||||
** This file is part of KDToolBox (https://github.com/KDAB/KDToolBox).
|
||||
**
|
||||
** 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 (including the next paragraph)
|
||||
** 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.
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef UPDATEABLEMODEL_H
|
||||
#define UPDATEABLEMODEL_H
|
||||
|
||||
#include <QModelIndex>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
#include <functional>
|
||||
|
||||
// concepts
|
||||
#define ForwardIt typename
|
||||
#define RandomIt typename
|
||||
#define Container typename
|
||||
#define BinaryPredicate typename
|
||||
#define EventHandler typename
|
||||
#define QAIM typename // QAbstractItemModel-derived class
|
||||
|
||||
// basic algorithm
|
||||
|
||||
// precondition: src and target are both ordered with respect to lessThan
|
||||
template<ForwardIt FwdIt, Container TargetCollection, BinaryPredicate LessThan, BinaryPredicate HasChanged,
|
||||
EventHandler OnChanged, EventHandler OnInsert, EventHandler OnRemove, EventHandler OnEqual>
|
||||
void updateCollection(const FwdIt srcBegin, const FwdIt srcEnd, TargetCollection &target, LessThan lessThan,
|
||||
HasChanged itemHasChanged, OnChanged onChanged, OnInsert onInsert, OnRemove onRemove,
|
||||
OnEqual onEqual)
|
||||
{
|
||||
auto srcIt = srcBegin;
|
||||
target.reserve(std::distance(srcBegin, srcEnd));
|
||||
auto targetIt = std::begin(target);
|
||||
|
||||
while (srcIt != srcEnd)
|
||||
{
|
||||
if (targetIt == std::end(target))
|
||||
{
|
||||
// insert: src has still items left while target has no more items
|
||||
// so, insert all remaining items from src into target
|
||||
targetIt = onInsert(srcIt, srcEnd, targetIt);
|
||||
// targetIt now points to the item after the item(s) just inserted
|
||||
srcIt = srcEnd;
|
||||
}
|
||||
else if (lessThan(*srcIt, *targetIt))
|
||||
{
|
||||
// insert: src has one or more items that need to be inserted into target
|
||||
auto srcInsertEnd = std::next(srcIt);
|
||||
while (srcInsertEnd != srcEnd && lessThan(*srcInsertEnd, *targetIt))
|
||||
{
|
||||
srcInsertEnd++;
|
||||
}
|
||||
targetIt = onInsert(srcIt, srcInsertEnd, targetIt);
|
||||
// targetIt now points to the item after the item(s) just inserted
|
||||
srcIt = srcInsertEnd;
|
||||
}
|
||||
else if (lessThan(*targetIt, *srcIt))
|
||||
{
|
||||
// removal: target has items that are not in src (any more), so remove them
|
||||
auto targetRemoveEnd = std::next(targetIt);
|
||||
auto targetEnd = std::end(target);
|
||||
while (targetRemoveEnd != targetEnd && lessThan(*targetRemoveEnd, *srcIt))
|
||||
{
|
||||
targetRemoveEnd++;
|
||||
}
|
||||
targetIt = onRemove(targetIt, targetRemoveEnd);
|
||||
// targetIt now points to the item after the items just removed,
|
||||
// the same item targetEnd was pointing to before the call
|
||||
}
|
||||
else
|
||||
{
|
||||
// same item, check for changes
|
||||
auto changes = itemHasChanged(*srcIt, *targetIt);
|
||||
if (changes)
|
||||
{
|
||||
onChanged(srcIt, targetIt, changes);
|
||||
}
|
||||
else
|
||||
{
|
||||
onEqual(srcIt, targetIt);
|
||||
}
|
||||
srcIt++;
|
||||
targetIt++;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetIt != std::end(target))
|
||||
{
|
||||
// target has some items left, but we're at the end of src, so remove the rest from target
|
||||
onRemove(targetIt, std::end(target));
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
namespace
|
||||
{
|
||||
// helper function to create a QSet<T> out of any container<T>.
|
||||
template<typename T>
|
||||
auto setFromContainer(T container) -> QSet<typename T::value_type>
|
||||
{
|
||||
QSet<typename T::value_type> set;
|
||||
set.reserve(container.size());
|
||||
for (const auto value : container)
|
||||
{
|
||||
set.insert(value);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
// helper for manual overload ranking
|
||||
// highest rank is selected first
|
||||
template<size_t Rank>
|
||||
struct OverloadRanker;
|
||||
template<>
|
||||
struct OverloadRanker<0>
|
||||
{
|
||||
};
|
||||
template<size_t Rank>
|
||||
struct OverloadRanker : OverloadRanker<Rank - 1>
|
||||
{
|
||||
};
|
||||
|
||||
// insert range into container, 2 overloads (using SFINAE) with manual ranking in case of multiple matches
|
||||
constexpr size_t BestInsertRangeOverloadRank = 1;
|
||||
|
||||
template<Container C, RandomIt InsertIt, ForwardIt FwdIt>
|
||||
auto insertRange_imp(C &container, InsertIt targetInsertBefore, FwdIt sourceBegin, FwdIt sourceEnd,
|
||||
OverloadRanker<1>) // Best match
|
||||
-> decltype(container.insert(targetInsertBefore, sourceBegin, sourceEnd), InsertIt())
|
||||
{
|
||||
// primary option: use insert with a whole range directly
|
||||
InsertIt it =
|
||||
std::next(container.insert(targetInsertBefore, sourceBegin, sourceEnd), std::distance(sourceBegin, sourceEnd));
|
||||
return it;
|
||||
}
|
||||
|
||||
template<Container C, RandomIt InsertIt, ForwardIt FwdIt>
|
||||
auto insertRange_imp(C &container, InsertIt targetInsertBefore, FwdIt sourceBegin, FwdIt sourceEnd,
|
||||
OverloadRanker<0>) // Fallback
|
||||
-> InsertIt
|
||||
{
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(5, 13, 0))
|
||||
// Check if, when using QVector in Qt , the value type is default constructable
|
||||
static_assert(!std::is_same<QVector<typename C::value_type>, C>::value ||
|
||||
std::is_default_constructible<typename C::value_type>::value,
|
||||
"When using QVector as your container type, the value type must be default-contructible. Consider "
|
||||
"using std::vector instead or use a more recent Qt version.");
|
||||
#endif
|
||||
|
||||
// store the location and the length of the inserted items
|
||||
const auto location = std::distance(std::begin(container), targetInsertBefore);
|
||||
const auto originalCount = container.size();
|
||||
|
||||
// append the new items to the container
|
||||
std::copy(sourceBegin, sourceEnd, std::back_inserter(container));
|
||||
|
||||
// move the items into the right position in the container
|
||||
const auto b = std::begin(container);
|
||||
targetInsertBefore = std::next(b, location);
|
||||
const InsertIt originalEnd = std::next(b, originalCount);
|
||||
return std::rotate(targetInsertBefore, originalEnd, std::end(container));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a range into the target container
|
||||
* @returns an iterator pointing one item past the last item inserted
|
||||
* @param container is the target container
|
||||
* @param targetInsertBefore iterator to the item the new items should be inserted before
|
||||
* @param sourceBegin iterator to the first item to insert into @arg container
|
||||
* @param sourceEnd iterator to the item past the last item to insert into @arg container
|
||||
*
|
||||
* Implementation is done using SFINEA to select an efficient method for the type of
|
||||
* container used.
|
||||
*/
|
||||
|
||||
// "public API"
|
||||
template<Container C, RandomIt InsertIt, ForwardIt FwdIt>
|
||||
InsertIt insertRange(C &container, InsertIt targetInsertBefore, FwdIt sourceBegin, FwdIt sourceEnd)
|
||||
{
|
||||
return insertRange_imp(container, targetInsertBefore, sourceBegin, sourceEnd,
|
||||
OverloadRanker<BestInsertRangeOverloadRank>{});
|
||||
}
|
||||
|
||||
// less than on data type
|
||||
constexpr size_t BestDataLessThanOverloadRank = 1;
|
||||
|
||||
template<typename T>
|
||||
auto dataLessThan_imp(const T &lhs, const T &rhs, OverloadRanker<1>) -> decltype(operator<(lhs, rhs), bool())
|
||||
{
|
||||
return operator<(lhs, rhs);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto dataLessThan_imp(const T &lhs, const T &rhs, OverloadRanker<0>) -> decltype(bool())
|
||||
{
|
||||
// fake implementation to stop the compiler from whining. User is probably providing his own via function pointer
|
||||
Q_UNUSED(lhs);
|
||||
Q_UNUSED(rhs);
|
||||
return false;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool dataLessThan(const T &lhs, const T &rhs)
|
||||
{
|
||||
return dataLessThan_imp(lhs, rhs, OverloadRanker<BestDataLessThanOverloadRank>{});
|
||||
}
|
||||
}
|
||||
|
||||
// actual class to inherit from
|
||||
template<QAIM BaseModel, typename DataType>
|
||||
class UpdateableModel : public BaseModel
|
||||
{
|
||||
// can't use Q_OBJECT on templates
|
||||
|
||||
public:
|
||||
explicit UpdateableModel(QObject *parent = nullptr)
|
||||
: BaseModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
protected: // types
|
||||
struct Operations
|
||||
{
|
||||
uint inserts;
|
||||
uint removals;
|
||||
uint updates;
|
||||
};
|
||||
|
||||
struct DataChanges
|
||||
{
|
||||
operator bool() const { return !changedColumns.isEmpty(); }
|
||||
QVector<int> changedColumns; // important: keep ordered!
|
||||
QVector<int> changedRoles;
|
||||
};
|
||||
|
||||
enum ChangeMergePolicy
|
||||
{
|
||||
AlwaysMergeNeighbouringRows,
|
||||
MergeWhenColumnsMatch,
|
||||
MergeWhenRolesMatch,
|
||||
MergeOnPerfectMatch
|
||||
};
|
||||
|
||||
using HasChangesFunction = std::function<DataChanges(const DataType & /*lhs*/, const DataType & /*rhs*/)>;
|
||||
|
||||
protected: // methods
|
||||
void setChangeMergePolicy(ChangeMergePolicy policy) { m_changeMergePolicy = policy; }
|
||||
|
||||
ChangeMergePolicy changeMergePolicy() const { return m_changeMergePolicy; }
|
||||
|
||||
virtual bool lessThan(const DataType &lhs, const DataType &rhs) const { return dataLessThan(lhs, rhs); }
|
||||
|
||||
virtual DataChanges itemHasChanged(const DataType &lhs, const DataType &rhs) const
|
||||
{
|
||||
Q_UNUSED(lhs);
|
||||
Q_UNUSED(rhs);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
template<ForwardIt Iterator, Container DataContainer, BinaryPredicate LessThan>
|
||||
Operations updateData(Iterator srcBegin, Iterator srcEnd, DataContainer &targetContainer, LessThan lessThan,
|
||||
HasChangesFunction itemHasChanged)
|
||||
{
|
||||
Operations ops{0, 0, 0};
|
||||
|
||||
// events
|
||||
auto onChanged = [this, &targetContainer, &ops](Iterator lhs, typename DataContainer::iterator rhs,
|
||||
const DataChanges &changes) {
|
||||
*rhs = *lhs;
|
||||
|
||||
int row = std::distance(targetContainer.begin(), rhs);
|
||||
addChange(row, changes.changedColumns, changes.changedRoles);
|
||||
|
||||
ops.updates++;
|
||||
};
|
||||
|
||||
auto onInsert = [this, &targetContainer, &ops](Iterator lhsBegin, Iterator lhsEnd,
|
||||
typename DataContainer::iterator rhsInsertAt) {
|
||||
flushCachedChanges();
|
||||
int firstNewRow = std::distance(targetContainer.begin(), rhsInsertAt);
|
||||
int lastNewRow = firstNewRow + std::distance(lhsBegin, lhsEnd) - 1;
|
||||
beginInsertRows(firstNewRow, lastNewRow);
|
||||
auto rangeEnd = insertRange(targetContainer, rhsInsertAt, lhsBegin, lhsEnd);
|
||||
endInsertRows();
|
||||
|
||||
ops.inserts += lhsEnd - lhsBegin;
|
||||
|
||||
return rangeEnd;
|
||||
};
|
||||
|
||||
auto onRemove = [this, &targetContainer, &ops](typename DataContainer::iterator rhsBegin,
|
||||
typename DataContainer::iterator rhsEnd) {
|
||||
flushCachedChanges();
|
||||
const auto targetContainerBegin = std::begin(targetContainer);
|
||||
int firstRemovedRow = std::distance(targetContainerBegin, rhsBegin);
|
||||
int lastRemovedRow = std::distance(targetContainerBegin, rhsEnd) - 1;
|
||||
beginRemoveRows(firstRemovedRow, lastRemovedRow);
|
||||
auto newRhsEnd = targetContainer.erase(rhsBegin, rhsEnd);
|
||||
endRemoveRows();
|
||||
|
||||
ops.removals += std::distance(rhsBegin, rhsEnd);
|
||||
|
||||
return newRhsEnd;
|
||||
};
|
||||
|
||||
auto onEqual = [this](Iterator /*lhs*/, typename DataContainer::iterator /*rhs*/) { flushCachedChanges(); };
|
||||
|
||||
updateCollection(srcBegin, srcEnd, targetContainer, lessThan, itemHasChanged, onChanged, onInsert, onRemove,
|
||||
onEqual);
|
||||
flushCachedChanges();
|
||||
|
||||
return ops;
|
||||
}
|
||||
|
||||
template<ForwardIt Iterator, Container Data>
|
||||
Operations updateData(Iterator srcBegin, Iterator srcEnd, Data &targetContainer, HasChangesFunction hasChanged = {})
|
||||
{
|
||||
auto lessThanFunction = [this](const DataType &lhs, const DataType &rhs) { return this->lessThan(lhs, rhs); };
|
||||
if (!hasChanged)
|
||||
{
|
||||
hasChanged = [this](const DataType &lhs, const DataType &rhs) -> DataChanges {
|
||||
return this->itemHasChanged(lhs, rhs);
|
||||
};
|
||||
}
|
||||
|
||||
return updateData(srcBegin, srcEnd, targetContainer, lessThanFunction, hasChanged);
|
||||
}
|
||||
|
||||
protected:
|
||||
// Shadowing the methods in the base model, because MSVC thinks the onRemove and onInsert
|
||||
// lambdas in updateData are not allowed to access the protected member functions in BaseClass.
|
||||
// GCC is ok with it...
|
||||
void beginInsertRows(int firstRow, int lastRow) { BaseModel::beginInsertRows(QModelIndex(), firstRow, lastRow); }
|
||||
|
||||
void endInsertRows() { BaseModel::endInsertRows(); }
|
||||
|
||||
void beginRemoveRows(int firstRow, int lastRow) { BaseModel::beginRemoveRows(QModelIndex(), firstRow, lastRow); }
|
||||
|
||||
void endRemoveRows() { BaseModel::endRemoveRows(); }
|
||||
|
||||
private:
|
||||
void flushCachedChanges()
|
||||
{
|
||||
if (m_firstChangedRow == -1)
|
||||
return;
|
||||
|
||||
// reconstruct vector of changed roles
|
||||
QVector<int> roles;
|
||||
roles.reserve(m_changedRoles.count());
|
||||
for (int role : qAsConst(m_changedRoles))
|
||||
{
|
||||
roles.append(role);
|
||||
}
|
||||
|
||||
// convert the vector of ints indicating changed columns into a vector of pairs of ints indicating ranges.
|
||||
// for example, the vector {1, 2, 3, 5, 6, 9} would be converted to {{1, 3}, {5, 6}, {9, 9}}
|
||||
typedef QVector<QPair<int, int>> ColumnRanges;
|
||||
auto accumulator = [](ColumnRanges ranges, int column) {
|
||||
if (ranges.isEmpty() || (ranges.last().second < column - 1))
|
||||
{
|
||||
ranges.append(qMakePair(column, column));
|
||||
}
|
||||
else
|
||||
{
|
||||
ranges[ranges.count() - 1].second = column;
|
||||
}
|
||||
return ranges;
|
||||
};
|
||||
const auto ranges =
|
||||
std::accumulate(m_changedColumns.cbegin(), m_changedColumns.cend(), ColumnRanges(), accumulator);
|
||||
|
||||
for (const auto &columnRange : ranges)
|
||||
{
|
||||
QModelIndex topLeft = BaseModel::index(m_firstChangedRow, columnRange.first);
|
||||
QModelIndex bottomRight = BaseModel::index(m_lastChangedRow, columnRange.second);
|
||||
Q_EMIT BaseModel::dataChanged(topLeft, bottomRight, roles);
|
||||
}
|
||||
|
||||
m_firstChangedRow = -1;
|
||||
}
|
||||
|
||||
void addChange(int row, const QVector<int> &columns, const QVector<int> &roles)
|
||||
{
|
||||
auto mergeColumns = [this](const QVector<int> &columns) {
|
||||
QVector<int> columnUnion;
|
||||
// we have to cast this to QAbstractItemModel, to work around QAbstractListModel making columnCount private
|
||||
columnUnion.reserve(static_cast<QAbstractItemModel *>(this)->columnCount());
|
||||
auto inserter = std::back_inserter(columnUnion);
|
||||
std::set_union(m_changedColumns.cbegin(), m_changedColumns.cend(), columns.cbegin(), columns.cend(),
|
||||
inserter);
|
||||
m_changedColumns = std::move(columnUnion);
|
||||
};
|
||||
|
||||
if (m_firstChangedRow > -1)
|
||||
{
|
||||
// from here rows are guaranteed to be sequential, so no need to check for that
|
||||
m_lastChangedRow = row; // optimistic, but lets see...
|
||||
|
||||
switch (m_changeMergePolicy)
|
||||
{
|
||||
case AlwaysMergeNeighbouringRows:
|
||||
m_changedRoles = m_changedRoles.intersect(setFromContainer(roles));
|
||||
mergeColumns(columns);
|
||||
return;
|
||||
case MergeWhenColumnsMatch:
|
||||
if (m_changedColumns == columns)
|
||||
{
|
||||
m_changedRoles = m_changedRoles.intersect(setFromContainer(roles));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case MergeWhenRolesMatch:
|
||||
if (m_changedRoles == setFromContainer(roles))
|
||||
{
|
||||
mergeColumns(columns);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case MergeOnPerfectMatch:
|
||||
if (m_changedColumns == columns && m_changedRoles == setFromContainer(roles))
|
||||
{
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
m_lastChangedRow = row - 1; // we were too optimistic
|
||||
flushCachedChanges();
|
||||
}
|
||||
|
||||
// just copy the details of the first change
|
||||
m_firstChangedRow = row;
|
||||
m_lastChangedRow = row;
|
||||
m_changedColumns = columns;
|
||||
m_changedRoles = setFromContainer(roles);
|
||||
}
|
||||
|
||||
ChangeMergePolicy m_changeMergePolicy = MergeOnPerfectMatch;
|
||||
int m_firstChangedRow = -1;
|
||||
int m_lastChangedRow = -1;
|
||||
QVector<int> m_changedColumns;
|
||||
QSet<int> m_changedRoles;
|
||||
};
|
||||
|
||||
#undef ForwardIt
|
||||
#undef RandomIt
|
||||
#undef Container
|
||||
#undef BinaryPredicate
|
||||
#undef EventHandler
|
||||
#undef QAIM
|
||||
|
||||
#endif // UPDATEABLEMODEL_H
|
||||
Reference in new issue
Block a user